Skip to content

Commit

Permalink
feat: add sample for email and verification code sign-in (#7)
Browse files Browse the repository at this point in the history
* fix: pnpm lockfile

* feat: add sample for email and verification code sign-in
  • Loading branch information
charIeszhao authored Oct 23, 2024
1 parent f769dbd commit e1f0be6
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 33 deletions.
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

0 comments on commit e1f0be6

Please sign in to comment.