Skip to content

feat: add Consent Screen for user consent management #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 23, 2025
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
19 changes: 13 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,22 @@ const MfaRecoveryCodeEnrollmentScreen = React.lazy(() => import("./screens/mfa-r
const ResetPasswordMfaPhoneChallengeScreen = React.lazy(() => import("./screens/reset-password-mfa-phone-challenge"));
const PasskeyEnrollmentScreen = React.lazy(() => import("./screens/passkey-enrollment"));
const MfaRecoveryCodeChallengeNewCodeScreen = React.lazy(() => import("./screens/mfa-recovery-code-challenge-new-code"));
const EmailOTPChallengeScreen = React.lazy(() => import("./screens/email-otp-challenge"));
// const EmailOTPChallengeScreen = React.lazy(() => import("./screens/email-otp-challenge"));
const LogoutScreen = React.lazy(() => import("./screens/logout"));
const LogoutAbortedScreen = React.lazy(() => import("./screens/logout-aborted"));
const LogoutCompleteScreen = React.lazy(() => import("./screens/logout-complete"));
const EmailVerificationResultScreen = React.lazy(() => import("./screens/email-verification-result"));
const LoginEmailVerificationScreen = React.lazy(() => import("./screens/login-email-verification"));
const MfaWebAuthnErrorScreen = React.lazy(() => import("./screens/mfa-webauthn-error"));
const MfaWebAuthnPlatformEnrollmentScreen = React.lazy(() => import("./screens/mfa-webauthn-platform-enrollment"));
const MfaWebAuthnNotAvailableErrorScreen = React.lazy(() => import("./screens/mfa-webauthn-not-available-error"))
// const MfaWebAuthnNotAvailableErrorScreen = React.lazy(() => import("./screens/mfa-webauthn-not-available-error"))
const MfaWebAuthnRoamingEnrollment = React.lazy(() => import("./screens/mfa-webauthn-roaming-enrollment"))
const MfaWebAuthnRoamingChallengeScreen = React.lazy(() => import("./screens/mfa-webauthn-roaming-challenge"));
const MfaWebAuthnPlatformChallengeScreen = React.lazy(() => import("./screens/mfa-webauthn-platform-challenge"));
const MfaWebAuthnEnrollmentSuccessScreen = React.lazy(() => import("./screens/mfa-webauthn-enrollment-success"));
const MfaWebAuthnChangeKeyNicknameScreen = React.lazy(() => import("./screens/mfa-webauthn-change-key-nickname"));
// const ConsentScreen = React.lazy(() => import("./screens/consent"));
const ResetPasswordMfaWebAuthnPlatformChallengeScreen = React.lazy(() => import("./screens/reset-password-mfa-webauthn-platform-challenge"));

const App: React.FC = () => {
const [screen, setScreen] = React.useState("login-id");
Expand Down Expand Up @@ -175,8 +177,8 @@ const App: React.FC = () => {
return <PasskeyEnrollmentScreen />;
case "mfa-recovery-code-challenge-new-code":
return <MfaRecoveryCodeChallengeNewCodeScreen />;
case "email-otp-challenge":
return <EmailOTPChallengeScreen />;
// case "email-otp-challenge":
// return <EmailOTPChallengeScreen />;
case "logout":
return <LogoutScreen />;
case "logout-aborted":
Expand All @@ -191,8 +193,8 @@ const App: React.FC = () => {
return <MfaWebAuthnErrorScreen />;
case "mfa-webauthn-platform-enrollment":
return <MfaWebAuthnPlatformEnrollmentScreen />;
case "mfa-webauthn-not-available-error":
return <MfaWebAuthnNotAvailableErrorScreen />
// case "mfa-webauthn-not-available-error":
// return <MfaWebAuthnNotAvailableErrorScreen />
case "mfa-webauthn-roaming-enrollment":
return <MfaWebAuthnRoamingEnrollment />
case "mfa-webauthn-roaming-challenge":
Expand All @@ -201,12 +203,17 @@ const App: React.FC = () => {
return <MfaWebAuthnPlatformChallengeScreen />
case "mfa-webauthn-enrollment-success":
return <MfaWebAuthnEnrollmentSuccessScreen />
case "reset-password-mfa-webauthn-platform-challenge":
return <ResetPasswordMfaWebAuthnPlatformChallengeScreen />;
// case "consent":
// return <ConsentScreen />;
case "mfa-webauthn-change-key-nickname":
return <MfaWebAuthnChangeKeyNicknameScreen />
default:
return <>No screen rendered</>;
}
};

return <Suspense fallback={<div>Loading...</div>}>{renderScreen()}</Suspense>;
};

Expand Down
61 changes: 0 additions & 61 deletions src/screens/customized-consent/index.tsx

This file was deleted.

115 changes: 0 additions & 115 deletions src/screens/email-otp-challenge/index.tsx

This file was deleted.

46 changes: 0 additions & 46 deletions src/screens/mfa-webauthn-not-available-error/index.tsx

This file was deleted.

102 changes: 102 additions & 0 deletions src/screens/reset-password-mfa-webauthn-platform-challenge/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import ResetPasswordMfaWebAuthnPlatformChallenge, {
type ContinueWithPasskeyOptions,
type TryAnotherMethodOptions,
} from '@auth0/auth0-acul-js/reset-password-mfa-webauthn-platform-challenge';

const ResetPasswordMfaWebAuthnPlatformChallengeComponent: React.FC = () => {
const sdk = useMemo(() => new ResetPasswordMfaWebAuthnPlatformChallenge(), []);
const { screen, transaction, client } = sdk;
const texts = screen.texts ?? {};
const { publicKey: publicKeyChallengeOptions, showRememberDevice } = screen;

const [rememberDevice, setRememberDevice] = useState(false);

const handleVerify = useCallback(() => {
const opts: ContinueWithPasskeyOptions = {};
if (showRememberDevice) {
opts.rememberDevice = rememberDevice;
}
sdk.continueWithPasskey(opts);
}, [sdk, rememberDevice, showRememberDevice]);

const handleTryAnotherMethod = useCallback(() => {
const opts: TryAnotherMethodOptions = {}; // Add custom options if needed
sdk.tryAnotherMethod(opts);
}, [sdk]);

// Effect to automatically trigger verification if publicKeyChallengeOptions are available.
// This provides a more seamless UX, prompting the user immediately.
useEffect(() => {
if (publicKeyChallengeOptions) { // Check !isLoading to prevent re-triggering if already in process
console.log("WebAuthn platform challenge options available. Automatically attempting verification.");
handleVerify();
}
}, [handleVerify, publicKeyChallengeOptions]);

return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4 antialiased">
<div className="w-full max-w-md bg-white rounded-lg shadow-xl p-8 space-y-6">
{client.logoUrl && (
<img src={client.logoUrl} alt={client.name ?? 'Client Logo'} className="mx-auto h-12 mb-6" />
)}
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-800">
{texts.title ?? 'Verify Your Identity'}
</h1>
<p className="mt-2 text-gray-600">
{texts.description ?? 'Please use your device\'s screen lock (fingerprint, face, PIN) or a connected security key to continue resetting your password.'}
</p>
</div>

{transaction.errors && transaction.errors.length > 0 && (
<div className="bg-red-50 border-l-4 border-red-400 text-red-700 p-4 rounded-md" role="alert">
<p className="font-bold">{texts.alertListTitle ?? 'Errors:'}</p>
{transaction.errors.map((err, index) => (
<p key={`tx-err-${index}`}>{err.message}</p>
))}
</div>
)}

{showRememberDevice && (
<div className="flex items-center justify-center mt-4">
<input
id="rememberDevice"
name="rememberDevice"
type="checkbox"
checked={rememberDevice}
onChange={(e) => setRememberDevice(e.target.checked)}
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label htmlFor="rememberDevice" className="ml-2 block text-sm text-gray-900">
{texts.rememberMeText ?? 'Remember this device for 30 days'}
</label>
</div>
)}

<div className="space-y-4 mt-6">
{/* Button to manually trigger verification if auto-trigger fails or as a retry option */}
{/* This might only be shown if publicKeyChallengeOptions exist but initial auto-verify failed */}
{publicKeyChallengeOptions && (
<button
onClick={handleVerify}
disabled={!publicKeyChallengeOptions}
className="w-full flex justify-center items-center px-4 py-2.5 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-60 disabled:cursor-not-allowed"
>
{(texts.retryButtonText ?? 'Retry Verification')}
</button>
)}

<button
onClick={handleTryAnotherMethod}
className="w-full flex justify-center px-4 py-2.5 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-60 disabled:cursor-not-allowed"
>
{texts.pickAuthenticatorText ?? 'Try Another Method'}
</button>
</div>
</div>
</div>
);
};

export default ResetPasswordMfaWebAuthnPlatformChallengeComponent;