Skip to content

Commit

Permalink
feat: add email verification code registration sample
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Oct 23, 2024
1 parent 0c594d5 commit b171a45
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 10 deletions.
21 changes: 20 additions & 1 deletion packages/shared/scss/normalized.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,34 @@ form {
}
}

.flex-wrapper {
display: flex;
align-items: center;
width: 100%;
gap: 10px;
}

input {
height: 24px;
flex: 1;
line-height: 24px;
outline: none;
padding: 10px;
margin: 0;
border-radius: 8px;
border: 1px solid var(--color-line-border);
}

.button {
height: 46px;
border: 1px solid var(--color-line-border);
border-radius: 8px;
font: var(--font-label-3);

&:not(:disabled):hover {
cursor: pointer;
}
}

.submit-button {
display: flex;
align-items: center;
Expand Down
25 changes: 25 additions & 0 deletions packages/sign-up-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.
Binary file not shown.
35 changes: 35 additions & 0 deletions packages/sign-up-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 class="logo" 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;">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>Register</span>
<div class="spinner"></div>
</button>
</form>
<div class="error-message hidden"></div>
</div>
</body>
</html>
32 changes: 32 additions & 0 deletions packages/sign-up-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-up",
"description": "A sample project demonstrates how to use Logto Experience API to build a verification code sign-up page.",
"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.0"
},
"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';
118 changes: 118 additions & 0 deletions packages/sign-up-with-verification-code/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Api } from '@logto/experience-sample-shared/api';
import logo from '@logto/experience-sample-shared/assets/logto-logo-light.svg';
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', () => {
document.querySelector('.logo')?.setAttribute('src', logo);
const form = document.querySelector('form');
const sendCodeButton = document.querySelector('.button');
const submitButton = document.querySelector('.submit-button');
const errorContainer = document.querySelector('.error-message');

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();

await api.experience.initInteraction(
{ interactionEvent: InteractionEvent.Register },
{ format: 'json' }
);
const { verificationId: id } = await api.experience.createAndSendVerificationCode({
identifier: { type: 'email', value: email },
interactionEvent: InteractionEvent.Register,
});

verificationId = id;
} catch (error) {
handleError(error);
}
});

form?.addEventListener('submit', async (event) => {
event.preventDefault();
errorContainer?.classList.remove('hidden');
const formData = new FormData(form);
const email = formData.get('email')?.toString();
const verificationCode = formData.get('verification-code')?.toString();

try {
submitButton?.setAttribute('disabled', 'disabled');
submitButton?.classList.add('loading');

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

await api.experience.verifyVerificationCodeVerification(
{
identifier: { type: 'email', value: email },
verificationId,
code: verificationCode,
},
{ format: 'json' }
);

await api.experience.identifyUser({ verificationId });

const { redirectTo } = await api.experience.submitInteraction({ format: 'json' });
window.location.replace(redirectTo);
} catch (error) {
handleError(error);
}
});
});

const handleError = (error: unknown) => {
const errorContainer = document.querySelector('.error-message');
const submitButton = document.querySelector('.submit-button');

console.error(error);
if (errorContainer) {
errorContainer.classList.remove('hidden');
errorContainer.innerHTML =
error instanceof Error
? error.message
: 'Error occurred. Please check debugger console for details.';
}

submitButton?.removeAttribute('disabled');
submitButton?.classList.remove('loading');
};
11 changes: 11 additions & 0 deletions packages/sign-up-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"]
}
51 changes: 42 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b171a45

Please sign in to comment.