@hanzo/ui 8.27.35 → 8.27.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/SignIn.cjs +180 -17
- package/dist/auth/SignIn.d.ts +25 -1
- package/dist/auth/SignIn.d.ts.map +1 -1
- package/dist/auth/SignIn.js +146 -18
- package/dist/auth/SignIn.js.map +1 -1
- package/dist/consent/Consent.cjs +35 -14
- package/dist/consent/Consent.d.ts +1 -1
- package/dist/consent/Consent.d.ts.map +1 -1
- package/dist/consent/Consent.js +36 -15
- package/dist/consent/Consent.js.map +1 -1
- package/dist/gallery.cjs +2 -1
- package/dist/gallery.d.ts.map +1 -1
- package/dist/gallery.html +8 -4
- package/dist/gallery.js +3 -2
- package/dist/gallery.js.map +1 -1
- package/dist/marketing/ClosingCta.cjs +1 -1
- package/dist/marketing/ClosingCta.js +1 -1
- package/dist/marketing/ClosingCta.js.map +1 -1
- package/dist/marketing/Faq.cjs +1 -1
- package/dist/marketing/Faq.js +1 -1
- package/dist/marketing/Faq.js.map +1 -1
- package/dist/marketing/Footer.cjs +1 -1
- package/dist/marketing/Footer.js +1 -1
- package/dist/marketing/Footer.js.map +1 -1
- package/dist/marketing/Hero.cjs +2 -2
- package/dist/marketing/Hero.js +2 -2
- package/dist/marketing/Hero.js.map +1 -1
- package/dist/marketing/Plans.cjs +1 -1
- package/dist/marketing/Plans.js +1 -1
- package/dist/marketing/Plans.js.map +1 -1
- package/dist/marketing/blocks.cjs +3 -3
- package/dist/marketing/blocks.js +3 -3
- package/dist/marketing/blocks.js.map +1 -1
- package/dist/onboarding/ChatOnboarding.cjs +1 -1
- package/dist/onboarding/ChatOnboarding.js +2 -2
- package/dist/onboarding/ChatOnboarding.js.map +1 -1
- package/dist/onboarding/Onboarding.cjs +89 -21
- package/dist/onboarding/Onboarding.d.ts +30 -4
- package/dist/onboarding/Onboarding.d.ts.map +1 -1
- package/dist/onboarding/Onboarding.js +92 -24
- package/dist/onboarding/Onboarding.js.map +1 -1
- package/dist/onboarding/account.cjs +16 -0
- package/dist/onboarding/account.d.ts +21 -0
- package/dist/onboarding/account.d.ts.map +1 -0
- package/dist/onboarding/account.js +14 -0
- package/dist/onboarding/account.js.map +1 -0
- package/dist/onboarding/api.cjs +4 -1
- package/dist/onboarding/api.d.ts +5 -1
- package/dist/onboarding/api.d.ts.map +1 -1
- package/dist/onboarding/api.js +2 -0
- package/dist/onboarding/api.js.map +1 -1
- package/dist/onboarding/frame.cjs +13 -2
- package/dist/onboarding/frame.d.ts +11 -2
- package/dist/onboarding/frame.d.ts.map +1 -1
- package/dist/onboarding/frame.js +12 -2
- package/dist/onboarding/frame.js.map +1 -1
- package/dist/onboarding/hooks.cjs +25 -7
- package/dist/onboarding/hooks.d.ts +15 -4
- package/dist/onboarding/hooks.d.ts.map +1 -1
- package/dist/onboarding/hooks.js +26 -9
- package/dist/onboarding/hooks.js.map +1 -1
- package/dist/onboarding/index.cjs +10 -2
- package/dist/onboarding/index.d.ts +6 -4
- package/dist/onboarding/index.d.ts.map +1 -1
- package/dist/onboarding/index.js +6 -4
- package/dist/onboarding/index.js.map +1 -1
- package/dist/onboarding/screens.cjs +14 -3
- package/dist/onboarding/screens.d.ts +10 -1
- package/dist/onboarding/screens.d.ts.map +1 -1
- package/dist/onboarding/screens.js +13 -3
- package/dist/onboarding/screens.js.map +1 -1
- package/dist/onboarding/state.cjs +17 -2
- package/dist/onboarding/state.d.ts +22 -3
- package/dist/onboarding/state.d.ts.map +1 -1
- package/dist/onboarding/state.js +15 -2
- package/dist/onboarding/state.js.map +1 -1
- package/dist/styles.css +3 -0
- package/package.json +3 -3
package/dist/auth/SignIn.cjs
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
'use client';"use strict";
|
|
2
2
|
'use client';
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.inOrder = void 0;
|
|
4
38
|
exports.SignIn = SignIn;
|
|
5
39
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
40
|
const react_1 = require("react");
|
|
41
|
+
const Iam = __importStar(require("@hanzo/iam"));
|
|
7
42
|
const iam_1 = require("@hanzo/iam");
|
|
8
43
|
const react_2 = require("@hanzo/iam/react");
|
|
9
44
|
const shell_1 = require("@hanzogui/shell");
|
|
@@ -14,6 +49,8 @@ const card_1 = require("../backends/gui/card.cjs");
|
|
|
14
49
|
const field_1 = require("../backends/gui/field.cjs");
|
|
15
50
|
const input_1 = require("../backends/gui/input.cjs");
|
|
16
51
|
const layout_1 = require("../backends/gui/layout.cjs");
|
|
52
|
+
const account_1 = require("../onboarding/account.cjs");
|
|
53
|
+
const session_1 = require("./session.cjs");
|
|
17
54
|
const FIRST = {
|
|
18
55
|
password: true,
|
|
19
56
|
code: true,
|
|
@@ -23,10 +60,83 @@ const FIRST = {
|
|
|
23
60
|
{ name: 'provider-github', type: 'GitHub' },
|
|
24
61
|
],
|
|
25
62
|
};
|
|
63
|
+
/** IAM's answer to a proven code for an address with no account (the SDK's typed `SignupRequired`). */
|
|
64
|
+
const signupRequired = (err) => {
|
|
65
|
+
const kind = Iam.SignupRequired;
|
|
66
|
+
return (typeof kind === 'function' && err instanceof kind) || err?.name === 'SignupRequired';
|
|
67
|
+
};
|
|
68
|
+
const creates = (client) => typeof client.createAccountWithCode === 'function';
|
|
26
69
|
const said = (err) => (err instanceof Error && err.message ? err.message : 'Something went wrong. Try again.');
|
|
27
|
-
|
|
70
|
+
/** The providers in the order they are offered: Google, Apple, GitHub, then any others as IAM lists them. */
|
|
71
|
+
const RANK = ['google', 'apple', 'github'];
|
|
72
|
+
const rank = (p) => {
|
|
73
|
+
const i = RANK.indexOf(p.type.toLowerCase());
|
|
74
|
+
return i < 0 ? RANK.length : i;
|
|
75
|
+
};
|
|
76
|
+
const inOrder = (providers) => [...providers].sort((a, b) => rank(a) - rank(b));
|
|
77
|
+
exports.inOrder = inOrder;
|
|
78
|
+
async function readMethods(serverUrl, clientId) {
|
|
79
|
+
const url = new URL(`${serverUrl.replace(/\/+$/, '')}/v1/iam/auth/methods`);
|
|
80
|
+
url.searchParams.set('clientId', clientId);
|
|
81
|
+
const res = await fetch(url.toString(), { headers: { Accept: 'application/json' } });
|
|
82
|
+
const body = (await res.json());
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw new Error('could not read the sign-in methods');
|
|
85
|
+
const data = (body.status ? body.data : body) ?? {};
|
|
86
|
+
const oauth = (Array.isArray(data.oauth) ? data.oauth : []);
|
|
87
|
+
const named = oauth.filter((p) => typeof p.name === 'string' && typeof p.type === 'string');
|
|
88
|
+
const google = named.find((p) => p.name === 'provider-google');
|
|
89
|
+
const id = google?.clientId ?? google?.client_id;
|
|
90
|
+
return {
|
|
91
|
+
methods: { password: data.password === true, code: data.code === true, signup: data.signup === true, providers: named.map((p) => ({ name: p.name, type: p.type })) },
|
|
92
|
+
extras: { phone: data.phone === true || data.sms === true, google: typeof id === 'string' && id ? { clientId: id, nonce: typeof google?.nonce === 'string' ? google.nonce : undefined } : undefined },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const taps = (client) => typeof client.loginWithGoogleCredential === 'function';
|
|
96
|
+
const GSI = 'https://accounts.google.com/gsi/client';
|
|
97
|
+
/** Load Google Identity Services once, after the page has painted and is idle. */
|
|
98
|
+
function loadGsi() {
|
|
99
|
+
const w = window;
|
|
100
|
+
if (w.google?.accounts?.id)
|
|
101
|
+
return Promise.resolve(w.google);
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const tag = document.createElement('script');
|
|
104
|
+
tag.src = GSI;
|
|
105
|
+
tag.async = true;
|
|
106
|
+
tag.onload = () => (w.google?.accounts?.id ? resolve(w.google) : reject(new Error('Google Identity Services did not load')));
|
|
107
|
+
tag.onerror = () => reject(new Error('Google Identity Services did not load'));
|
|
108
|
+
document.head.appendChild(tag);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const whenIdle = (run) => {
|
|
112
|
+
let done = false;
|
|
113
|
+
const go = () => {
|
|
114
|
+
if (!done)
|
|
115
|
+
run();
|
|
116
|
+
};
|
|
117
|
+
const arm = () => {
|
|
118
|
+
const ric = window.requestIdleCallback;
|
|
119
|
+
if (ric)
|
|
120
|
+
ric(go);
|
|
121
|
+
else
|
|
122
|
+
window.setTimeout(go, 1200);
|
|
123
|
+
};
|
|
124
|
+
if (document.readyState === 'complete')
|
|
125
|
+
arm();
|
|
126
|
+
else
|
|
127
|
+
window.addEventListener('load', arm, { once: true });
|
|
128
|
+
return () => {
|
|
129
|
+
done = true;
|
|
130
|
+
window.removeEventListener('load', arm);
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/** A nonce for one One Tap prompt: IAM's when it sends one, else sixteen random bytes. */
|
|
134
|
+
const nonceFor = (given) => given ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
const PHONE = /^\+?[\d\s().-]{7,}$/;
|
|
136
|
+
function SignIn({ mode = 'login', frame = true, policy, aupPath = '/aup', oneTap = false, site = 'Hanzo', callbackPath = '/auth/callback', providers, via, loginPath = '/login', signupPath = '/signup', termsPath = '/terms', privacyPath = '/privacy', track, authorize = (u) => u, onMethod, onShown, }) {
|
|
28
137
|
const { config, sdk } = (0, react_2.useIam)();
|
|
29
138
|
const [methods, setMethods] = (0, react_1.useState)(providers ? { ...FIRST, providers } : FIRST);
|
|
139
|
+
const [extras, setExtras] = (0, react_1.useState)({ phone: false });
|
|
30
140
|
const [step, setStep] = (0, react_1.useState)('email');
|
|
31
141
|
const [email, setEmail] = (0, react_1.useState)('');
|
|
32
142
|
const [password, setPassword] = (0, react_1.useState)('');
|
|
@@ -37,20 +147,21 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
37
147
|
const origin = via ?? (typeof window === 'undefined' ? '' : window.location.origin);
|
|
38
148
|
const iam = (0, react_1.useMemo)(() => new iam_1.IAM({ ...config, proxyBaseUrl: origin || undefined, redirectUri: origin ? `${origin}${callbackPath}` : config.redirectUri }), [config, origin, callbackPath]);
|
|
39
149
|
(0, react_1.useEffect)(() => {
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
.methods()
|
|
150
|
+
let current = true;
|
|
151
|
+
readMethods(config.serverUrl, config.clientId)
|
|
43
152
|
.then((m) => {
|
|
44
153
|
// A list with no provider in it is IAM saying none; one that failed to
|
|
45
154
|
// arrive keeps the ones Hanzo runs.
|
|
46
|
-
if (
|
|
47
|
-
|
|
155
|
+
if (!current)
|
|
156
|
+
return;
|
|
157
|
+
setMethods(m.methods);
|
|
158
|
+
setExtras(m.extras);
|
|
48
159
|
})
|
|
49
160
|
.catch(() => { });
|
|
50
161
|
return () => {
|
|
51
|
-
|
|
162
|
+
current = false;
|
|
52
163
|
};
|
|
53
|
-
}, [
|
|
164
|
+
}, [config.serverUrl, config.clientId]);
|
|
54
165
|
(0, react_1.useEffect)(() => {
|
|
55
166
|
track?.('signup_viewed', { mode });
|
|
56
167
|
onShown?.();
|
|
@@ -58,6 +169,39 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
58
169
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
170
|
}, []);
|
|
60
171
|
const signup = mode === 'signup';
|
|
172
|
+
// Google One Tap, for a visitor with no session, once IAM has named the client and the SDK can take the credential.
|
|
173
|
+
const client = extras.google;
|
|
174
|
+
(0, react_1.useEffect)(() => {
|
|
175
|
+
if (!oneTap || !client || !taps(iam) || (0, session_1.live)())
|
|
176
|
+
return;
|
|
177
|
+
const nonce = nonceFor(client.nonce);
|
|
178
|
+
const stop = whenIdle(() => {
|
|
179
|
+
loadGsi()
|
|
180
|
+
.then((g) => {
|
|
181
|
+
g.accounts.id.initialize({
|
|
182
|
+
client_id: client.clientId,
|
|
183
|
+
nonce,
|
|
184
|
+
auto_select: false,
|
|
185
|
+
cancel_on_tap_outside: true,
|
|
186
|
+
callback: (r) => {
|
|
187
|
+
if (!r.credential)
|
|
188
|
+
return;
|
|
189
|
+
onMethod?.('google_one_tap', mode);
|
|
190
|
+
track?.('signin_clicked', { method: 'google_one_tap' });
|
|
191
|
+
void run(() => iam.loginWithGoogleCredential(r.credential, nonce));
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
g.accounts.id.prompt();
|
|
195
|
+
})
|
|
196
|
+
.catch(() => { });
|
|
197
|
+
});
|
|
198
|
+
return () => {
|
|
199
|
+
stop();
|
|
200
|
+
window.google?.accounts?.id?.cancel();
|
|
201
|
+
};
|
|
202
|
+
// One prompt per card and client.
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
}, [oneTap, client?.clientId, iam]);
|
|
61
205
|
function commit(method) {
|
|
62
206
|
onMethod?.(method, mode);
|
|
63
207
|
if (signup)
|
|
@@ -107,11 +251,13 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
107
251
|
function next(e) {
|
|
108
252
|
e.preventDefault();
|
|
109
253
|
const address = email.trim();
|
|
110
|
-
|
|
111
|
-
|
|
254
|
+
// An address gets an emailed code; digits get a texted one, where IAM offers a phone sign-in.
|
|
255
|
+
if (!address.includes('@') && !(extras.phone && PHONE.test(address))) {
|
|
256
|
+
setWrong(extras.phone ? 'Enter your email address or phone number.' : 'Enter your email address.');
|
|
112
257
|
return;
|
|
113
258
|
}
|
|
114
|
-
|
|
259
|
+
// A sign-up whose SDK cannot make an account from a code keeps the password form.
|
|
260
|
+
if (signup && !creates(iam)) {
|
|
115
261
|
void run(async () => {
|
|
116
262
|
await iam.sendLoginCode(address);
|
|
117
263
|
setStep('create');
|
|
@@ -119,10 +265,10 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
119
265
|
return;
|
|
120
266
|
}
|
|
121
267
|
setWrong(null);
|
|
122
|
-
if (methods.
|
|
123
|
-
setStep('password');
|
|
124
|
-
else
|
|
268
|
+
if (methods.code)
|
|
125
269
|
void sendCode();
|
|
270
|
+
else
|
|
271
|
+
setStep('password');
|
|
126
272
|
}
|
|
127
273
|
function finish(e) {
|
|
128
274
|
e.preventDefault();
|
|
@@ -133,7 +279,17 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
133
279
|
}
|
|
134
280
|
else if (step === 'code') {
|
|
135
281
|
commit('code');
|
|
136
|
-
void run(() =>
|
|
282
|
+
void run(async () => {
|
|
283
|
+
try {
|
|
284
|
+
return await iam.loginWithCode(address, code.trim());
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
// The code is good and the address has no account: ask for the terms before making one.
|
|
288
|
+
if (!signupRequired(err) || !policy || !creates(iam))
|
|
289
|
+
throw err;
|
|
290
|
+
setStep('terms');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
137
293
|
}
|
|
138
294
|
else if (step === 'create') {
|
|
139
295
|
commit('email');
|
|
@@ -147,12 +303,19 @@ function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback
|
|
|
147
303
|
setCode('');
|
|
148
304
|
};
|
|
149
305
|
const query = typeof window === 'undefined' ? '' : window.location.search;
|
|
306
|
+
if (step === 'terms' && policy && creates(iam)) {
|
|
307
|
+
return ((0, jsx_runtime_1.jsx)(layout_1.YStack, { position: "fixed", t: 0, l: 0, r: 0, b: 0, z: 1000, bg: "$background", overflow: "scroll", children: (0, jsx_runtime_1.jsx)(account_1.CreateAccount, { site: site, email: email.trim(), busy: busy, wrong: wrong, termsPath: termsPath, aupPath: aupPath, onOther: back, onCreate: () => {
|
|
308
|
+
commit('email');
|
|
309
|
+
track?.('terms_accepted', { method: 'email-code' });
|
|
310
|
+
void run(() => iam.createAccountWithCode(email.trim(), code.trim(), policy));
|
|
311
|
+
} }) }));
|
|
312
|
+
}
|
|
150
313
|
const title = signup ? `Create your ${site} account` : `Log in to ${site}`;
|
|
151
314
|
const one = busy ? 'One moment…' : null;
|
|
152
|
-
return ((0, jsx_runtime_1.jsxs)(layout_1.YStack, { render: "section", "aria-label": signup ? 'Sign up' : 'Log in', items: "center", gap: "$5", minH: 560, pt: 112, pb: 96, px:
|
|
315
|
+
return ((0, jsx_runtime_1.jsxs)(layout_1.YStack, { render: "section", "aria-label": signup ? 'Sign up' : 'Log in', items: "center", gap: "$5", ...(frame ? { minH: 560, pt: 112, pb: 96, px: '$4' } : {}), children: [(0, jsx_runtime_1.jsx)(card_1.Card, { width: "100%", maxW: 400, children: (0, jsx_runtime_1.jsxs)(card_1.CardContent, { gap: "$5", children: [frame ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(layout_1.XStack, { items: "center", gap: "$2.5", children: [(0, jsx_runtime_1.jsx)(shell_1.HanzoMark, { size: 20 }), (0, jsx_runtime_1.jsx)(layout_1.SizableText, { size: "$5", fontWeight: "500", color: "$ink", children: site })] }), (0, jsx_runtime_1.jsx)(layout_1.Heading, { render: "h1", size: "$8", fontWeight: "500", color: "$ink", m: 0, children: title })] })) : null, step === 'email' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [methods.providers.length ? ((0, jsx_runtime_1.jsx)(layout_1.YStack, { gap: "$2.5", children: (0, exports.inOrder)(methods.providers).map((p) => ((0, jsx_runtime_1.jsxs)(button_1.Button, { type: "button", variant: "secondary", size: "lg", rounded: "$10", width: "100%", disabled: busy, onClick: () => social(p.name, p.type), children: [(0, jsx_runtime_1.jsx)(Brand, { kind: p.type.toLowerCase() }), `Continue with ${p.type}`] }, p.name))) })) : null, methods.providers.length ? (0, jsx_runtime_1.jsx)(field_1.FieldSeparator, { children: "or" }) : null, (0, jsx_runtime_1.jsxs)(layout_1.YStack, { render: (0, jsx_runtime_1.jsx)("form", { onSubmit: next, noValidate: true }), gap: "$4", children: [(0, jsx_runtime_1.jsxs)(field_1.Field, { gap: "$2", children: [(0, jsx_runtime_1.jsx)(field_1.FieldLabel, { htmlFor: "hanzo-email", children: extras.phone ? 'Email or phone' : 'Email' }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-email", type: extras.phone ? 'text' : 'email', name: "email", autoComplete: signup ? 'email' : 'username', value: email, onChangeText: setEmail, placeholder: extras.phone ? 'you@company.com or +1 555 010 0100' : 'you@company.com', autoFocus: true })] }), (0, jsx_runtime_1.jsx)(Wrong, { text: wrong }), (0, jsx_runtime_1.jsx)(Go, { busy: busy, children: one ?? 'Continue' })] })] })) : ((0, jsx_runtime_1.jsxs)(layout_1.YStack, { render: (0, jsx_runtime_1.jsx)("form", { onSubmit: finish, noValidate: true }), gap: "$4", children: [(0, jsx_runtime_1.jsxs)(layout_1.XStack, { items: "center", gap: "$2", minW: 0, children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", variant: "ghost", size: "icon", "aria-label": extras.phone ? 'Use another email or phone' : 'Use another email', onClick: back, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ArrowLeft, { size: 16 }) }), (0, jsx_runtime_1.jsx)(layout_1.SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, minW: 0, children: email.trim() })] }), step === 'password' ? ((0, jsx_runtime_1.jsxs)(field_1.Field, { gap: "$2", children: [(0, jsx_runtime_1.jsx)(field_1.FieldLabel, { htmlFor: "hanzo-password", children: "Password" }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-password", type: "password", name: "password", autoComplete: "current-password", value: password, onChangeText: setPassword, autoFocus: true })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(layout_1.Paragraph, { size: "$2", color: "$quiet", m: 0, children: ["We sent a code to ", email.trim(), ". It is good for a few minutes."] }), (0, jsx_runtime_1.jsxs)(field_1.Field, { gap: "$2", children: [(0, jsx_runtime_1.jsx)(field_1.FieldLabel, { htmlFor: "hanzo-code", children: "Code" }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-code", name: "code", inputMode: "numeric", autoComplete: "one-time-code", value: code, onChangeText: setCode, autoFocus: true })] })] })), step === 'create' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(field_1.Field, { gap: "$2", children: [(0, jsx_runtime_1.jsx)(field_1.FieldLabel, { htmlFor: "hanzo-name", children: "Your name" }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-name", name: "name", autoComplete: "name", value: name, onChangeText: setName })] }), (0, jsx_runtime_1.jsxs)(field_1.Field, { gap: "$2", children: [(0, jsx_runtime_1.jsx)(field_1.FieldLabel, { htmlFor: "hanzo-new-password", children: "Password" }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-new-password", type: "password", name: "password", autoComplete: "new-password", value: password, onChangeText: setPassword })] })] })) : null, (0, jsx_runtime_1.jsx)(Wrong, { text: wrong }), (0, jsx_runtime_1.jsx)(Go, { busy: busy, children: one ?? (step === 'create' ? 'Create account' : 'Continue') }), step === 'password' && methods.code ? ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => void sendCode(), children: "Email me a code instead" })) : null, step !== 'password' ? ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => void sendCode(), children: "Send a new code" })) : null, step === 'code' && methods.password && !signup ? ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => setStep('password'), children: "Use a password instead" })) : null] })), signup ? ((0, jsx_runtime_1.jsxs)(layout_1.Paragraph, { size: "$1", color: "$quiet", m: 0, children: ["By creating an account you agree to the ", (0, jsx_runtime_1.jsx)(layout_1.Anchor, { href: termsPath, color: "$ink", textDecorationLine: "underline", children: "Terms" }), " and the ", (0, jsx_runtime_1.jsx)(layout_1.Anchor, { href: privacyPath, color: "$ink", textDecorationLine: "underline", children: "Privacy Policy" }), "."] })) : null] }) }), frame ? ((0, jsx_runtime_1.jsxs)(layout_1.Paragraph, { size: "$2", color: "$quiet", m: 0, children: [signup ? 'Have an account? ' : `New to ${site}? `, (0, jsx_runtime_1.jsx)(layout_1.Anchor, { href: `${signup ? loginPath : signupPath}${query}`, color: "$ink", textDecorationLine: "underline", children: signup ? 'Log in' : 'Create an account' })] })) : null] }));
|
|
153
316
|
}
|
|
154
317
|
function Brand({ kind }) {
|
|
155
|
-
return kind === 'google' ? (0, jsx_runtime_1.jsx)(react_simple_icons_1.SiGoogle, { size: 16 }) : kind === 'github' ? (0, jsx_runtime_1.jsx)(react_simple_icons_1.SiGithub, { size: 16 }) : null;
|
|
318
|
+
return kind === 'google' ? (0, jsx_runtime_1.jsx)(react_simple_icons_1.SiGoogle, { size: 16 }) : kind === 'apple' ? (0, jsx_runtime_1.jsx)(react_simple_icons_1.SiApple, { size: 16 }) : kind === 'github' ? (0, jsx_runtime_1.jsx)(react_simple_icons_1.SiGithub, { size: 16 }) : null;
|
|
156
319
|
}
|
|
157
320
|
/** The primary action of a step: filled with the ink, full width. */
|
|
158
321
|
function Go({ busy, children }) {
|
package/dist/auth/SignIn.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Policy } from '../onboarding/state.js';
|
|
1
2
|
/**
|
|
2
3
|
* Signing in and signing up, drawn on the host site's own page.
|
|
3
4
|
*
|
|
@@ -29,6 +30,28 @@ export interface Provider {
|
|
|
29
30
|
}
|
|
30
31
|
export interface SignInProps {
|
|
31
32
|
mode?: Mode;
|
|
33
|
+
/**
|
|
34
|
+
* Draw the card alone, for a page that has its own brand, headline and other-mode
|
|
35
|
+
* line (a hero): no outer padding or minimum height, no brand row, no heading and
|
|
36
|
+
* no "New to Hanzo?" line. Default true.
|
|
37
|
+
*/
|
|
38
|
+
frame?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The versions of the Terms and the Acceptable Use Policy. An address proved by a
|
|
41
|
+
* code that holds no account is asked to accept them on "Let's create your
|
|
42
|
+
* account" before IAM makes the account. Without it, that address is refused as
|
|
43
|
+
* it always was.
|
|
44
|
+
*/
|
|
45
|
+
policy?: Policy;
|
|
46
|
+
aupPath?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Offer Google One Tap to a visitor with no session who is signed in to Google:
|
|
49
|
+
* the Google Identity Services script is loaded after first paint, the client id
|
|
50
|
+
* comes from IAM's auth/methods, and the credential goes to IAM
|
|
51
|
+
* (`loginWithGoogleCredential`). Drawn only when IAM advertises a Google client id
|
|
52
|
+
* and the SDK has that call; otherwise nothing is loaded. Default false.
|
|
53
|
+
*/
|
|
54
|
+
oneTap?: boolean;
|
|
32
55
|
/** The product's name on the card: `Hanzo`. */
|
|
33
56
|
site?: string;
|
|
34
57
|
/** Where IAM sends the browser with the code. Default `/auth/callback`. */
|
|
@@ -51,5 +74,6 @@ export interface SignInProps {
|
|
|
51
74
|
/** The card is drawn (once). */
|
|
52
75
|
onShown?: () => void;
|
|
53
76
|
}
|
|
54
|
-
export declare
|
|
77
|
+
export declare const inOrder: (providers: Provider[]) => Provider[];
|
|
78
|
+
export declare function SignIn({ mode, frame, policy, aupPath, oneTap, site, callbackPath, providers, via, loginPath, signupPath, termsPath, privacyPath, track, authorize, onMethod, onShown, }: SignInProps): import("react").JSX.Element;
|
|
55
79
|
//# sourceMappingURL=SignIn.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignIn.d.ts","sourceRoot":"","sources":["../../src/auth/SignIn.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SignIn.d.ts","sourceRoot":"","sources":["../../src/auth/SignIn.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAA;AAIrC,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAA;IACX;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sFAAsF;IACtF,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;IACtB,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wFAAwF;IACxF,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/D,wFAAwF;IACxF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC/C,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AA8BD,eAAO,MAAM,OAAO,cAAe,QAAQ,EAAE,KAAG,QAAQ,EAAsD,CAAA;AAuE9G,wBAAgB,MAAM,CAAC,EACrB,IAAc,EACd,KAAY,EACZ,MAAM,EACN,OAAgB,EAChB,MAAc,EACd,IAAc,EACd,YAA+B,EAC/B,SAAS,EACT,GAAG,EACH,SAAoB,EACpB,UAAsB,EACtB,SAAoB,EACpB,WAAwB,EACxB,KAAK,EACL,SAAoB,EACpB,QAAQ,EACR,OAAO,GACR,EAAE,WAAW,+BAwTb"}
|
package/dist/auth/SignIn.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import * as Iam from '@hanzo/iam';
|
|
4
5
|
import { IAM, SecondFactor } from '@hanzo/iam';
|
|
5
6
|
import { useIam } from '@hanzo/iam/react';
|
|
6
7
|
import { HanzoMark } from '@hanzogui/shell';
|
|
7
8
|
import { ArrowLeft } from '@hanzogui/lucide-icons-2';
|
|
8
|
-
import { SiGithub, SiGoogle } from '@icons-pack/react-simple-icons';
|
|
9
|
+
import { SiApple, SiGithub, SiGoogle } from '@icons-pack/react-simple-icons';
|
|
9
10
|
import { Button } from '../backends/gui/button.js';
|
|
10
11
|
import { Card, CardContent } from '../backends/gui/card.js';
|
|
11
12
|
import { Field, FieldError, FieldLabel, FieldSeparator } from '../backends/gui/field.js';
|
|
12
13
|
import { Input } from '../backends/gui/input.js';
|
|
13
14
|
import { Anchor, Heading, Paragraph, SizableText, XStack, YStack } from '../backends/gui/layout.js';
|
|
15
|
+
import { CreateAccount } from '../onboarding/account.js';
|
|
16
|
+
import { live } from './session.js';
|
|
14
17
|
const FIRST = {
|
|
15
18
|
password: true,
|
|
16
19
|
code: true,
|
|
@@ -20,10 +23,82 @@ const FIRST = {
|
|
|
20
23
|
{ name: 'provider-github', type: 'GitHub' },
|
|
21
24
|
],
|
|
22
25
|
};
|
|
26
|
+
/** IAM's answer to a proven code for an address with no account (the SDK's typed `SignupRequired`). */
|
|
27
|
+
const signupRequired = (err) => {
|
|
28
|
+
const kind = Iam.SignupRequired;
|
|
29
|
+
return (typeof kind === 'function' && err instanceof kind) || err?.name === 'SignupRequired';
|
|
30
|
+
};
|
|
31
|
+
const creates = (client) => typeof client.createAccountWithCode === 'function';
|
|
23
32
|
const said = (err) => (err instanceof Error && err.message ? err.message : 'Something went wrong. Try again.');
|
|
24
|
-
|
|
33
|
+
/** The providers in the order they are offered: Google, Apple, GitHub, then any others as IAM lists them. */
|
|
34
|
+
const RANK = ['google', 'apple', 'github'];
|
|
35
|
+
const rank = (p) => {
|
|
36
|
+
const i = RANK.indexOf(p.type.toLowerCase());
|
|
37
|
+
return i < 0 ? RANK.length : i;
|
|
38
|
+
};
|
|
39
|
+
export const inOrder = (providers) => [...providers].sort((a, b) => rank(a) - rank(b));
|
|
40
|
+
async function readMethods(serverUrl, clientId) {
|
|
41
|
+
const url = new URL(`${serverUrl.replace(/\/+$/, '')}/v1/iam/auth/methods`);
|
|
42
|
+
url.searchParams.set('clientId', clientId);
|
|
43
|
+
const res = await fetch(url.toString(), { headers: { Accept: 'application/json' } });
|
|
44
|
+
const body = (await res.json());
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error('could not read the sign-in methods');
|
|
47
|
+
const data = (body.status ? body.data : body) ?? {};
|
|
48
|
+
const oauth = (Array.isArray(data.oauth) ? data.oauth : []);
|
|
49
|
+
const named = oauth.filter((p) => typeof p.name === 'string' && typeof p.type === 'string');
|
|
50
|
+
const google = named.find((p) => p.name === 'provider-google');
|
|
51
|
+
const id = google?.clientId ?? google?.client_id;
|
|
52
|
+
return {
|
|
53
|
+
methods: { password: data.password === true, code: data.code === true, signup: data.signup === true, providers: named.map((p) => ({ name: p.name, type: p.type })) },
|
|
54
|
+
extras: { phone: data.phone === true || data.sms === true, google: typeof id === 'string' && id ? { clientId: id, nonce: typeof google?.nonce === 'string' ? google.nonce : undefined } : undefined },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const taps = (client) => typeof client.loginWithGoogleCredential === 'function';
|
|
58
|
+
const GSI = 'https://accounts.google.com/gsi/client';
|
|
59
|
+
/** Load Google Identity Services once, after the page has painted and is idle. */
|
|
60
|
+
function loadGsi() {
|
|
61
|
+
const w = window;
|
|
62
|
+
if (w.google?.accounts?.id)
|
|
63
|
+
return Promise.resolve(w.google);
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const tag = document.createElement('script');
|
|
66
|
+
tag.src = GSI;
|
|
67
|
+
tag.async = true;
|
|
68
|
+
tag.onload = () => (w.google?.accounts?.id ? resolve(w.google) : reject(new Error('Google Identity Services did not load')));
|
|
69
|
+
tag.onerror = () => reject(new Error('Google Identity Services did not load'));
|
|
70
|
+
document.head.appendChild(tag);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const whenIdle = (run) => {
|
|
74
|
+
let done = false;
|
|
75
|
+
const go = () => {
|
|
76
|
+
if (!done)
|
|
77
|
+
run();
|
|
78
|
+
};
|
|
79
|
+
const arm = () => {
|
|
80
|
+
const ric = window.requestIdleCallback;
|
|
81
|
+
if (ric)
|
|
82
|
+
ric(go);
|
|
83
|
+
else
|
|
84
|
+
window.setTimeout(go, 1200);
|
|
85
|
+
};
|
|
86
|
+
if (document.readyState === 'complete')
|
|
87
|
+
arm();
|
|
88
|
+
else
|
|
89
|
+
window.addEventListener('load', arm, { once: true });
|
|
90
|
+
return () => {
|
|
91
|
+
done = true;
|
|
92
|
+
window.removeEventListener('load', arm);
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/** A nonce for one One Tap prompt: IAM's when it sends one, else sixteen random bytes. */
|
|
96
|
+
const nonceFor = (given) => given ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
97
|
+
const PHONE = /^\+?[\d\s().-]{7,}$/;
|
|
98
|
+
export function SignIn({ mode = 'login', frame = true, policy, aupPath = '/aup', oneTap = false, site = 'Hanzo', callbackPath = '/auth/callback', providers, via, loginPath = '/login', signupPath = '/signup', termsPath = '/terms', privacyPath = '/privacy', track, authorize = (u) => u, onMethod, onShown, }) {
|
|
25
99
|
const { config, sdk } = useIam();
|
|
26
100
|
const [methods, setMethods] = useState(providers ? { ...FIRST, providers } : FIRST);
|
|
101
|
+
const [extras, setExtras] = useState({ phone: false });
|
|
27
102
|
const [step, setStep] = useState('email');
|
|
28
103
|
const [email, setEmail] = useState('');
|
|
29
104
|
const [password, setPassword] = useState('');
|
|
@@ -34,20 +109,21 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
34
109
|
const origin = via ?? (typeof window === 'undefined' ? '' : window.location.origin);
|
|
35
110
|
const iam = useMemo(() => new IAM({ ...config, proxyBaseUrl: origin || undefined, redirectUri: origin ? `${origin}${callbackPath}` : config.redirectUri }), [config, origin, callbackPath]);
|
|
36
111
|
useEffect(() => {
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
.methods()
|
|
112
|
+
let current = true;
|
|
113
|
+
readMethods(config.serverUrl, config.clientId)
|
|
40
114
|
.then((m) => {
|
|
41
115
|
// A list with no provider in it is IAM saying none; one that failed to
|
|
42
116
|
// arrive keeps the ones Hanzo runs.
|
|
43
|
-
if (
|
|
44
|
-
|
|
117
|
+
if (!current)
|
|
118
|
+
return;
|
|
119
|
+
setMethods(m.methods);
|
|
120
|
+
setExtras(m.extras);
|
|
45
121
|
})
|
|
46
122
|
.catch(() => { });
|
|
47
123
|
return () => {
|
|
48
|
-
|
|
124
|
+
current = false;
|
|
49
125
|
};
|
|
50
|
-
}, [
|
|
126
|
+
}, [config.serverUrl, config.clientId]);
|
|
51
127
|
useEffect(() => {
|
|
52
128
|
track?.('signup_viewed', { mode });
|
|
53
129
|
onShown?.();
|
|
@@ -55,6 +131,39 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
55
131
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
132
|
}, []);
|
|
57
133
|
const signup = mode === 'signup';
|
|
134
|
+
// Google One Tap, for a visitor with no session, once IAM has named the client and the SDK can take the credential.
|
|
135
|
+
const client = extras.google;
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!oneTap || !client || !taps(iam) || live())
|
|
138
|
+
return;
|
|
139
|
+
const nonce = nonceFor(client.nonce);
|
|
140
|
+
const stop = whenIdle(() => {
|
|
141
|
+
loadGsi()
|
|
142
|
+
.then((g) => {
|
|
143
|
+
g.accounts.id.initialize({
|
|
144
|
+
client_id: client.clientId,
|
|
145
|
+
nonce,
|
|
146
|
+
auto_select: false,
|
|
147
|
+
cancel_on_tap_outside: true,
|
|
148
|
+
callback: (r) => {
|
|
149
|
+
if (!r.credential)
|
|
150
|
+
return;
|
|
151
|
+
onMethod?.('google_one_tap', mode);
|
|
152
|
+
track?.('signin_clicked', { method: 'google_one_tap' });
|
|
153
|
+
void run(() => iam.loginWithGoogleCredential(r.credential, nonce));
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
g.accounts.id.prompt();
|
|
157
|
+
})
|
|
158
|
+
.catch(() => { });
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
stop();
|
|
162
|
+
window.google?.accounts?.id?.cancel();
|
|
163
|
+
};
|
|
164
|
+
// One prompt per card and client.
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
}, [oneTap, client?.clientId, iam]);
|
|
58
167
|
function commit(method) {
|
|
59
168
|
onMethod?.(method, mode);
|
|
60
169
|
if (signup)
|
|
@@ -104,11 +213,13 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
104
213
|
function next(e) {
|
|
105
214
|
e.preventDefault();
|
|
106
215
|
const address = email.trim();
|
|
107
|
-
|
|
108
|
-
|
|
216
|
+
// An address gets an emailed code; digits get a texted one, where IAM offers a phone sign-in.
|
|
217
|
+
if (!address.includes('@') && !(extras.phone && PHONE.test(address))) {
|
|
218
|
+
setWrong(extras.phone ? 'Enter your email address or phone number.' : 'Enter your email address.');
|
|
109
219
|
return;
|
|
110
220
|
}
|
|
111
|
-
|
|
221
|
+
// A sign-up whose SDK cannot make an account from a code keeps the password form.
|
|
222
|
+
if (signup && !creates(iam)) {
|
|
112
223
|
void run(async () => {
|
|
113
224
|
await iam.sendLoginCode(address);
|
|
114
225
|
setStep('create');
|
|
@@ -116,10 +227,10 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
116
227
|
return;
|
|
117
228
|
}
|
|
118
229
|
setWrong(null);
|
|
119
|
-
if (methods.
|
|
120
|
-
setStep('password');
|
|
121
|
-
else
|
|
230
|
+
if (methods.code)
|
|
122
231
|
void sendCode();
|
|
232
|
+
else
|
|
233
|
+
setStep('password');
|
|
123
234
|
}
|
|
124
235
|
function finish(e) {
|
|
125
236
|
e.preventDefault();
|
|
@@ -130,7 +241,17 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
130
241
|
}
|
|
131
242
|
else if (step === 'code') {
|
|
132
243
|
commit('code');
|
|
133
|
-
void run(() =>
|
|
244
|
+
void run(async () => {
|
|
245
|
+
try {
|
|
246
|
+
return await iam.loginWithCode(address, code.trim());
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
// The code is good and the address has no account: ask for the terms before making one.
|
|
250
|
+
if (!signupRequired(err) || !policy || !creates(iam))
|
|
251
|
+
throw err;
|
|
252
|
+
setStep('terms');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
134
255
|
}
|
|
135
256
|
else if (step === 'create') {
|
|
136
257
|
commit('email');
|
|
@@ -144,12 +265,19 @@ export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/c
|
|
|
144
265
|
setCode('');
|
|
145
266
|
};
|
|
146
267
|
const query = typeof window === 'undefined' ? '' : window.location.search;
|
|
268
|
+
if (step === 'terms' && policy && creates(iam)) {
|
|
269
|
+
return (_jsx(YStack, { position: "fixed", t: 0, l: 0, r: 0, b: 0, z: 1000, bg: "$background", overflow: "scroll", children: _jsx(CreateAccount, { site: site, email: email.trim(), busy: busy, wrong: wrong, termsPath: termsPath, aupPath: aupPath, onOther: back, onCreate: () => {
|
|
270
|
+
commit('email');
|
|
271
|
+
track?.('terms_accepted', { method: 'email-code' });
|
|
272
|
+
void run(() => iam.createAccountWithCode(email.trim(), code.trim(), policy));
|
|
273
|
+
} }) }));
|
|
274
|
+
}
|
|
147
275
|
const title = signup ? `Create your ${site} account` : `Log in to ${site}`;
|
|
148
276
|
const one = busy ? 'One moment…' : null;
|
|
149
|
-
return (_jsxs(YStack, { render: "section", "aria-label": signup ? 'Sign up' : 'Log in', items: "center", gap: "$5", minH: 560, pt: 112, pb: 96, px:
|
|
277
|
+
return (_jsxs(YStack, { render: "section", "aria-label": signup ? 'Sign up' : 'Log in', items: "center", gap: "$5", ...(frame ? { minH: 560, pt: 112, pb: 96, px: '$4' } : {}), children: [_jsx(Card, { width: "100%", maxW: 400, children: _jsxs(CardContent, { gap: "$5", children: [frame ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(HanzoMark, { size: 20 }), _jsx(SizableText, { size: "$5", fontWeight: "500", color: "$ink", children: site })] }), _jsx(Heading, { render: "h1", size: "$8", fontWeight: "500", color: "$ink", m: 0, children: title })] })) : null, step === 'email' ? (_jsxs(_Fragment, { children: [methods.providers.length ? (_jsx(YStack, { gap: "$2.5", children: inOrder(methods.providers).map((p) => (_jsxs(Button, { type: "button", variant: "secondary", size: "lg", rounded: "$10", width: "100%", disabled: busy, onClick: () => social(p.name, p.type), children: [_jsx(Brand, { kind: p.type.toLowerCase() }), `Continue with ${p.type}`] }, p.name))) })) : null, methods.providers.length ? _jsx(FieldSeparator, { children: "or" }) : null, _jsxs(YStack, { render: _jsx("form", { onSubmit: next, noValidate: true }), gap: "$4", children: [_jsxs(Field, { gap: "$2", children: [_jsx(FieldLabel, { htmlFor: "hanzo-email", children: extras.phone ? 'Email or phone' : 'Email' }), _jsx(Input, { id: "hanzo-email", type: extras.phone ? 'text' : 'email', name: "email", autoComplete: signup ? 'email' : 'username', value: email, onChangeText: setEmail, placeholder: extras.phone ? 'you@company.com or +1 555 010 0100' : 'you@company.com', autoFocus: true })] }), _jsx(Wrong, { text: wrong }), _jsx(Go, { busy: busy, children: one ?? 'Continue' })] })] })) : (_jsxs(YStack, { render: _jsx("form", { onSubmit: finish, noValidate: true }), gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": extras.phone ? 'Use another email or phone' : 'Use another email', onClick: back, children: _jsx(ArrowLeft, { size: 16 }) }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, minW: 0, children: email.trim() })] }), step === 'password' ? (_jsxs(Field, { gap: "$2", children: [_jsx(FieldLabel, { htmlFor: "hanzo-password", children: "Password" }), _jsx(Input, { id: "hanzo-password", type: "password", name: "password", autoComplete: "current-password", value: password, onChangeText: setPassword, autoFocus: true })] })) : (_jsxs(_Fragment, { children: [_jsxs(Paragraph, { size: "$2", color: "$quiet", m: 0, children: ["We sent a code to ", email.trim(), ". It is good for a few minutes."] }), _jsxs(Field, { gap: "$2", children: [_jsx(FieldLabel, { htmlFor: "hanzo-code", children: "Code" }), _jsx(Input, { id: "hanzo-code", name: "code", inputMode: "numeric", autoComplete: "one-time-code", value: code, onChangeText: setCode, autoFocus: true })] })] })), step === 'create' ? (_jsxs(_Fragment, { children: [_jsxs(Field, { gap: "$2", children: [_jsx(FieldLabel, { htmlFor: "hanzo-name", children: "Your name" }), _jsx(Input, { id: "hanzo-name", name: "name", autoComplete: "name", value: name, onChangeText: setName })] }), _jsxs(Field, { gap: "$2", children: [_jsx(FieldLabel, { htmlFor: "hanzo-new-password", children: "Password" }), _jsx(Input, { id: "hanzo-new-password", type: "password", name: "password", autoComplete: "new-password", value: password, onChangeText: setPassword })] })] })) : null, _jsx(Wrong, { text: wrong }), _jsx(Go, { busy: busy, children: one ?? (step === 'create' ? 'Create account' : 'Continue') }), step === 'password' && methods.code ? (_jsx(Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => void sendCode(), children: "Email me a code instead" })) : null, step !== 'password' ? (_jsx(Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => void sendCode(), children: "Send a new code" })) : null, step === 'code' && methods.password && !signup ? (_jsx(Button, { type: "button", variant: "linkMuted", size: "lg", disabled: busy, onClick: () => setStep('password'), children: "Use a password instead" })) : null] })), signup ? (_jsxs(Paragraph, { size: "$1", color: "$quiet", m: 0, children: ["By creating an account you agree to the ", _jsx(Anchor, { href: termsPath, color: "$ink", textDecorationLine: "underline", children: "Terms" }), " and the ", _jsx(Anchor, { href: privacyPath, color: "$ink", textDecorationLine: "underline", children: "Privacy Policy" }), "."] })) : null] }) }), frame ? (_jsxs(Paragraph, { size: "$2", color: "$quiet", m: 0, children: [signup ? 'Have an account? ' : `New to ${site}? `, _jsx(Anchor, { href: `${signup ? loginPath : signupPath}${query}`, color: "$ink", textDecorationLine: "underline", children: signup ? 'Log in' : 'Create an account' })] })) : null] }));
|
|
150
278
|
}
|
|
151
279
|
function Brand({ kind }) {
|
|
152
|
-
return kind === 'google' ? _jsx(SiGoogle, { size: 16 }) : kind === 'github' ? _jsx(SiGithub, { size: 16 }) : null;
|
|
280
|
+
return kind === 'google' ? _jsx(SiGoogle, { size: 16 }) : kind === 'apple' ? _jsx(SiApple, { size: 16 }) : kind === 'github' ? _jsx(SiGithub, { size: 16 }) : null;
|
|
153
281
|
}
|
|
154
282
|
/** The primary action of a step: filled with the ink, full width. */
|
|
155
283
|
function Go({ busy, children }) {
|