diff --git a/packages/connect-react/src/components/append/AppendInitScreen.tsx b/packages/connect-react/src/components/append/AppendInitScreen.tsx index 5dfbf363f..e775ff3ca 100644 --- a/packages/connect-react/src/components/append/AppendInitScreen.tsx +++ b/packages/connect-react/src/components/append/AppendInitScreen.tsx @@ -19,13 +19,21 @@ export enum AppendInitState { } const AppendInitScreen = () => { - const { config, navigateToScreen, handleErrorHard, handleErrorSoft, handleSkip, handleCredentialExistsError } = - useAppendProcess(); + const { + config, + navigateToScreen, + handleErrorHard, + handleErrorSoft, + handleSkip, + handleCredentialExistsError, + onReadMoreClick, + } = useAppendProcess(); const { getConnectService } = useShared(); const [attestationOptions, setAttestationOptions] = useState(''); const [errorMessage, setErrorMessage] = useState(undefined); const [appendLoading, setAppendLoading] = useState(false); const [appendInitState, setAppendInitState] = useState(AppendInitState.SilentLoading); + const [skipping, setSkipping] = useState(false); const statefulLoader = useRef( new StatefulLoader( () => setAppendInitState(AppendInitState.Loading), @@ -130,6 +138,10 @@ const AppendInitScreen = () => { }, []); const handleSubmit = useCallback(async () => { + if (appendLoading || skipping) { + return; + } + setAppendLoading(true); setErrorMessage(undefined); @@ -151,7 +163,7 @@ const AppendInitScreen = () => { aaguidName: res.val.passkeyOperation.aaguidDetails?.name, aaguidIcon: res.val.passkeyOperation.aaguidDetails?.iconLight, }); - }, [attestationOptions, config, getConnectService]); + }, [attestationOptions, config, getConnectService, appendLoading, skipping]); const handleSituation = async (situationCode: AppendSituationCode) => { log.debug(`situation: ${situationCode}`); @@ -187,6 +199,15 @@ const AppendInitScreen = () => { } }; + const onSkip = useCallback(() => { + if (skipping || appendLoading) { + return; + } + + setSkipping(true); + void handleSituation(AppendSituationCode.ExplicitSkipByUser); + }, [skipping, appendLoading]); + switch (appendInitState) { case AppendInitState.SilentLoading: return <>; @@ -199,9 +220,12 @@ const AppendInitScreen = () => { setAppendInitState(AppendInitState.ShowBenefits)} + handleShowBenefits={() => { + void onReadMoreClick(); + setAppendInitState(AppendInitState.ShowBenefits); + }} handleSubmit={() => void handleSubmit()} - handleSkip={() => void handleSituation(AppendSituationCode.ExplicitSkipByUser)} + handleSkip={() => onSkip()} /> ); } diff --git a/packages/connect-react/src/components/append/AppendSuccessScreen.tsx b/packages/connect-react/src/components/append/AppendSuccessScreen.tsx index 9d1c1e27c..dc902339e 100644 --- a/packages/connect-react/src/components/append/AppendSuccessScreen.tsx +++ b/packages/connect-react/src/components/append/AppendSuccessScreen.tsx @@ -12,6 +12,8 @@ type Props = { const AppendSuccessScreen = ({ aaguidName }: Props) => { const { config } = useAppendProcess(); + const [completing, setCompleting] = React.useState(false); + let passkeyStoredTxt = <>Your passkey has been stored.; if (aaguidName) { passkeyStoredTxt = <>Your passkey is stored in {aaguidName}.; @@ -31,7 +33,15 @@ const AppendSuccessScreen = ({ aaguidName }: Props) => {
void config.onComplete('complete')} + isLoading={completing} + onClick={() => { + if (completing) { + return; + } + + setCompleting(true); + void config.onComplete('complete'); + }} > Continue diff --git a/packages/connect-react/src/components/shared/PasskeyButton.tsx b/packages/connect-react/src/components/shared/PasskeyButton.tsx index 4b4b2c37c..44f3c863c 100644 --- a/packages/connect-react/src/components/shared/PasskeyButton.tsx +++ b/packages/connect-react/src/components/shared/PasskeyButton.tsx @@ -24,6 +24,11 @@ export const PasskeyButton = ({ identifier, isLoading, onClick }: Props) => { className='cb-passkey-button' onClick={(e: FormEvent) => { e.preventDefault(); + + if (isLoading) { + return; + } + onClick(); }} > diff --git a/packages/connect-react/src/contexts/AppendProcessContext.ts b/packages/connect-react/src/contexts/AppendProcessContext.ts index 62f0cc2aa..300de0de7 100644 --- a/packages/connect-react/src/contexts/AppendProcessContext.ts +++ b/packages/connect-react/src/contexts/AppendProcessContext.ts @@ -17,6 +17,7 @@ export interface AppendProcessContextProps { handleErrorHard: (situation: AppendSituationCode, expected: boolean) => Promise; handleCredentialExistsError: () => Promise; handleSkip: (situation: AppendSituationCode, explicit?: boolean) => Promise; + onReadMoreClick: () => Promise; } export const initialContext: AppendProcessContextProps = { @@ -28,6 +29,7 @@ export const initialContext: AppendProcessContextProps = { handleErrorHard: missingImplementation, handleCredentialExistsError: missingImplementation, handleSkip: missingImplementation, + onReadMoreClick: missingImplementation, }; const AppendProcessContext = createContext(initialContext); diff --git a/packages/connect-react/src/contexts/AppendProcessProvider.tsx b/packages/connect-react/src/contexts/AppendProcessProvider.tsx index fe96b02ec..f8e38bb26 100644 --- a/packages/connect-react/src/contexts/AppendProcessProvider.tsx +++ b/packages/connect-react/src/contexts/AppendProcessProvider.tsx @@ -64,6 +64,10 @@ export const AppendProcessProvider: FC> = ({ children, [getConnectService, config], ); + const onReadMoreClick = useCallback(async () => { + await getConnectService().recordEventAppendLearnMore(); + }, [getConnectService, config]); + const handleCredentialExistsError = useCallback(async () => { log.debug('error (credential-exists)'); @@ -81,6 +85,7 @@ export const AppendProcessProvider: FC> = ({ children, handleErrorHard, handleCredentialExistsError, handleSkip, + onReadMoreClick, }), [currentScreenType, navigateToScreen, config], ); diff --git a/packages/web-core/openapi/spec_v2.yaml b/packages/web-core/openapi/spec_v2.yaml index f8914cba9..d9d8c8f3a 100644 --- a/packages/web-core/openapi/spec_v2.yaml +++ b/packages/web-core/openapi/spec_v2.yaml @@ -1724,7 +1724,7 @@ components: passkeyEventType: type: string - enum: [ login-explicit-abort, login-error, login-error-untyped, login-error-unexpected, login-one-tap-switch, user-append-after-cross-platform-blacklisted, user-append-after-login-error-blacklisted, append-credential-exists, append-explicit-abort, append-error, append-error-unexpected, manage-error-unexpected ] + enum: [ login-explicit-abort, login-error, login-error-untyped, login-error-unexpected, login-one-tap-switch, user-append-after-cross-platform-blacklisted, user-append-after-login-error-blacklisted, append-credential-exists, append-explicit-abort, append-error, append-error-unexpected, manage-error-unexpected, append-learn-more ] blockType: type: string diff --git a/packages/web-core/src/api/v2/api.ts b/packages/web-core/src/api/v2/api.ts index 5ac2a472e..e775e1bb1 100644 --- a/packages/web-core/src/api/v2/api.ts +++ b/packages/web-core/src/api/v2/api.ts @@ -1890,7 +1890,8 @@ export const PasskeyEventType = { AppendExplicitAbort: 'append-explicit-abort', AppendError: 'append-error', AppendErrorUnexpected: 'append-error-unexpected', - ManageErrorUnexpected: 'manage-error-unexpected' + ManageErrorUnexpected: 'manage-error-unexpected', + AppendLearnMore: 'append-learn-more' } as const; export type PasskeyEventType = typeof PasskeyEventType[keyof typeof PasskeyEventType]; diff --git a/packages/web-core/src/services/ConnectService.ts b/packages/web-core/src/services/ConnectService.ts index bff8163ab..57afd2a5c 100644 --- a/packages/web-core/src/services/ConnectService.ts +++ b/packages/web-core/src/services/ConnectService.ts @@ -625,6 +625,10 @@ export class ConnectService { return this.#recordEvent(PasskeyEventType.AppendExplicitAbort, undefined, challenge); } + recordEventAppendLearnMore() { + return this.#recordEvent(PasskeyEventType.AppendLearnMore); + } + // This function can be used to catch events that would usually not create backend interaction (e.g. when a passkey ceremony is canceled) #recordEvent(eventType: PasskeyEventType, message?: string, challenge?: string) { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId);