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
34 changes: 29 additions & 5 deletions packages/connect-react/src/components/append/AppendInitScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined>(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),
Expand Down Expand Up @@ -130,6 +138,10 @@ const AppendInitScreen = () => {
}, []);

const handleSubmit = useCallback(async () => {
if (appendLoading || skipping) {
return;
}

setAppendLoading(true);
setErrorMessage(undefined);

Expand All @@ -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}`);
Expand Down Expand Up @@ -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 <></>;
Expand All @@ -199,9 +220,12 @@ const AppendInitScreen = () => {
<AppendInitLoaded2
errorMessage={errorMessage}
appendLoading={appendLoading}
handleShowBenefits={() => setAppendInitState(AppendInitState.ShowBenefits)}
handleShowBenefits={() => {
void onReadMoreClick();
setAppendInitState(AppendInitState.ShowBenefits);
}}
handleSubmit={() => void handleSubmit()}
handleSkip={() => void handleSituation(AppendSituationCode.ExplicitSkipByUser)}
handleSkip={() => onSkip()}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.</>;
Expand All @@ -31,7 +33,15 @@ const AppendSuccessScreen = ({ aaguidName }: Props) => {
<div className='cb-append-success-cta'>
<PrimaryButton
className='cb-append-success-cta-continue'
onClick={() => void config.onComplete('complete')}
isLoading={completing}
onClick={() => {
if (completing) {
return;
}

setCompleting(true);
void config.onComplete('complete');
}}
>
Continue
</PrimaryButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export const PasskeyButton = ({ identifier, isLoading, onClick }: Props) => {
className='cb-passkey-button'
onClick={(e: FormEvent) => {
e.preventDefault();

if (isLoading) {
return;
}

onClick();
}}
>
Expand Down
2 changes: 2 additions & 0 deletions packages/connect-react/src/contexts/AppendProcessContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface AppendProcessContextProps {
handleErrorHard: (situation: AppendSituationCode, expected: boolean) => Promise<void>;
handleCredentialExistsError: () => Promise<void>;
handleSkip: (situation: AppendSituationCode, explicit?: boolean) => Promise<void>;
onReadMoreClick: () => Promise<void>;
}

export const initialContext: AppendProcessContextProps = {
Expand All @@ -28,6 +29,7 @@ export const initialContext: AppendProcessContextProps = {
handleErrorHard: missingImplementation,
handleCredentialExistsError: missingImplementation,
handleSkip: missingImplementation,
onReadMoreClick: missingImplementation,
};

const AppendProcessContext = createContext<AppendProcessContextProps>(initialContext);
Expand Down
5 changes: 5 additions & 0 deletions packages/connect-react/src/contexts/AppendProcessProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const AppendProcessProvider: FC<PropsWithChildren<Props>> = ({ children,
[getConnectService, config],
);

const onReadMoreClick = useCallback(async () => {
await getConnectService().recordEventAppendLearnMore();
}, [getConnectService, config]);

const handleCredentialExistsError = useCallback(async () => {
log.debug('error (credential-exists)');

Expand All @@ -81,6 +85,7 @@ export const AppendProcessProvider: FC<PropsWithChildren<Props>> = ({ children,
handleErrorHard,
handleCredentialExistsError,
handleSkip,
onReadMoreClick,
}),
[currentScreenType, navigateToScreen, config],
);
Expand Down
2 changes: 1 addition & 1 deletion packages/web-core/openapi/spec_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/web-core/src/api/v2/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
4 changes: 4 additions & 0 deletions packages/web-core/src/services/ConnectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading