Skip to content
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

feat: add sample for email and verification code sign-in #7

Merged
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
25 changes: 25 additions & 0 deletions packages/sign-in-with-verification-code/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: '@silverhand/eslint-config',
rules: {
'jsx-a11y/no-autofocus': 'off',
'unicorn/prefer-string-replace-all': 'off',
'no-restricted-syntax': 'off',
'@silverhand/fp/no-mutation': 'off',
'@silverhand/fp/no-let': 'off',
},
overrides: [
{
files: ['*.config.js', '*.config.ts', '*.d.ts'],
rules: {
'import/no-unused-modules': 'off',
},
},
{
files: ['*.d.ts'],
rules: {
'import/no-unassigned-import': 'off',
},
},
],
};
Binary file not shown.
35 changes: 35 additions & 0 deletions packages/sign-in-with-verification-code/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Logto experience sample</title>
<link rel="icon" href="./favicon.ico" />
<script type="module" src="src/index.ts"></script>
</head>

<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="app">
<form>
<img fetchpriority="high" class="logo" src="https://logto.io/logo.svg" alt="Logto branding logo" />
<div class="input-field">
<label for="email">Email</label>
<div class="flex-wrapper">
<input type="text" name="email" required />
<button class="button" style="width:100px" type="button">Send code</button>
</div>
</div>
<div class="input-field">
<label for="verification-code">Verification code</label>
<input type="text" name="verification-code" required />
</div>
<button class="submit-button" type="submit">
<span>Sign in</span>
<div class="spinner"></div>
</button>
</form>
<div class="error-message hidden"></div>
</div>
</body>
</html>
32 changes: 32 additions & 0 deletions packages/sign-in-with-verification-code/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@logto/experience-sample-verification-code-sign-in",
"description": "A sample project demonstrates how to use Logto Experience API to build a passwordless sign-in page with verification code via email or phone.",
"author": "Silverhand Inc. <contact@silverhand.io>",
"license": "MIT",
"version": "0.0.0",
"type": "module",
"scripts": {
"precommit": "lint-staged",
"start": "vite",
"dev": "logto-tunnel --verbose & vite",
"build": "tsc -b && vite build",
"lint": "eslint --ext .ts src",
"preview": "vite preview"
},
"devDependencies": {
"@logto/experience-sample-shared": "workspace:^",
"@logto/schemas": "^1.19.0",
"@silverhand/eslint-config": "^6.0.1",
"@silverhand/ts-config": "^6.0.0",
"eslint": "^8.56.0",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0",
"stylelint": "^15.0.0",
"typescript": "^5.5.3",
"vite": "^5.4.6"
},
"stylelint": {
"extends": "@silverhand/eslint-config/.stylelintrc"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'vite/client';
128 changes: 128 additions & 0 deletions packages/sign-in-with-verification-code/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Api } from '@logto/experience-sample-shared/api';
import { clearError, handleError, setSubmitLoading } from '@logto/experience-sample-shared/utils';
import { InteractionEvent } from '@logto/schemas';

import '@logto/experience-sample-shared/scss/normalized.scss';

const defaultResendCodeTimeout = 60;
const api = new Api({ baseUrl: window.location.origin });

window.addEventListener('load', () => {
const form = document.querySelector('form');
const sendCodeButton = document.querySelector('.button');

let verificationId = '';

sendCodeButton?.addEventListener('click', async (event) => {
event.preventDefault();
if (!form) {
return;
}

const email = new FormData(form).get('email')?.toString();

try {
if (!email) {
throw new Error('Email is required.');
}

sendCodeButton.setAttribute('disabled', 'disabled');
sendCodeButton.innerHTML = `Resend (in ${defaultResendCodeTimeout}s)`;

let timeoutId = -1;
let remainingSeconds = defaultResendCodeTimeout - 1;

const countDown = () => {
timeoutId = window.setTimeout(() => {
if (remainingSeconds > 0) {
sendCodeButton.innerHTML = `Resend (in ${remainingSeconds}s)`;
remainingSeconds--;
countDown();
} else {
window.clearTimeout(timeoutId);
sendCodeButton.innerHTML = 'Resend';
sendCodeButton.removeAttribute('disabled');
}
}, 1000);
};

countDown();

/**
* Step 1: Initialize a sign-in type interaction.
*/
await api.experience.initInteraction({ interactionEvent: InteractionEvent.SignIn });

/**
* Step 2: Create a verification record and send out the verification code.
* Note: You can also use `type: 'phone'` if that's your sign-in identifier.
*/
const { verificationId: id } = await api.experience.createAndSendVerificationCode({
identifier: { type: 'email', value: email },
interactionEvent: InteractionEvent.SignIn,
});

// Save the verificationId for later use.
verificationId = id;
} catch (error) {
handleError(error);
}
});

form?.addEventListener('submit', async (event) => {
event.preventDefault();
setSubmitLoading(true);
clearError();

try {
const formData = new FormData(form);
const email = formData.get('email')?.toString();
const verificationCode = formData.get('verification-code')?.toString();

if (!email || !verificationCode) {
throw new Error('Email and verification code are required.');
}

/**
* Step 3: Verify the verification code, with the code and verification ID received from step 2.
*/
await api.experience.verifyVerificationCodeVerification({
identifier: { type: 'email', value: email },
verificationId,
code: verificationCode,
});

/**
* Step 4: Identify the user.
*/
try {
await api.experience.identifyUser({ verificationId });
} catch {
/**
* If the user is not yet registered, this API will returned an error with code 'user.user_not_exist'.
*
* In this case, you have two options:
* 1. Show the error and redirect the user to the sign-up page.
* 2. Prompt the user for auto-register and continue the flow.
*
* Example code for auto-register:
* --------------------------------------------
* // Update the interaction event to 'Register'.
* await api.experience.updateInteractionEvent({ interactionEvent: InteractionEvent.Register });
*
* // Identify the user again.
* await api.experience.identifyUser({ verificationId });
*/
}

/**
* Step 5: Submit the interaction to complete the sign-in process. Redirect back to your app via
* the "Redirect URI" you configured in Logto Admin Console.
*/
const { redirectTo } = await api.experience.submitInteraction({ format: 'json' });
window.location.replace(redirectTo);
} catch (error) {
handleError(error);
}
});
});
11 changes: 11 additions & 0 deletions packages/sign-in-with-verification-code/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"baseUrl": "./",
"outDir": "dist",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
3 changes: 2 additions & 1 deletion packages/sign-up-with-verification-code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ window.addEventListener('load', () => {

/**
* Step 2: Create a verification record and send out the verification code.
* Note: You can also use `type: 'phone'` if that's your sign-in identifier.
*/
const { verificationId: id } = await api.experience.createAndSendVerificationCode({
identifier: { type: 'email', value: email },
Expand Down Expand Up @@ -83,7 +84,7 @@ window.addEventListener('load', () => {
}

/**
* Step 3: Verify the verification code.
* Step 3: Verify the verification code, with the code and verification ID received from step 2.
*/
await api.experience.verifyVerificationCodeVerification({
identifier: { type: 'email', value: email },
Expand Down
Loading