@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.
Files changed (78) hide show
  1. package/dist/auth/SignIn.cjs +180 -17
  2. package/dist/auth/SignIn.d.ts +25 -1
  3. package/dist/auth/SignIn.d.ts.map +1 -1
  4. package/dist/auth/SignIn.js +146 -18
  5. package/dist/auth/SignIn.js.map +1 -1
  6. package/dist/consent/Consent.cjs +35 -14
  7. package/dist/consent/Consent.d.ts +1 -1
  8. package/dist/consent/Consent.d.ts.map +1 -1
  9. package/dist/consent/Consent.js +36 -15
  10. package/dist/consent/Consent.js.map +1 -1
  11. package/dist/gallery.cjs +2 -1
  12. package/dist/gallery.d.ts.map +1 -1
  13. package/dist/gallery.html +8 -4
  14. package/dist/gallery.js +3 -2
  15. package/dist/gallery.js.map +1 -1
  16. package/dist/marketing/ClosingCta.cjs +1 -1
  17. package/dist/marketing/ClosingCta.js +1 -1
  18. package/dist/marketing/ClosingCta.js.map +1 -1
  19. package/dist/marketing/Faq.cjs +1 -1
  20. package/dist/marketing/Faq.js +1 -1
  21. package/dist/marketing/Faq.js.map +1 -1
  22. package/dist/marketing/Footer.cjs +1 -1
  23. package/dist/marketing/Footer.js +1 -1
  24. package/dist/marketing/Footer.js.map +1 -1
  25. package/dist/marketing/Hero.cjs +2 -2
  26. package/dist/marketing/Hero.js +2 -2
  27. package/dist/marketing/Hero.js.map +1 -1
  28. package/dist/marketing/Plans.cjs +1 -1
  29. package/dist/marketing/Plans.js +1 -1
  30. package/dist/marketing/Plans.js.map +1 -1
  31. package/dist/marketing/blocks.cjs +3 -3
  32. package/dist/marketing/blocks.js +3 -3
  33. package/dist/marketing/blocks.js.map +1 -1
  34. package/dist/onboarding/ChatOnboarding.cjs +1 -1
  35. package/dist/onboarding/ChatOnboarding.js +2 -2
  36. package/dist/onboarding/ChatOnboarding.js.map +1 -1
  37. package/dist/onboarding/Onboarding.cjs +89 -21
  38. package/dist/onboarding/Onboarding.d.ts +30 -4
  39. package/dist/onboarding/Onboarding.d.ts.map +1 -1
  40. package/dist/onboarding/Onboarding.js +92 -24
  41. package/dist/onboarding/Onboarding.js.map +1 -1
  42. package/dist/onboarding/account.cjs +16 -0
  43. package/dist/onboarding/account.d.ts +21 -0
  44. package/dist/onboarding/account.d.ts.map +1 -0
  45. package/dist/onboarding/account.js +14 -0
  46. package/dist/onboarding/account.js.map +1 -0
  47. package/dist/onboarding/api.cjs +4 -1
  48. package/dist/onboarding/api.d.ts +5 -1
  49. package/dist/onboarding/api.d.ts.map +1 -1
  50. package/dist/onboarding/api.js +2 -0
  51. package/dist/onboarding/api.js.map +1 -1
  52. package/dist/onboarding/frame.cjs +13 -2
  53. package/dist/onboarding/frame.d.ts +11 -2
  54. package/dist/onboarding/frame.d.ts.map +1 -1
  55. package/dist/onboarding/frame.js +12 -2
  56. package/dist/onboarding/frame.js.map +1 -1
  57. package/dist/onboarding/hooks.cjs +25 -7
  58. package/dist/onboarding/hooks.d.ts +15 -4
  59. package/dist/onboarding/hooks.d.ts.map +1 -1
  60. package/dist/onboarding/hooks.js +26 -9
  61. package/dist/onboarding/hooks.js.map +1 -1
  62. package/dist/onboarding/index.cjs +10 -2
  63. package/dist/onboarding/index.d.ts +6 -4
  64. package/dist/onboarding/index.d.ts.map +1 -1
  65. package/dist/onboarding/index.js +6 -4
  66. package/dist/onboarding/index.js.map +1 -1
  67. package/dist/onboarding/screens.cjs +14 -3
  68. package/dist/onboarding/screens.d.ts +10 -1
  69. package/dist/onboarding/screens.d.ts.map +1 -1
  70. package/dist/onboarding/screens.js +13 -3
  71. package/dist/onboarding/screens.js.map +1 -1
  72. package/dist/onboarding/state.cjs +17 -2
  73. package/dist/onboarding/state.d.ts +22 -3
  74. package/dist/onboarding/state.d.ts.map +1 -1
  75. package/dist/onboarding/state.js +15 -2
  76. package/dist/onboarding/state.js.map +1 -1
  77. package/dist/styles.css +3 -0
  78. package/package.json +3 -3
@@ -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
- function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback', providers, via, loginPath = '/login', signupPath = '/signup', termsPath = '/terms', privacyPath = '/privacy', track, authorize = (u) => u, onMethod, onShown, }) {
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 live = true;
41
- iam
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 (live)
47
- setMethods(m);
155
+ if (!current)
156
+ return;
157
+ setMethods(m.methods);
158
+ setExtras(m.extras);
48
159
  })
49
160
  .catch(() => { });
50
161
  return () => {
51
- live = false;
162
+ current = false;
52
163
  };
53
- }, [iam]);
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
- if (!address.includes('@')) {
111
- setWrong('Enter your email address.');
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
- if (signup) {
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.password)
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(() => iam.loginWithCode(address, code.trim()));
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: "$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: [(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 }), 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: 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: "Email" }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: "hanzo-email", type: "email", name: "email", autoComplete: signup ? 'email' : 'username', value: email, onChangeText: setEmail, placeholder: "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": "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] })), 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] }) }), (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' })] })] }));
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 }) {
@@ -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 function SignIn({ mode, site, callbackPath, providers, via, loginPath, signupPath, termsPath, privacyPath, track, authorize, onMethod, onShown, }: SignInProps): import("react").JSX.Element;
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":"AAcA;;;;;;;;;;;;;;;;;;;;;;;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,+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;AAcD,wBAAgB,MAAM,CAAC,EACrB,IAAc,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,+BA6Ob"}
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"}
@@ -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
- export function SignIn({ mode = 'login', site = 'Hanzo', callbackPath = '/auth/callback', providers, via, loginPath = '/login', signupPath = '/signup', termsPath = '/terms', privacyPath = '/privacy', track, authorize = (u) => u, onMethod, onShown, }) {
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 live = true;
38
- iam
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 (live)
44
- setMethods(m);
117
+ if (!current)
118
+ return;
119
+ setMethods(m.methods);
120
+ setExtras(m.extras);
45
121
  })
46
122
  .catch(() => { });
47
123
  return () => {
48
- live = false;
124
+ current = false;
49
125
  };
50
- }, [iam]);
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
- if (!address.includes('@')) {
108
- setWrong('Enter your email address.');
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
- if (signup) {
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.password)
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(() => iam.loginWithCode(address, code.trim()));
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: "$4", children: [_jsx(Card, { width: "100%", maxW: 400, children: _jsxs(CardContent, { gap: "$5", 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 }), step === 'email' ? (_jsxs(_Fragment, { children: [methods.providers.length ? (_jsx(YStack, { gap: "$2.5", children: 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: "Email" }), _jsx(Input, { id: "hanzo-email", type: "email", name: "email", autoComplete: signup ? 'email' : 'username', value: email, onChangeText: setEmail, placeholder: "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": "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] })), 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] }) }), _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' })] })] }));
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 }) {