Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-wings-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix issue where the combined flow wouldn't trigger if a phone number was used as an identifier while set as an optional field.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
import type { LoadedClerk, SignUpResource } from '@clerk/types';

import { handleCombinedFlowTransfer, hasOptionalFields } from '../handleCombinedFlowTransfer';

// eslint-disable-next-line no-var -- Jest hoists mock calls to the top of the file, so var is needed.
var mockCompleteSignUpFlow: jest.Mock;
jest.mock('../lazy-sign-up', () => {
mockCompleteSignUpFlow = jest.fn();
return {
lazyCompleteSignUpFlow: () => {
return Promise.resolve(mockCompleteSignUpFlow);
},
};
});

const mockNavigate = jest.fn();
const mockHandleError = jest.fn();

describe('handleCombinedFlowTransfer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should call completeSignUpFlow', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockResolvedValue({}),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['phone_number'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).not.toHaveBeenCalled();
expect(mockClerk.client.signUp.create).toHaveBeenCalled();
expect(mockCompleteSignUpFlow).toHaveBeenCalled();
});

it('should call navigate if password is enabled', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['password'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'phoneNumber',
identifierValue: '+1234567890',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: true,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if identifier is username', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: [],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'username',
identifierValue: 'test',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});

it('should call navigate if first_name is optional', async () => {
const mockClerk = {
client: {
signUp: {
create: jest.fn().mockImplementation((...args) => Promise.resolve(args)),
optionalFields: ['first_name'],
},
},
};

await handleCombinedFlowTransfer({
identifierAttribute: 'emailAddress',
identifierValue: 'test@test.com',
signUpMode: 'public',
navigate: mockNavigate,
handleError: mockHandleError,
clerk: mockClerk as unknown as LoadedClerk,
afterSignUpUrl: 'https://test.com',
passwordEnabled: false,
});

expect(mockNavigate).toHaveBeenCalled();
expect(mockClerk.client.signUp.create).not.toHaveBeenCalled();
expect(mockCompleteSignUpFlow).not.toHaveBeenCalled();
});
});

describe('hasOptionalFields', () => {
it('should return true if there are optional fields', () => {
const signUp = {
optionalFields: ['legal_accepted'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(true);
});

it('should return false if the identifier attribute is phoneNumber and the optional field is phone_number', () => {
const signUp = {
optionalFields: ['phone_number'],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});

it('should return false if there are no optional fields', () => {
const signUp = {
optionalFields: [],
} as unknown as SignUpResource;

expect(hasOptionalFields(signUp, 'phoneNumber')).toBe(false);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function handleCombinedFlowTransfer({
// inform us if the instance is eligible for moving directly to verification.
if (
!passwordEnabled &&
!hasOptionalFields(clerk.client.signUp) &&
!hasOptionalFields(clerk.client.signUp, identifierAttribute) &&
(identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber')
) {
return clerk.client.signUp
Expand All@@ -95,14 +95,28 @@ export function handleCombinedFlowTransfer({
return navigate(`create`, { searchParams: paramsToForward });
}

Comment thread
dstaley marked this conversation as resolved.
function hasOptionalFields(signUp: SignUpResource) {
const filteredFields = signUp.optionalFields.filter(
field =>
!field.startsWith('oauth_') &&
!field.startsWith('web3_') &&
field !== 'password' &&
field !== 'enterprise_sso' &&
field !== 'saml',
);
export function hasOptionalFields(
signUp: SignUpResource,
identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username',
) {
const filteredFields = signUp.optionalFields.filter(field => {
// OAuth, Web3, and SAML fields, while optional, are not relevant once sign up has been initiated with an identifier.
if (field.startsWith('oauth_') || field.startsWith('web3_') || ['enterprise_sso', 'saml'].includes(field)) {
return false;
}

// We already check for whether password is enabled, so we don't consider it an optional field.
if (field === 'password') {
return false;
}

Comment thread
dstaley marked this conversation as resolved.
// If a phone number is used as the identifier, we don't need to consider the phone_number field.
if (identifierAttribute === 'phoneNumber' && field === 'phone_number') {
return false;
}

return true;
});

return filteredFields.length > 0;
}