@draftlab/auth 0.0.4 → 0.1.0

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/ui/code.d.ts CHANGED
@@ -1,155 +1,42 @@
1
- import { CodeProviderError, CodeProviderOptions } from "../provider/code.js";
1
+ import { CodeProviderOptions } from "../provider/code.js";
2
2
 
3
3
  //#region src/ui/code.d.ts
4
4
 
5
5
  /**
6
- * Default text copy for the PIN code authentication UI.
7
- * All text can be customized via the copy prop.
6
+ * Type for customizable UI copy text
8
7
  */
9
- declare const DEFAULT_COPY: {
10
- /** Placeholder text for the email/contact input field */
11
- email_placeholder: string;
12
- /** Error message displayed when the entered email/contact is invalid */
13
- email_invalid: string;
14
- /** Text for the primary action button */
15
- button_continue: string;
16
- /** Informational text explaining that a PIN code will be sent */
17
- code_info: string;
18
- /** Placeholder text for the PIN code input field */
19
- code_placeholder: string;
20
- /** Error message displayed when the entered PIN code is incorrect */
21
- code_invalid: string;
22
- /** Success message prefix when code is initially sent */
23
- code_sent: string;
24
- /** Success message prefix when code is resent */
25
- code_resent: string;
26
- /** Text asking if user didn't receive the code */
27
- code_didnt_get: string;
28
- /** Text for the resend code button */
29
- code_resend: string;
30
- };
31
- /**
32
- * Type for customizable UI copy text.
33
- * All properties are optional to allow partial customization.
34
- */
35
- type CodeUICopy = typeof DEFAULT_COPY;
8
+ interface CodeUICopy {
9
+ readonly email_placeholder: string;
10
+ readonly email_invalid: string;
11
+ readonly button_continue: string;
12
+ readonly code_info: string;
13
+ readonly code_placeholder: string;
14
+ readonly code_invalid: string;
15
+ readonly code_sent: string;
16
+ readonly code_resent: string;
17
+ readonly code_didnt_get: string;
18
+ readonly code_resend: string;
19
+ }
36
20
  /**
37
- * Input mode for the contact field.
38
- * Determines the input type and validation behavior.
21
+ * Input mode for the contact field
39
22
  */
40
23
  type CodeUIMode = "email" | "phone";
41
24
  /**
42
- * Configuration options for the CodeUI component.
25
+ * Configuration options for the CodeUI component
43
26
  */
44
- interface CodeUIOptions {
27
+ interface CodeUIOptions extends Pick<CodeProviderOptions, "sendCode"> {
45
28
  /**
46
- * Callback function for sending PIN codes to users.
47
- * Should handle delivery via email, SMS, or other channels based on the claims.
48
- *
49
- * @param claims - User contact information (email, phone, etc.)
50
- * @param code - The generated PIN code to send
51
- * @returns Promise resolving to undefined on success, or error object on failure
52
- *
53
- * @example
54
- * ```ts
55
- * sendCode: async (claims, code) => {
56
- * if (claims.email) {
57
- * await emailService.send({
58
- * to: claims.email,
59
- * subject: 'Your verification code',
60
- * text: `Your PIN code is: ${code}`
61
- * })
62
- * } else if (claims.phone) {
63
- * await smsService.send(claims.phone, `PIN: ${code}`)
64
- * } else {
65
- * return {
66
- * type: "invalid_claim",
67
- * key: "contact",
68
- * value: "Email or phone required"
69
- * }
70
- * }
71
- * }
72
- * ```
29
+ * Input mode determining the type of contact information to collect
30
+ * @default "email"
73
31
  */
74
- sendCode: (claims: Record<string, string>, code: string) => Promise<CodeProviderError | undefined>;
32
+ readonly mode?: CodeUIMode;
75
33
  /**
76
- * Custom text copy for UI labels and messages.
77
- * Allows full customization of all displayed text.
78
- *
79
- * @example
80
- * ```ts
81
- * copy: {
82
- * email_placeholder: "Enter your email address",
83
- * code_info: "Check your email for a 6-digit verification code",
84
- * button_continue: "Verify",
85
- * code_invalid: "The code you entered is incorrect"
86
- * }
87
- * ```
34
+ * Custom text copy for UI labels, messages, and errors
88
35
  */
89
36
  readonly copy?: Partial<CodeUICopy>;
90
- /**
91
- * Input mode determining the type of contact information to collect.
92
- *
93
- * @default "email"
94
- *
95
- * @example
96
- * ```ts
97
- * mode: "phone" // Collect phone numbers instead of emails
98
- * ```
99
- */
100
- readonly mode?: CodeUIMode;
101
37
  }
102
38
  /**
103
- * Creates a complete UI configuration for PIN code authentication.
104
- * Provides pre-built forms for collecting user contact info and verifying PIN codes.
105
- *
106
- * @param options - Configuration options for the UI
107
- * @returns Complete CodeProvider configuration with UI handlers
108
- *
109
- * @example
110
- * ```ts
111
- * // Basic email-based PIN authentication
112
- * const emailCodeUI = CodeUI({
113
- * sendCode: async (claims, code) => {
114
- * await emailService.send(claims.email, `Code: ${code}`)
115
- * }
116
- * })
117
- *
118
- * // Phone-based PIN authentication with custom copy
119
- * const phoneCodeUI = CodeUI({
120
- * mode: "phone",
121
- * copy: {
122
- * email_placeholder: "Phone number",
123
- * code_info: "We'll send a verification code via SMS",
124
- * email_invalid: "Please enter a valid phone number"
125
- * },
126
- * sendCode: async (claims, code) => {
127
- * await smsService.send(claims.phone, `Verification: ${code}`)
128
- * }
129
- * })
130
- *
131
- * // Multi-mode authentication
132
- * const flexibleCodeUI = CodeUI({
133
- * copy: {
134
- * email_placeholder: "Email or phone number",
135
- * code_info: "We'll send a code to your email or phone"
136
- * },
137
- * sendCode: async (claims, code) => {
138
- * if (claims.email && claims.email.includes('@')) {
139
- * await emailService.send(claims.email, `Code: ${code}`)
140
- * } else if (claims.email) {
141
- * // Treat as phone number if no @ symbol
142
- * await smsService.send(claims.email, `Code: ${code}`)
143
- * } else {
144
- * return {
145
- * type: "invalid_claim",
146
- * key: "contact",
147
- * value: "Email or phone required"
148
- * }
149
- * }
150
- * }
151
- * })
152
- * ```
39
+ * Creates a complete UI configuration for PIN code authentication
153
40
  */
154
41
  declare const CodeUI: (options: CodeUIOptions) => CodeProviderOptions;
155
42
  //#endregion
package/dist/ui/code.js CHANGED
@@ -1,11 +1,9 @@
1
- import { UnknownStateError } from "../error.js";
2
- import { Layout } from "./base.js";
3
- import { FormAlert } from "./form.js";
1
+ import { Layout, renderToHTML } from "./base.js";
2
+ import { jsx, jsxs } from "preact/jsx-runtime";
4
3
 
5
- //#region src/ui/code.ts
4
+ //#region src/ui/code.tsx
6
5
  /**
7
- * Default text copy for the PIN code authentication UI.
8
- * All text can be customized via the copy prop.
6
+ * Default text copy for the PIN code authentication UI
9
7
  */
10
8
  const DEFAULT_COPY = {
11
9
  email_placeholder: "Email",
@@ -20,174 +18,215 @@ const DEFAULT_COPY = {
20
18
  code_resend: "Resend"
21
19
  };
22
20
  /**
23
- * Creates a complete UI configuration for PIN code authentication.
24
- * Provides pre-built forms for collecting user contact info and verifying PIN codes.
25
- *
26
- * @param options - Configuration options for the UI
27
- * @returns Complete CodeProvider configuration with UI handlers
28
- *
29
- * @example
30
- * ```ts
31
- * // Basic email-based PIN authentication
32
- * const emailCodeUI = CodeUI({
33
- * sendCode: async (claims, code) => {
34
- * await emailService.send(claims.email, `Code: ${code}`)
35
- * }
36
- * })
37
- *
38
- * // Phone-based PIN authentication with custom copy
39
- * const phoneCodeUI = CodeUI({
40
- * mode: "phone",
41
- * copy: {
42
- * email_placeholder: "Phone number",
43
- * code_info: "We'll send a verification code via SMS",
44
- * email_invalid: "Please enter a valid phone number"
45
- * },
46
- * sendCode: async (claims, code) => {
47
- * await smsService.send(claims.phone, `Verification: ${code}`)
48
- * }
49
- * })
50
- *
51
- * // Multi-mode authentication
52
- * const flexibleCodeUI = CodeUI({
53
- * copy: {
54
- * email_placeholder: "Email or phone number",
55
- * code_info: "We'll send a code to your email or phone"
56
- * },
57
- * sendCode: async (claims, code) => {
58
- * if (claims.email && claims.email.includes('@')) {
59
- * await emailService.send(claims.email, `Code: ${code}`)
60
- * } else if (claims.email) {
61
- * // Treat as phone number if no @ symbol
62
- * await smsService.send(claims.email, `Code: ${code}`)
63
- * } else {
64
- * return {
65
- * type: "invalid_claim",
66
- * key: "contact",
67
- * value: "Email or phone required"
68
- * }
69
- * }
70
- * }
71
- * })
72
- * ```
21
+ * FormAlert component for displaying messages
22
+ */
23
+ const FormAlert = ({ message, color = "danger" }) => {
24
+ if (!message) return null;
25
+ return /* @__PURE__ */ jsxs("div", {
26
+ "data-component": "form-alert",
27
+ "data-color": color,
28
+ children: [/* @__PURE__ */ jsx("i", {
29
+ "data-slot": color === "success" ? "icon-success" : "icon-danger",
30
+ children: color === "success" ? /* @__PURE__ */ jsx("svg", {
31
+ fill: "none",
32
+ stroke: "currentColor",
33
+ viewBox: "0 0 24 24",
34
+ xmlns: "http://www.w3.org/2000/svg",
35
+ "aria-label": "Success",
36
+ role: "img",
37
+ children: /* @__PURE__ */ jsx("path", {
38
+ strokeLinecap: "round",
39
+ strokeLinejoin: "round",
40
+ strokeWidth: 2,
41
+ d: "M5 13l4 4L19 7"
42
+ })
43
+ }) : /* @__PURE__ */ jsx("svg", {
44
+ fill: "none",
45
+ stroke: "currentColor",
46
+ viewBox: "0 0 24 24",
47
+ xmlns: "http://www.w3.org/2000/svg",
48
+ "aria-label": "Error",
49
+ role: "img",
50
+ children: /* @__PURE__ */ jsx("path", {
51
+ strokeLinecap: "round",
52
+ strokeLinejoin: "round",
53
+ strokeWidth: 2,
54
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.232 16.5c-.77.833.192 2.5 1.732 2.5z"
55
+ })
56
+ })
57
+ }), /* @__PURE__ */ jsx("span", {
58
+ "data-slot": "message",
59
+ children: message
60
+ })]
61
+ });
62
+ };
63
+ /**
64
+ * Input component with consistent styling
65
+ */
66
+ const Input = ({ type, name, placeholder, value, required, autoComplete, autoFocus,...props }) => /* @__PURE__ */ jsx("input", {
67
+ type,
68
+ name,
69
+ placeholder,
70
+ value,
71
+ required,
72
+ autoComplete,
73
+ "data-component": "input",
74
+ ...props
75
+ });
76
+ /**
77
+ * Button component with consistent styling
78
+ */
79
+ const Button = ({ type = "submit", children,...props }) => /* @__PURE__ */ jsx("button", {
80
+ type,
81
+ "data-component": "button",
82
+ ...props,
83
+ children
84
+ });
85
+ /**
86
+ * Link component with consistent styling
87
+ */
88
+ /**
89
+ * Gets the appropriate error message for display
90
+ */
91
+ const getErrorMessage = (error, copy) => {
92
+ if (!error?.type) return void 0;
93
+ switch (error.type) {
94
+ case "invalid_code": return copy.code_invalid;
95
+ case "invalid_claim": return copy.email_invalid;
96
+ }
97
+ };
98
+ /**
99
+ * Gets the appropriate success message for display
100
+ */
101
+ const getSuccessMessage = (state, copy) => {
102
+ if (state.type === "start" || !state.claims) return void 0;
103
+ const contact = state.claims.email || state.claims.phone || "";
104
+ const prefix = state.resend ? copy.code_resent : copy.code_sent;
105
+ return {
106
+ message: `${prefix}${contact}`,
107
+ contact
108
+ };
109
+ };
110
+ /**
111
+ * Creates a complete UI configuration for PIN code authentication
73
112
  */
74
113
  const CodeUI = (options) => {
75
114
  const copy = {
76
115
  ...DEFAULT_COPY,
77
116
  ...options.copy
78
117
  };
79
- const inputMode = options.mode ?? "email";
118
+ const mode = options.mode || "email";
80
119
  /**
81
- * Determines the appropriate input field attributes based on the selected mode.
120
+ * Renders the start form for collecting contact information
82
121
  */
83
- const getInputAttributes = () => {
84
- switch (inputMode) {
85
- case "email": return {
86
- type: "email",
87
- name: "email",
88
- inputmode: "email",
89
- autocomplete: "email"
90
- };
91
- case "phone": return {
92
- type: "tel",
93
- name: "phone",
94
- inputmode: "tel",
95
- autocomplete: "tel"
96
- };
97
- }
122
+ const renderStart = (form, error, state) => {
123
+ const success = getSuccessMessage(state || { type: "start" }, copy);
124
+ return /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsxs("form", {
125
+ "data-component": "form",
126
+ method: "post",
127
+ children: [
128
+ success ? /* @__PURE__ */ jsx(FormAlert, {
129
+ message: success.message,
130
+ color: "success"
131
+ }) : /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error, copy) }),
132
+ /* @__PURE__ */ jsx(Input, {
133
+ type: mode === "email" ? "email" : "tel",
134
+ name: mode,
135
+ placeholder: copy.email_placeholder,
136
+ value: form?.get(mode)?.toString() || "",
137
+ autoComplete: mode,
138
+ required: true
139
+ }),
140
+ /* @__PURE__ */ jsx("input", {
141
+ type: "hidden",
142
+ name: "action",
143
+ value: "request"
144
+ }),
145
+ /* @__PURE__ */ jsx(Button, {
146
+ type: "submit",
147
+ children: copy.button_continue
148
+ }),
149
+ /* @__PURE__ */ jsx("p", {
150
+ style: {
151
+ fontSize: "0.875rem",
152
+ color: "var(--color-high)",
153
+ textAlign: "center",
154
+ margin: "1rem 0 0 0"
155
+ },
156
+ children: copy.code_info
157
+ })
158
+ ]
159
+ }) });
98
160
  };
99
161
  /**
100
- * Gets the appropriate contact value from claims for display purposes.
162
+ * Renders the code verification form
101
163
  */
102
- const getContactValue = (claims) => {
103
- return claims.email || claims.phone || Object.values(claims)[0] || "";
164
+ const renderCode = (_form, error, state) => {
165
+ const success = getSuccessMessage(state, copy);
166
+ const contact = state.type === "code" ? state.claims?.[mode] || "" : "";
167
+ return /* @__PURE__ */ jsxs(Layout, { children: [/* @__PURE__ */ jsxs("form", {
168
+ "data-component": "form",
169
+ method: "post",
170
+ children: [
171
+ success ? /* @__PURE__ */ jsx(FormAlert, {
172
+ message: success.message,
173
+ color: "success"
174
+ }) : /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error, copy) }),
175
+ /* @__PURE__ */ jsx("input", {
176
+ name: "action",
177
+ type: "hidden",
178
+ value: "verify"
179
+ }),
180
+ /* @__PURE__ */ jsx(Input, {
181
+ type: "text",
182
+ name: "code",
183
+ placeholder: copy.code_placeholder,
184
+ "aria-label": "6-digit verification code",
185
+ autoComplete: "one-time-code",
186
+ inputMode: "numeric",
187
+ maxLength: 6,
188
+ minLength: 6,
189
+ pattern: "[0-9]{6}",
190
+ autoFocus: true,
191
+ required: true
192
+ }),
193
+ /* @__PURE__ */ jsx(Button, {
194
+ type: "submit",
195
+ children: copy.button_continue
196
+ })
197
+ ]
198
+ }), /* @__PURE__ */ jsxs("form", {
199
+ method: "post",
200
+ children: [
201
+ /* @__PURE__ */ jsx("input", {
202
+ name: "action",
203
+ type: "hidden",
204
+ value: "resend"
205
+ }),
206
+ /* @__PURE__ */ jsx("input", {
207
+ name: mode,
208
+ type: "hidden",
209
+ value: contact
210
+ }),
211
+ /* @__PURE__ */ jsxs("div", {
212
+ "data-component": "form-footer",
213
+ children: [/* @__PURE__ */ jsx("span", {
214
+ style: { fontSize: "0.875rem" },
215
+ children: copy.code_didnt_get
216
+ }), /* @__PURE__ */ jsx(Button, {
217
+ type: "submit",
218
+ "data-component": "link",
219
+ children: copy.code_resend
220
+ })]
221
+ })
222
+ ]
223
+ })] });
104
224
  };
105
225
  return {
106
226
  sendCode: options.sendCode,
107
- length: 6,
108
- request: async (_req, state, _form, error) => {
109
- if (state.type === "start") {
110
- const inputAttrs = getInputAttributes();
111
- const formContent = `
112
- <form data-component="form" method="post">
113
- ${error?.type === "invalid_claim" ? FormAlert({ message: copy.email_invalid }) : ""}
114
-
115
- <input name="action" type="hidden" value="request" />
116
-
117
- <input
118
- autofocus
119
- data-component="input"
120
- placeholder="${copy.email_placeholder}"
121
- required
122
- type="${inputAttrs.type}"
123
- name="${inputAttrs.name}"
124
- inputmode="${inputAttrs.inputmode}"
125
- autocomplete="${inputAttrs.autocomplete}"
126
- />
127
-
128
- <button data-component="button" type="submit">
129
- ${copy.button_continue}
130
- </button>
131
- </form>
132
-
133
- <p data-component="form-footer">${copy.code_info}</p>
134
- `;
135
- const html = Layout({ children: formContent });
136
- return new Response(html, { headers: { "Content-Type": "text/html" } });
137
- }
138
- if (state.type === "code") {
139
- const contactValue = getContactValue(state.claims);
140
- const hiddenInputs = Object.entries(state.claims).map(([key, value]) => `<input name="${key}" type="hidden" value="${value}" />`).join("");
141
- const formContent = `
142
- <form data-component="form" method="post">
143
- ${error?.type === "invalid_code" ? FormAlert({ message: copy.code_invalid }) : ""}
144
-
145
- ${FormAlert({
146
- color: "success",
147
- message: (state.resend ? copy.code_resent : copy.code_sent) + contactValue
148
- })}
149
-
150
- <input name="action" type="hidden" value="verify" />
151
-
152
- <input
153
- aria-label="6-digit verification code"
154
- autocomplete="one-time-code"
155
- autofocus
156
- data-component="input"
157
- inputmode="numeric"
158
- maxlength="6"
159
- minlength="6"
160
- name="code"
161
- pattern="[0-9]{6}"
162
- placeholder="${copy.code_placeholder}"
163
- required
164
- type="text"
165
- />
166
-
167
- <button data-component="button" type="submit">
168
- ${copy.button_continue}
169
- </button>
170
- </form>
171
-
172
- <form method="post">
173
- ${hiddenInputs}
174
-
175
- <input name="action" type="hidden" value="resend" />
176
-
177
- <div data-component="form-footer">
178
- <span>
179
- ${copy.code_didnt_get}
180
- <button data-component="link" type="submit">
181
- ${copy.code_resend}
182
- </button>
183
- </span>
184
- </div>
185
- </form>
186
- `;
187
- const html = Layout({ children: formContent });
188
- return new Response(html, { headers: { "Content-Type": "text/html" } });
189
- }
190
- throw new UnknownStateError();
227
+ request: async (_req, state, form, error) => {
228
+ const html = renderToHTML(state.type === "start" ? renderStart(form, error, state) : renderCode(form, error, state));
229
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
191
230
  }
192
231
  };
193
232
  };
package/dist/ui/form.d.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { ComponentChildren } from "preact";
2
+
1
3
  //#region src/ui/form.d.ts
2
- /**
3
- * Form alert component for displaying success and error messages.
4
- * Provides consistent styling and iconography for user feedback in authentication forms.
5
- */
4
+
6
5
  /**
7
6
  * Alert color variant determining the visual style and icon.
8
7
  */
@@ -24,8 +23,11 @@ interface FormAlertProps {
24
23
  }
25
24
  /**
26
25
  * Form alert component that displays error or success messages.
27
- * Returns an HTML string for the alert or empty string if no message.
26
+ * Returns a Preact component or null if no message.
28
27
  */
29
- declare const FormAlert: (props: FormAlertProps) => string;
28
+ declare const FormAlert: ({
29
+ message,
30
+ color
31
+ }: FormAlertProps) => ComponentChildren;
30
32
  //#endregion
31
33
  export { FormAlert, FormAlertColor, FormAlertProps };