@puzzle-ui/fullstack 0.0.0-stage → 0.5.1
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/README.md +65 -2
- package/dist/LICENSE +21 -0
- package/dist/auth/client/auth.css +206 -0
- package/dist/auth/client/experience.cjs +340 -0
- package/dist/auth/client/experience.cjs.map +1 -0
- package/dist/auth/client/experience.d.cts +67 -0
- package/dist/auth/client/experience.d.ts +67 -0
- package/dist/auth/client/experience.js +340 -0
- package/dist/auth/client/experience.js.map +1 -0
- package/dist/auth/client/index.cjs +234 -0
- package/dist/auth/client/index.cjs.map +1 -0
- package/dist/auth/client/index.d.cts +48 -0
- package/dist/auth/client/index.d.ts +48 -0
- package/dist/auth/client/index.js +216 -0
- package/dist/auth/client/index.js.map +1 -0
- package/dist/auth/client/messages.cjs +80 -0
- package/dist/auth/client/messages.cjs.map +1 -0
- package/dist/auth/client/messages.d.cts +92 -0
- package/dist/auth/client/messages.d.ts +92 -0
- package/dist/auth/client/messages.js +79 -0
- package/dist/auth/client/messages.js.map +1 -0
- package/dist/auth/client/passkeys.cjs +43 -0
- package/dist/auth/client/passkeys.cjs.map +1 -0
- package/dist/auth/client/passkeys.d.cts +23 -0
- package/dist/auth/client/passkeys.d.ts +23 -0
- package/dist/auth/client/passkeys.js +40 -0
- package/dist/auth/client/passkeys.js.map +1 -0
- package/dist/auth/client/react.cjs +859 -0
- package/dist/auth/client/react.cjs.map +1 -0
- package/dist/auth/client/react.d.cts +72 -0
- package/dist/auth/client/react.d.ts +72 -0
- package/dist/auth/client/react.js +850 -0
- package/dist/auth/client/react.js.map +1 -0
- package/dist/auth/next/index.cjs +40 -0
- package/dist/auth/next/index.cjs.map +1 -0
- package/dist/auth/next/index.d.cts +12 -0
- package/dist/auth/next/index.d.ts +12 -0
- package/dist/auth/next/index.js +38 -0
- package/dist/auth/next/index.js.map +1 -0
- package/dist/auth/server/health.cjs +51 -0
- package/dist/auth/server/health.cjs.map +1 -0
- package/dist/auth/server/health.d.cts +15 -0
- package/dist/auth/server/health.d.ts +15 -0
- package/dist/auth/server/health.js +51 -0
- package/dist/auth/server/health.js.map +1 -0
- package/dist/auth/server/index.cjs +108 -0
- package/dist/auth/server/index.cjs.map +1 -0
- package/dist/auth/server/index.d.cts +22 -0
- package/dist/auth/server/index.d.ts +22 -0
- package/dist/auth/server/index.js +88 -0
- package/dist/auth/server/index.js.map +1 -0
- package/dist/auth/server/lifecycle.cjs +126 -0
- package/dist/auth/server/lifecycle.cjs.map +1 -0
- package/dist/auth/server/lifecycle.d.cts +20 -0
- package/dist/auth/server/lifecycle.d.ts +20 -0
- package/dist/auth/server/lifecycle.js +126 -0
- package/dist/auth/server/lifecycle.js.map +1 -0
- package/dist/auth/server/mfa.cjs +682 -0
- package/dist/auth/server/mfa.cjs.map +1 -0
- package/dist/auth/server/mfa.d.cts +90 -0
- package/dist/auth/server/mfa.d.ts +90 -0
- package/dist/auth/server/mfa.js +673 -0
- package/dist/auth/server/mfa.js.map +1 -0
- package/dist/auth/server/session.cjs +58 -0
- package/dist/auth/server/session.cjs.map +1 -0
- package/dist/auth/server/session.d.cts +14 -0
- package/dist/auth/server/session.d.ts +14 -0
- package/dist/auth/server/session.js +53 -0
- package/dist/auth/server/session.js.map +1 -0
- package/dist/auth/shared/index.cjs +0 -0
- package/dist/auth/shared/index.d.cts +64 -0
- package/dist/auth/shared/index.d.ts +64 -0
- package/dist/auth/shared/index.js +1 -0
- package/dist/auth/tanstack/index.cjs +34 -0
- package/dist/auth/tanstack/index.cjs.map +1 -0
- package/dist/auth/tanstack/index.d.cts +7 -0
- package/dist/auth/tanstack/index.d.ts +7 -0
- package/dist/auth/tanstack/index.js +32 -0
- package/dist/auth/tanstack/index.js.map +1 -0
- package/package.json +113 -4
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @puzzle-ui/fullstack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Documentation: https://puzzle-ui.com/docs/fullstack](https://puzzle-ui.com/docs/fullstack)
|
|
4
|
+
|
|
5
|
+
`@puzzle-ui/fullstack` connects Puzzle Auth to browser clients, React UI, server routes, and framework adapters. Use it when an application needs a ready-to-use browser auth client or Request/Response handlers; configure auth policy, storage, and sessions with the lower-level packages.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @puzzle-ui/fullstack react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
React and React DOM 19 are required peer dependencies. `next` (15 or 16) and `@tanstack/react-start` are optional peers for their respective adapters. On the server, configure the managers from `@puzzle-ui/auth` and `@puzzle-ui/session` with your storage backend. See the [Auth docs](https://puzzle-ui.com/docs/auth), [Storage docs](https://puzzle-ui.com/docs/storage), and [developer authentication guide](https://puzzle-ui.com/docs/developer/authentication).
|
|
14
|
+
|
|
15
|
+
## Client
|
|
16
|
+
|
|
17
|
+
Wrap the application in `AuthProvider`, then use the hooks or exported forms below it:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { AuthProvider, useAuth, useUser } from "@puzzle-ui/fullstack/auth/client";
|
|
21
|
+
|
|
22
|
+
export const Providers = ({ children }: { children: React.ReactNode }) => (
|
|
23
|
+
<AuthProvider options={{ basePath: "/api/auth" }}>{children}</AuthProvider>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const Account = () => {
|
|
27
|
+
const user = useUser();
|
|
28
|
+
const { signOut } = useAuth();
|
|
29
|
+
|
|
30
|
+
return user ? <button onClick={() => void signOut()}>{user.email} · Sign out</button> : null;
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The client provides auth state and actions, sign-in/sign-up and MFA components, `AuthExperience` for branded registration flows, locale helpers, and passkey browser helpers. Browser state is not an authorization boundary; protect server resources independently.
|
|
35
|
+
|
|
36
|
+
## Server and framework adapters
|
|
37
|
+
|
|
38
|
+
Create one server instance from configured auth and session managers, then register its routes:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createAuthServer, createAuthRouteHandlers } from "@puzzle-ui/fullstack/auth/server";
|
|
42
|
+
|
|
43
|
+
export const authServer = createAuthServer({
|
|
44
|
+
auth,
|
|
45
|
+
mfa,
|
|
46
|
+
stepUp,
|
|
47
|
+
sessions,
|
|
48
|
+
cookie,
|
|
49
|
+
csrf,
|
|
50
|
+
resolveClientIp,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const authRoutes = createAuthRouteHandlers(authServer);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `@puzzle-ui/fullstack/auth/next` for Next.js `GET`/`POST` route handlers or `@puzzle-ui/fullstack/auth/tanstack` to create a TanStack Start Request dispatcher. Configure cookies, CSRF, storage and trusted proxy IP resolution in the server layer. Do not expose server managers to browser bundles.
|
|
57
|
+
|
|
58
|
+
## Entry points
|
|
59
|
+
|
|
60
|
+
- `@puzzle-ui/fullstack/auth/client` — browser client, React provider/hooks/components, auth experience, localization, and passkey helpers.
|
|
61
|
+
- `@puzzle-ui/fullstack/auth/server` — server handlers, route map, session and step-up helpers, lifecycle handlers, readiness checks.
|
|
62
|
+
- `@puzzle-ui/fullstack/auth/shared` — serializable public auth contracts (types only).
|
|
63
|
+
- `@puzzle-ui/fullstack/auth/next` — Next.js route-handler adapter.
|
|
64
|
+
- `@puzzle-ui/fullstack/auth/tanstack` — TanStack Start adapter.
|
|
65
|
+
|
|
66
|
+
There is no root package export; import a documented subpath. See the [full documentation](https://puzzle-ui.com/docs/fullstack) for API details and production guidance.
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sjoerd van Den Berg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
.puzzle-auth {
|
|
2
|
+
flex-direction: column;
|
|
3
|
+
gap: 1.25rem;
|
|
4
|
+
width: 100%;
|
|
5
|
+
max-width: 28rem;
|
|
6
|
+
margin-inline: auto;
|
|
7
|
+
display: flex;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.puzzle-auth__header {
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: .5rem;
|
|
13
|
+
display: flex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.puzzle-auth__header h1 {
|
|
17
|
+
margin: 0;
|
|
18
|
+
font-size: 1.5rem;
|
|
19
|
+
font-weight: 650;
|
|
20
|
+
line-height: 1.25;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.puzzle-auth__header p {
|
|
24
|
+
color: var(--puzzle-text-secondary, #737373);
|
|
25
|
+
margin: 0;
|
|
26
|
+
line-height: 1.5;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.puzzle-auth__form {
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
display: flex;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.puzzle-auth__footer, .puzzle-auth__actions {
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: .75rem;
|
|
40
|
+
display: flex;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.puzzle-auth__text-button {
|
|
44
|
+
color: var(--puzzle-primary, #2563eb);
|
|
45
|
+
font: inherit;
|
|
46
|
+
text-underline-offset: .2em;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
background: none;
|
|
49
|
+
border: 0;
|
|
50
|
+
padding: 0;
|
|
51
|
+
text-decoration: underline;
|
|
52
|
+
display: inline;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.puzzle-auth__text-button:focus-visible {
|
|
56
|
+
outline: 2px solid var(--puzzle-focus, #2563eb);
|
|
57
|
+
outline-offset: 3px;
|
|
58
|
+
border-radius: 2px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.puzzle-auth__factor-list {
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: .75rem;
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
list-style: none;
|
|
67
|
+
display: flex;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.puzzle-auth__factor-list li {
|
|
71
|
+
border-bottom: 1px solid var(--puzzle-border, #e5e7eb);
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
padding-block: .75rem;
|
|
76
|
+
display: flex;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.puzzle-auth__factor-list li > div {
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: .25rem;
|
|
82
|
+
display: flex;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.puzzle-auth__factor-list li span {
|
|
86
|
+
color: var(--puzzle-text-secondary, #737373);
|
|
87
|
+
font-size: .875rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.puzzle-auth__recovery-codes {
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
gap: .75rem;
|
|
93
|
+
display: flex;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.puzzle-auth__recovery-codes ul {
|
|
97
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
98
|
+
gap: .5rem;
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
list-style: none;
|
|
102
|
+
display: grid;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.puzzle-auth__recovery-codes li {
|
|
106
|
+
border: 1px solid var(--puzzle-border, #e5e7eb);
|
|
107
|
+
border-radius: var(--puzzle-radius-sm, .375rem);
|
|
108
|
+
background: var(--puzzle-surface, #fff);
|
|
109
|
+
text-align: center;
|
|
110
|
+
padding: .625rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.puzzle-auth__recovery-codes code {
|
|
114
|
+
font-family: var(--puzzle-font-mono, monospace);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.puzzle-auth--settings {
|
|
118
|
+
max-width: 40rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.puzzle-auth--settings h2 {
|
|
122
|
+
margin: 0;
|
|
123
|
+
font-size: 1.25rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.puzzle-auth-experience {
|
|
127
|
+
align-items: var(--puzzle-auth-experience-align, center);
|
|
128
|
+
width: 100%;
|
|
129
|
+
max-width: var(--puzzle-auth-experience-width, 32rem);
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
margin-inline: auto;
|
|
132
|
+
display: flex;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.puzzle-auth-experience__branding {
|
|
136
|
+
align-items: inherit;
|
|
137
|
+
gap: var(--puzzle-spacing-2, .5rem);
|
|
138
|
+
width: 100%;
|
|
139
|
+
text-align: center;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
margin-block-end: var(--puzzle-spacing-6, 1.5rem);
|
|
142
|
+
display: flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.puzzle-auth-experience__branding h1, .puzzle-auth-experience__branding p {
|
|
146
|
+
margin: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.puzzle-auth-experience__content {
|
|
150
|
+
width: 100%;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.puzzle-auth__custom-field {
|
|
154
|
+
gap: var(--puzzle-spacing-2, .5rem);
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
width: 100%;
|
|
157
|
+
display: flex;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.puzzle-auth__custom-field input {
|
|
161
|
+
box-sizing: border-box;
|
|
162
|
+
width: 100%;
|
|
163
|
+
min-width: 0;
|
|
164
|
+
padding: var(--puzzle-spacing-3, .75rem);
|
|
165
|
+
color: inherit;
|
|
166
|
+
background: var(--puzzle-color-surface, transparent);
|
|
167
|
+
border: 1px solid var(--puzzle-color-border, currentColor);
|
|
168
|
+
border-radius: var(--puzzle-radius-md, .375rem);
|
|
169
|
+
font: inherit;
|
|
170
|
+
background: var(--puzzle-surface, #fff);
|
|
171
|
+
border: 1px solid var(--puzzle-border, #e5e7eb);
|
|
172
|
+
border-radius: var(--puzzle-radius-sm, .375rem);
|
|
173
|
+
font: inherit;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.puzzle-auth__custom-field input:focus-visible {
|
|
177
|
+
outline: 2px solid var(--puzzle-focus, #2563eb);
|
|
178
|
+
outline-offset: 2px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (prefers-reduced-motion: reduce) {
|
|
182
|
+
.puzzle-auth *, .puzzle-auth :before, .puzzle-auth :after, .puzzle-auth-experience *, .puzzle-auth-experience :before, .puzzle-auth-experience :after {
|
|
183
|
+
scroll-behavior: auto !important;
|
|
184
|
+
transition-duration: .01ms !important;
|
|
185
|
+
animation-duration: .01ms !important;
|
|
186
|
+
animation-iteration-count: 1 !important;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@media (forced-colors: active) {
|
|
191
|
+
.puzzle-auth__text-button {
|
|
192
|
+
color: linktext;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.puzzle-auth__factor-list li, .puzzle-auth__recovery-codes li {
|
|
196
|
+
border-color: canvastext;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.puzzle-auth__custom-field input {
|
|
200
|
+
border: 1px solid canvastext;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.puzzle-auth__custom-field input:focus-visible {
|
|
204
|
+
outline: 2px solid highlight;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const require_react = require("./react.cjs");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let _puzzle_ui_ui = require("@puzzle-ui/ui");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
// src/auth/client/experience.tsx
|
|
7
|
+
const EMPTY_FIELDS = [];
|
|
8
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
const isValidEmail = (value) => {
|
|
10
|
+
const email = value.trim();
|
|
11
|
+
return email.length <= 254 && EMAIL_PATTERN.test(email);
|
|
12
|
+
};
|
|
13
|
+
const getEmailError = (value, messages) => {
|
|
14
|
+
const email = value.trim();
|
|
15
|
+
if (!email) return messages.emailRequired;
|
|
16
|
+
if (!isValidEmail(email)) return "Enter a valid email address.";
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const isSafeRedirect = (value) => Boolean(value && value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && !/[\u0000-\u001f\u007f]/.test(value));
|
|
20
|
+
const redirectAfterSuccess = (value) => {
|
|
21
|
+
if (!isSafeRedirect(value) || typeof window === "undefined") return;
|
|
22
|
+
window.location.assign(value);
|
|
23
|
+
};
|
|
24
|
+
const getInitialValues = (fields) => Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? (field.type === "checkbox" ? false : "")]));
|
|
25
|
+
const validateFieldDefinition = (fields) => {
|
|
26
|
+
const names = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(field.name)) throw new Error(`Invalid custom auth field name: "${field.name}".`);
|
|
29
|
+
if ([
|
|
30
|
+
"name",
|
|
31
|
+
"email",
|
|
32
|
+
"password",
|
|
33
|
+
"confirmPassword"
|
|
34
|
+
].includes(field.name)) throw new Error(`Custom auth field name "${field.name}" is reserved.`);
|
|
35
|
+
if (names.has(field.name)) throw new Error(`Duplicate custom auth field name: "${field.name}".`);
|
|
36
|
+
if (field.type === "select" && (!field.options || field.options.length === 0)) throw new Error(`Select field "${field.name}" requires at least one option.`);
|
|
37
|
+
names.add(field.name);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const validateFields = async (fields, values) => {
|
|
41
|
+
const errors = {};
|
|
42
|
+
for (const field of fields) {
|
|
43
|
+
const value = values[field.name];
|
|
44
|
+
if (field.required && (value === void 0 || value === "" || typeof value === "boolean" && !value)) {
|
|
45
|
+
errors[field.name] = `${field.label} is required.`;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!field.validate) continue;
|
|
49
|
+
const message = await field.validate(value ?? "", values);
|
|
50
|
+
if (message) errors[field.name] = message;
|
|
51
|
+
}
|
|
52
|
+
return errors;
|
|
53
|
+
};
|
|
54
|
+
const AuthCustomFields = ({ fields, values, errors, disabled, onChange }) => {
|
|
55
|
+
const generatedId = (0, react.useId)();
|
|
56
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fields.map((field) => {
|
|
57
|
+
const id = `${generatedId}-${field.name}`;
|
|
58
|
+
const value = values[field.name] ?? "";
|
|
59
|
+
const error = errors[field.name];
|
|
60
|
+
const commonProps = {
|
|
61
|
+
name: field.name,
|
|
62
|
+
disabled: disabled || field.disabled,
|
|
63
|
+
required: field.required
|
|
64
|
+
};
|
|
65
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.FormField, {
|
|
66
|
+
controlId: id,
|
|
67
|
+
label: field.label,
|
|
68
|
+
description: field.description,
|
|
69
|
+
error,
|
|
70
|
+
required: field.required,
|
|
71
|
+
disabled: commonProps.disabled,
|
|
72
|
+
className: field.type === "url" || field.type === "number" ? "puzzle-auth__custom-field" : void 0,
|
|
73
|
+
children: field.type === "textarea" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextAreaInput, {
|
|
74
|
+
...commonProps,
|
|
75
|
+
value: String(value),
|
|
76
|
+
placeholder: field.placeholder,
|
|
77
|
+
resizable: true,
|
|
78
|
+
onChange: (event) => onChange(field.name, event.currentTarget.value)
|
|
79
|
+
}) : field.type === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Select, {
|
|
80
|
+
...commonProps,
|
|
81
|
+
fullWidth: true,
|
|
82
|
+
value: String(value),
|
|
83
|
+
placeholder: field.placeholder ?? "Select an option",
|
|
84
|
+
options: field.options ?? [],
|
|
85
|
+
onChange: (value) => onChange(field.name, value ?? "")
|
|
86
|
+
}) : field.type === "checkbox" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Checkbox, {
|
|
87
|
+
...commonProps,
|
|
88
|
+
checked: Boolean(value),
|
|
89
|
+
onChange: (event) => onChange(field.name, event.currentTarget.checked)
|
|
90
|
+
}) : field.type === "url" || field.type === "number" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
91
|
+
...commonProps,
|
|
92
|
+
type: field.type,
|
|
93
|
+
value: String(value),
|
|
94
|
+
placeholder: field.placeholder,
|
|
95
|
+
autoComplete: field.autoComplete,
|
|
96
|
+
onChange: (event) => onChange(field.name, event.currentTarget.value)
|
|
97
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextInput, {
|
|
98
|
+
...commonProps,
|
|
99
|
+
type: field.type === "tel" ? "phone" : field.type,
|
|
100
|
+
value: String(value),
|
|
101
|
+
placeholder: field.placeholder,
|
|
102
|
+
autoComplete: field.autoComplete,
|
|
103
|
+
onChange: (event) => onChange(field.name, event.currentTarget.value)
|
|
104
|
+
})
|
|
105
|
+
}, field.name);
|
|
106
|
+
}) });
|
|
107
|
+
};
|
|
108
|
+
const AuthExperience = ({ mode = "sign-in", config = {}, className, onSubmit, onSuccess, onSignIn, onSignUp, onForgotPassword }) => {
|
|
109
|
+
const { signUp, messages } = require_react.useAuth();
|
|
110
|
+
const fields = config.fields ?? EMPTY_FIELDS;
|
|
111
|
+
const branding = config.branding;
|
|
112
|
+
const layout = config.layout;
|
|
113
|
+
const redirects = config.redirects;
|
|
114
|
+
validateFieldDefinition(fields);
|
|
115
|
+
const [values, setValues] = (0, react.useState)(() => getInitialValues(fields));
|
|
116
|
+
const [errors, setErrors] = (0, react.useState)({});
|
|
117
|
+
const [formError, setFormError] = (0, react.useState)(null);
|
|
118
|
+
const [emailError, setEmailError] = (0, react.useState)(null);
|
|
119
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
120
|
+
const [name, setName] = (0, react.useState)("");
|
|
121
|
+
const [email, setEmail] = (0, react.useState)("");
|
|
122
|
+
const [password, setPassword] = (0, react.useState)("");
|
|
123
|
+
const [confirmation, setConfirmation] = (0, react.useState)("");
|
|
124
|
+
(0, react.useEffect)(() => {
|
|
125
|
+
setValues((current) => {
|
|
126
|
+
const next = {};
|
|
127
|
+
for (const field of fields) {
|
|
128
|
+
const currentValue = Object.prototype.hasOwnProperty.call(current, field.name) ? current[field.name] : void 0;
|
|
129
|
+
next[field.name] = currentValue ?? field.defaultValue ?? (field.type === "checkbox" ? false : "");
|
|
130
|
+
}
|
|
131
|
+
const currentNames = Object.keys(current);
|
|
132
|
+
const nextNames = Object.keys(next);
|
|
133
|
+
return currentNames.length === nextNames.length && nextNames.every((fieldName) => current[fieldName] === next[fieldName]) ? current : next;
|
|
134
|
+
});
|
|
135
|
+
setErrors((current) => {
|
|
136
|
+
const activeNames = new Set(fields.map((field) => field.name));
|
|
137
|
+
const next = Object.fromEntries(Object.entries(current).filter(([fieldName]) => activeNames.has(fieldName)));
|
|
138
|
+
return Object.keys(next).length === Object.keys(current).length ? current : next;
|
|
139
|
+
});
|
|
140
|
+
}, [fields]);
|
|
141
|
+
const updateField = (fieldName, value) => {
|
|
142
|
+
setValues((current) => ({
|
|
143
|
+
...current,
|
|
144
|
+
[fieldName]: value
|
|
145
|
+
}));
|
|
146
|
+
setErrors((current) => {
|
|
147
|
+
if (!(fieldName in current)) return current;
|
|
148
|
+
const next = { ...current };
|
|
149
|
+
delete next[fieldName];
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const handleEmailChange = (value) => {
|
|
154
|
+
setEmail(value);
|
|
155
|
+
if (emailError) setEmailError(getEmailError(value, messages));
|
|
156
|
+
};
|
|
157
|
+
const handleSubmit = async (event) => {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
setFormError(null);
|
|
160
|
+
setErrors({});
|
|
161
|
+
const nextEmailError = getEmailError(email, messages);
|
|
162
|
+
setEmailError(nextEmailError);
|
|
163
|
+
if (nextEmailError) return;
|
|
164
|
+
if (password !== confirmation) {
|
|
165
|
+
setFormError(messages.passwordMismatch);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!password) {
|
|
169
|
+
setFormError(messages.passwordRequired);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
setLoading(true);
|
|
173
|
+
try {
|
|
174
|
+
const nextErrors = await validateFields(fields, values);
|
|
175
|
+
if (Object.keys(nextErrors).length > 0) {
|
|
176
|
+
setErrors(nextErrors);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const registration = {
|
|
180
|
+
name: name.trim() || null,
|
|
181
|
+
email: email.trim(),
|
|
182
|
+
password
|
|
183
|
+
};
|
|
184
|
+
if ((onSubmit ? await onSubmit({
|
|
185
|
+
registration,
|
|
186
|
+
fields: { ...values },
|
|
187
|
+
signUp
|
|
188
|
+
}) : await signUp(registration)).status === "authenticated") {
|
|
189
|
+
onSuccess?.();
|
|
190
|
+
redirectAfterSuccess(redirects?.afterSignUp);
|
|
191
|
+
}
|
|
192
|
+
} catch (cause) {
|
|
193
|
+
setFormError(cause instanceof Error ? cause.message : messages.unexpectedError);
|
|
194
|
+
} finally {
|
|
195
|
+
setLoading(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const wrapperClassName = [
|
|
199
|
+
"puzzle-auth",
|
|
200
|
+
"puzzle-auth-experience",
|
|
201
|
+
layout?.className,
|
|
202
|
+
className
|
|
203
|
+
].filter(Boolean).join(" ");
|
|
204
|
+
const contentClassName = ["puzzle-auth-experience__content", layout?.contentClassName].filter(Boolean).join(" ");
|
|
205
|
+
const style = {
|
|
206
|
+
"--puzzle-auth-experience-width": layout?.width === "sm" ? "24rem" : layout?.width === "lg" ? "48rem" : layout?.width === "full" ? "100%" : "32rem",
|
|
207
|
+
"--puzzle-auth-experience-align": layout?.align === "left" ? "flex-start" : "center"
|
|
208
|
+
};
|
|
209
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
210
|
+
className: wrapperClassName,
|
|
211
|
+
style,
|
|
212
|
+
children: [
|
|
213
|
+
layout?.header,
|
|
214
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
215
|
+
className: "puzzle-auth-experience__branding",
|
|
216
|
+
children: [
|
|
217
|
+
branding?.logo,
|
|
218
|
+
branding?.title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Heading, {
|
|
219
|
+
as: "h1",
|
|
220
|
+
level: "headline2",
|
|
221
|
+
children: branding.title
|
|
222
|
+
}) : null,
|
|
223
|
+
branding?.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Text, {
|
|
224
|
+
as: "p",
|
|
225
|
+
children: branding.description
|
|
226
|
+
}) : null
|
|
227
|
+
]
|
|
228
|
+
}),
|
|
229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
230
|
+
className: contentClassName,
|
|
231
|
+
children: mode === "sign-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_react.SignInForm, {
|
|
232
|
+
redirectTo: redirects?.afterSignIn,
|
|
233
|
+
onSuccess,
|
|
234
|
+
onForgotPassword,
|
|
235
|
+
onSignUp
|
|
236
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
237
|
+
formError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
238
|
+
role: "alert",
|
|
239
|
+
"aria-live": "assertive",
|
|
240
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Alert, {
|
|
241
|
+
severity: "error",
|
|
242
|
+
children: formError
|
|
243
|
+
})
|
|
244
|
+
}) : null,
|
|
245
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
246
|
+
className: "puzzle-auth__form",
|
|
247
|
+
onSubmit: (event) => void handleSubmit(event),
|
|
248
|
+
noValidate: true,
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.FormField, {
|
|
251
|
+
label: messages.name,
|
|
252
|
+
controlId: "puzzle-auth-experience-name",
|
|
253
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextInput, {
|
|
254
|
+
name: "name",
|
|
255
|
+
type: "text",
|
|
256
|
+
autoComplete: "name",
|
|
257
|
+
value: name,
|
|
258
|
+
disabled: loading,
|
|
259
|
+
onChange: (event) => setName(event.currentTarget.value)
|
|
260
|
+
})
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.FormField, {
|
|
263
|
+
label: messages.email,
|
|
264
|
+
controlId: "puzzle-auth-experience-email",
|
|
265
|
+
required: true,
|
|
266
|
+
error: emailError,
|
|
267
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextInput, {
|
|
268
|
+
name: "email",
|
|
269
|
+
type: "email",
|
|
270
|
+
autoComplete: "email",
|
|
271
|
+
required: true,
|
|
272
|
+
value: email,
|
|
273
|
+
disabled: loading,
|
|
274
|
+
onChange: (event) => handleEmailChange(event.currentTarget.value)
|
|
275
|
+
})
|
|
276
|
+
}),
|
|
277
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.FormField, {
|
|
278
|
+
label: messages.password,
|
|
279
|
+
controlId: "puzzle-auth-experience-password",
|
|
280
|
+
required: true,
|
|
281
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextInput, {
|
|
282
|
+
name: "new-password",
|
|
283
|
+
type: "password",
|
|
284
|
+
suffix: null,
|
|
285
|
+
autoComplete: "new-password",
|
|
286
|
+
required: true,
|
|
287
|
+
value: password,
|
|
288
|
+
disabled: loading,
|
|
289
|
+
onChange: (event) => setPassword(event.currentTarget.value)
|
|
290
|
+
})
|
|
291
|
+
}),
|
|
292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.FormField, {
|
|
293
|
+
label: messages.confirmPassword,
|
|
294
|
+
controlId: "puzzle-auth-experience-confirm",
|
|
295
|
+
required: true,
|
|
296
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.TextInput, {
|
|
297
|
+
name: "confirm-password",
|
|
298
|
+
type: "password",
|
|
299
|
+
suffix: null,
|
|
300
|
+
autoComplete: "new-password",
|
|
301
|
+
required: true,
|
|
302
|
+
value: confirmation,
|
|
303
|
+
disabled: loading,
|
|
304
|
+
onChange: (event) => setConfirmation(event.currentTarget.value)
|
|
305
|
+
})
|
|
306
|
+
}),
|
|
307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuthCustomFields, {
|
|
308
|
+
fields,
|
|
309
|
+
values,
|
|
310
|
+
errors,
|
|
311
|
+
disabled: loading,
|
|
312
|
+
onChange: updateField
|
|
313
|
+
}),
|
|
314
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Button, {
|
|
315
|
+
type: "submit",
|
|
316
|
+
fullWidth: true,
|
|
317
|
+
loading,
|
|
318
|
+
disabled: loading || !email.trim() || !password || !confirmation,
|
|
319
|
+
children: loading ? messages.signingUp : messages.createAccount
|
|
320
|
+
})
|
|
321
|
+
]
|
|
322
|
+
}),
|
|
323
|
+
onSignIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
324
|
+
className: "puzzle-auth__footer",
|
|
325
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Text, { children: messages.existingAccount }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_ui.Button, {
|
|
326
|
+
type: "button",
|
|
327
|
+
variant: "link",
|
|
328
|
+
onClick: onSignIn,
|
|
329
|
+
children: messages.signIn
|
|
330
|
+
})]
|
|
331
|
+
}) : null
|
|
332
|
+
] })
|
|
333
|
+
}),
|
|
334
|
+
layout?.footer
|
|
335
|
+
]
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
exports.AuthExperience = AuthExperience;
|
|
339
|
+
|
|
340
|
+
//# sourceMappingURL=experience.cjs.map
|