Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
33732bd
Add code stubs from SPIKE
yunakim714 Jan 9, 2026
49d41c6
Call out to authorizeWebauthnRegistration auth helper
yunakim714 Jan 12, 2026
016ac69
Merge branch 'feature/webauthn-login' into W-20474693-passkey-creation
yunakim714 Jan 12, 2026
ba2e2e8
Show toast on account page if customer is registered
yunakim714 Jan 12, 2026
2d8e140
pull in OTP Auth modal changes from 1 click checkout feature branch
hajinsuha1 Dec 31, 2025
d626a93
Add OTP auth modal
yunakim714 Jan 13, 2026
7a4b57c
Add test files
yunakim714 Jan 13, 2026
028e560
Only show Create Passkey toast if webauthn is browser compatible
yunakim714 Jan 13, 2026
9cab5e2
Only show toast on successful login or account creation
yunakim714 Jan 14, 2026
319d109
Show passkey toast on checkout page
yunakim714 Jan 14, 2026
240a7b8
Add close button to toast
yunakim714 Jan 14, 2026
13704ab
Create passkey context so modal opens from anywhere within the storef…
yunakim714 Jan 15, 2026
6c8f205
Add test file for provider:
yunakim714 Jan 15, 2026
152e35d
Localize text
yunakim714 Jan 15, 2026
dd47c53
Localize text
yunakim714 Jan 15, 2026
f80a2bd
Remove passkey creation from checkout
yunakim714 Jan 16, 2026
a180a8b
Lint
yunakim714 Jan 16, 2026
674b624
Cleanup
yunakim714 Jan 16, 2026
608910a
Fix bug
yunakim714 Jan 16, 2026
4e56ffe
Call startWebauthnUserRegistration auth helper from passkey modal
yunakim714 Jan 16, 2026
405ee0f
Update to preview version of commerce-sdk-react for now - REVERT when…
yunakim714 Jan 16, 2026
f4e8321
Lint
yunakim714 Jan 16, 2026
83de486
Remove eslint comments
yunakim714 Jan 20, 2026
4c53b55
Merge branch 'W-20474693-passkey-creation' into W-20224082-passkey-re…
yunakim714 Jan 20, 2026
6b45907
Call finish endpoint upon successful start
yunakim714 Jan 20, 2026
47d5556
Merge branch 'feature/webauthn-login' into W-20224082-passkey-registr…
yunakim714 Jan 20, 2026
8399163
Update handleOTPVerification
yunakim714 Jan 21, 2026
068b055
Merge branch 'W-20224082-passkey-registration' of github.com:Salesfor…
yunakim714 Jan 21, 2026
f92b0b9
add tests for handleotpverification
yunakim714 Jan 21, 2026
f9854b7
Fix credential object format
yunakim714 Jan 22, 2026
9e3d05f
Lint
yunakim714 Jan 26, 2026
830c9ce
Update public key format
yunakim714 Jan 26, 2026
04283a3
Use built in PublicKeyCredential methods
yunakim714 Jan 27, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const OtpAuth = ({
resendAttempt: true
})
await handleSendEmailOtp(form.getValues('email'))
otpInputs.clear()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should clear the OTP input when shopper clicks Resend Code

} catch (error) {
setResendTimer(0)
await track('/otp-resend-failed', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {
const config = getConfig()
const webauthnConfig = config.app.login.passkey
const authorizeWebauthnRegistration = useAuthHelper(AuthHelpers.AuthorizeWebauthnRegistration)
const startWebauthnUserRegistration = useAuthHelper(AuthHelpers.StartWebauthnUserRegistration)
const finishWebauthnUserRegistration = useAuthHelper(AuthHelpers.FinishWebauthnUserRegistration)

const handleRegisterPasskey = async () => {
setIsLoading(true)
Expand Down Expand Up @@ -82,9 +84,128 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {
}
}

/**
* Convert base64url string to ArrayBuffer
*/
const base64UrlToArrayBuffer = (base64Url) => {
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const binaryString = atob(base64)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
}

/**
* Convert ArrayBuffer to base64url string
*/
const arrayBufferToBase64Url = (buffer) => {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
const base64 = btoa(binary)
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}

const handleOtpVerification = async (code) => {
// TODO: Implement OTP verification
return {success: true}
setIsLoading(true)
setError(null)

try {
// Step 1: Start WebAuthn registration
const response = await startWebauthnUserRegistration.mutateAsync({
user_id: customer.email,
pwd_action_token: code,
...(passkeyNickname && {nick_name: passkeyNickname})
})

// Step 2: Convert response to WebAuthn PublicKeyCredentialCreationOptions format
const publicKey = {
challenge: base64UrlToArrayBuffer(response.challenge),
rp: {
name: response.rp.name,
id: response.rp.id
},
user: {
...response.user,
id: base64UrlToArrayBuffer(response.user.id)
},
pubKeyCredParams: response.pubKeyCredParams || [],
authenticatorSelection: response.authenticatorSelection,
timeout: response.timeout,
attestation: response.attestation || 'none'
}

// Step 3: Call navigator.credentials.create()
if (!navigator.credentials || !navigator.credentials.create) {
throw new Error('WebAuthn API not available in this browser')
}

// navigator.credentials.create() will show a browser/system prompt
// This may appear to hang if the user doesn't interact with the prompt
let credential
try {
credential = await navigator.credentials.create({
publicKey
})
} catch (createError) {
// Handle user cancellation or other errors from the WebAuthn API
if (createError.name === 'NotAllowedError' || createError.name === 'AbortError') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we include a comment on when each of these errors are thrown?
e.g., AbortError implies the action was stopped, NotAllowedError implies a lack of permission or authorization.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be addressed in the error handling ticket - will note in the description of the new work item!

throw new Error('Passkey registration was cancelled or timed out')
}
throw createError
}

if (!credential) {
throw new Error('Failed to create credential: user cancelled or operation failed')
}

// Step 4: Convert credential to JSON format
const clientExtensionResults = credential.getClientExtensionResults?.() || {}
const credentialJson = {
type: credential.type,
id: credential.id,
rawId: arrayBufferToBase64Url(credential.rawId),
response: {
attestationObject: arrayBufferToBase64Url(
credential.response.attestationObject
),
clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON)
},
...(Object.keys(clientExtensionResults).length > 0 && {clientExtensionResults})
}

// Step 5: Finish WebAuthn registration
await finishWebauthnUserRegistration.mutateAsync({
username: customer.email,
credential: credentialJson,
pwd_action_token: code
})

// Step 6: Close OTP modal and main modal on success
setIsOtpAuthOpen(false)
onClose()

return {success: true}
} catch (err) {
const errorMessage =
err.message ||
formatMessage({
id: 'passkey_registration.modal.error.registration_failed',
defaultMessage: 'Failed to register passkey'
})
Comment on lines +175 to +180
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we always return the localized error message and never the api error mesage? or map error messages from the API to a localized message similar to what we did here: https://salesforce.quip.com/97bPANYv5D2U

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking through this again, we will definitely need a localized error for some errors like when the /webauthn/register/authorize returns "Too many webauthn user authorization requests were made. Please try again later.".

How about we keep the error handling generic and create a separate story for error handling for registration and login to make sure we handle all the cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Return error result for OTP component to display
return {
success: false,
error: errorMessage
}
} finally {
setIsLoading(false)
}
}

const resetState = () => {
Expand Down
Loading
Loading