@foir/sdk 0.2.0 → 0.4.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/README.md +107 -1
- package/dist/auth.d.ts +78 -1
- package/dist/auth.js +136 -0
- package/dist/next.d.ts +178 -0
- package/dist/next.js +275 -0
- package/dist/react.d.ts +319 -0
- package/dist/react.js +812 -0
- package/package.json +9 -1
package/dist/react.js
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/react/FoirProvider.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
// src/graphql/foir-request.ts
|
|
15
|
+
import { print } from "graphql";
|
|
16
|
+
function isFoirPreview(env) {
|
|
17
|
+
return env.FOIR_PREVIEW === "true";
|
|
18
|
+
}
|
|
19
|
+
var FoirRequestError = class extends Error {
|
|
20
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
21
|
+
status;
|
|
22
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
23
|
+
graphQLErrors;
|
|
24
|
+
cause;
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "FoirRequestError";
|
|
28
|
+
this.cause = options?.cause;
|
|
29
|
+
this.status = options?.status;
|
|
30
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
async function foirRequest(env, document, variables, options) {
|
|
34
|
+
const endpoint = env.PLATFORM_API_URL;
|
|
35
|
+
const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
36
|
+
if (!endpoint || !apiKey) {
|
|
37
|
+
throw new FoirRequestError(
|
|
38
|
+
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"x-api-key": apiKey
|
|
44
|
+
};
|
|
45
|
+
if (isFoirPreview(env)) {
|
|
46
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
47
|
+
}
|
|
48
|
+
if (options?.bearerToken) {
|
|
49
|
+
headers["Authorization"] = `Bearer ${options.bearerToken}`;
|
|
50
|
+
}
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(endpoint, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify({ query: print(document), variables })
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
|
|
63
|
+
status: res.status
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
if (json.errors?.length) {
|
|
68
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
69
|
+
if (json.data) {
|
|
70
|
+
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
71
|
+
} else {
|
|
72
|
+
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
73
|
+
graphQLErrors: json.errors
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!json.data) {
|
|
78
|
+
throw new FoirRequestError("[foir] empty GraphQL response");
|
|
79
|
+
}
|
|
80
|
+
return json.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/auth/operations.ts
|
|
84
|
+
import { parse } from "graphql";
|
|
85
|
+
var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
|
|
86
|
+
var CustomerLoginDocument = parse(
|
|
87
|
+
`mutation CustomerLogin($email: String!, $password: String!) {
|
|
88
|
+
customerLogin(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
89
|
+
}`
|
|
90
|
+
);
|
|
91
|
+
var CustomerRegisterDocument = parse(
|
|
92
|
+
`mutation CustomerRegister($email: String!, $password: String!) {
|
|
93
|
+
customerRegister(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
94
|
+
}`
|
|
95
|
+
);
|
|
96
|
+
var CustomerRefreshTokenDocument = parse(
|
|
97
|
+
`mutation CustomerRefreshToken($refreshToken: String!) {
|
|
98
|
+
customerRefreshToken(refreshToken: $refreshToken) { ${AUTH_RESULT_FIELDS} }
|
|
99
|
+
}`
|
|
100
|
+
);
|
|
101
|
+
var CustomerRequestOTPDocument = parse(
|
|
102
|
+
`mutation CustomerRequestOTP($email: String!) {
|
|
103
|
+
customerRequestOTP(email: $email)
|
|
104
|
+
}`
|
|
105
|
+
);
|
|
106
|
+
var CustomerLoginOTPDocument = parse(
|
|
107
|
+
`mutation CustomerLoginOTP($email: String!, $otp: String!) {
|
|
108
|
+
customerLoginOTP(email: $email, otp: $otp) { ${AUTH_RESULT_FIELDS} }
|
|
109
|
+
}`
|
|
110
|
+
);
|
|
111
|
+
var CustomerLoginWithProviderDocument = parse(
|
|
112
|
+
`mutation CustomerLoginWithProvider($input: ProviderLoginInput!) {
|
|
113
|
+
customerLoginWithProvider(input: $input) { redirectUrl token }
|
|
114
|
+
}`
|
|
115
|
+
);
|
|
116
|
+
var CustomerProviderCallbackDocument = parse(
|
|
117
|
+
`mutation CustomerProviderCallback($input: ProviderCallbackInput!) {
|
|
118
|
+
customerProviderCallback(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
119
|
+
}`
|
|
120
|
+
);
|
|
121
|
+
var CustomerProviderVerifyOTPDocument = parse(
|
|
122
|
+
`mutation CustomerProviderVerifyOTP($input: ProviderOTPVerifyInput!) {
|
|
123
|
+
customerProviderVerifyOTP(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
124
|
+
}`
|
|
125
|
+
);
|
|
126
|
+
var AuthProvidersDocument = parse(
|
|
127
|
+
`query AuthProviders {
|
|
128
|
+
authProviders { key name type isDefault }
|
|
129
|
+
}`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// src/auth/auth.ts
|
|
133
|
+
var TOKEN_KEY = "foir:token";
|
|
134
|
+
var REFRESH_KEY = "foir:refreshToken";
|
|
135
|
+
var CUSTOMER_ID_KEY = "foir:customerId";
|
|
136
|
+
var EMAIL_KEY = "foir:email";
|
|
137
|
+
async function customerLogin(env, vars) {
|
|
138
|
+
return (await foirRequest(env, CustomerLoginDocument, vars)).customerLogin;
|
|
139
|
+
}
|
|
140
|
+
async function customerRegister(env, vars) {
|
|
141
|
+
return (await foirRequest(env, CustomerRegisterDocument, vars)).customerRegister;
|
|
142
|
+
}
|
|
143
|
+
async function customerRequestOTP(env, email) {
|
|
144
|
+
return (await foirRequest(env, CustomerRequestOTPDocument, { email })).customerRequestOTP;
|
|
145
|
+
}
|
|
146
|
+
async function customerLoginOTP(env, vars) {
|
|
147
|
+
return (await foirRequest(env, CustomerLoginOTPDocument, vars)).customerLoginOTP;
|
|
148
|
+
}
|
|
149
|
+
async function customerRefreshToken(env, refreshToken) {
|
|
150
|
+
return (await foirRequest(env, CustomerRefreshTokenDocument, { refreshToken })).customerRefreshToken;
|
|
151
|
+
}
|
|
152
|
+
async function customerLoginWithProvider(env, input) {
|
|
153
|
+
return (await foirRequest(env, CustomerLoginWithProviderDocument, { input })).customerLoginWithProvider;
|
|
154
|
+
}
|
|
155
|
+
async function customerProviderCallback(env, input) {
|
|
156
|
+
return (await foirRequest(env, CustomerProviderCallbackDocument, { input })).customerProviderCallback;
|
|
157
|
+
}
|
|
158
|
+
async function getAuthProviders(env) {
|
|
159
|
+
return (await foirRequest(env, AuthProvidersDocument, {})).authProviders;
|
|
160
|
+
}
|
|
161
|
+
async function saveSession(adapter, result) {
|
|
162
|
+
await adapter.set(TOKEN_KEY, result.token);
|
|
163
|
+
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
164
|
+
await adapter.set(CUSTOMER_ID_KEY, result.customerId);
|
|
165
|
+
await adapter.set(EMAIL_KEY, result.email);
|
|
166
|
+
return { id: result.customerId, email: result.email };
|
|
167
|
+
}
|
|
168
|
+
async function clearSession(adapter) {
|
|
169
|
+
await adapter.unset(TOKEN_KEY);
|
|
170
|
+
await adapter.unset(REFRESH_KEY);
|
|
171
|
+
await adapter.unset(CUSTOMER_ID_KEY);
|
|
172
|
+
await adapter.unset(EMAIL_KEY);
|
|
173
|
+
}
|
|
174
|
+
async function getSessionToken(adapter) {
|
|
175
|
+
return await adapter.get(TOKEN_KEY) ?? null;
|
|
176
|
+
}
|
|
177
|
+
async function getSessionCustomer(adapter) {
|
|
178
|
+
const id = await adapter.get(CUSTOMER_ID_KEY) ?? null;
|
|
179
|
+
const email = await adapter.get(EMAIL_KEY) ?? null;
|
|
180
|
+
if (!id) return null;
|
|
181
|
+
return { id, email: email ?? "" };
|
|
182
|
+
}
|
|
183
|
+
async function loginWithPassword(adapter, env, vars) {
|
|
184
|
+
return saveSession(adapter, await customerLogin(env, vars));
|
|
185
|
+
}
|
|
186
|
+
async function registerWithPassword(adapter, env, vars) {
|
|
187
|
+
return saveSession(adapter, await customerRegister(env, vars));
|
|
188
|
+
}
|
|
189
|
+
async function loginWithOTP(adapter, env, vars) {
|
|
190
|
+
return saveSession(adapter, await customerLoginOTP(env, vars));
|
|
191
|
+
}
|
|
192
|
+
async function refreshSession(adapter, env) {
|
|
193
|
+
const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
|
|
194
|
+
if (!refreshToken) return null;
|
|
195
|
+
try {
|
|
196
|
+
const result = await customerRefreshToken(env, refreshToken);
|
|
197
|
+
await saveSession(adapter, result);
|
|
198
|
+
return { token: result.token, refreshToken: result.refreshToken };
|
|
199
|
+
} catch {
|
|
200
|
+
await clearSession(adapter);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/auth/session-client.ts
|
|
206
|
+
var DEFAULT_SESSION_PATH = "/session/token";
|
|
207
|
+
var DEFAULT_LOGOUT_PATH = "/session/logout";
|
|
208
|
+
var DEFAULT_REFRESH_SKEW_SECONDS = 60;
|
|
209
|
+
var MIN_REFRESH_DELAY_MS = 5e3;
|
|
210
|
+
var CSRF_HEADER = { "X-Foir-Session": "1" };
|
|
211
|
+
function createFoirSessionClient(config) {
|
|
212
|
+
const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH}`;
|
|
213
|
+
const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
214
|
+
const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
|
|
215
|
+
let cached = null;
|
|
216
|
+
let inflight = null;
|
|
217
|
+
let timer = null;
|
|
218
|
+
let destroyed = false;
|
|
219
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
220
|
+
function emit(user) {
|
|
221
|
+
for (const l of listeners) l(user);
|
|
222
|
+
}
|
|
223
|
+
function clearTimer() {
|
|
224
|
+
if (timer !== null) {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
timer = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function scheduleRefresh(expiresAt) {
|
|
230
|
+
clearTimer();
|
|
231
|
+
if (destroyed || typeof setTimeout === "undefined") return;
|
|
232
|
+
const delay = Math.max(expiresAt - Date.now() - skewMs, MIN_REFRESH_DELAY_MS);
|
|
233
|
+
timer = setTimeout(() => {
|
|
234
|
+
void exchange();
|
|
235
|
+
}, delay);
|
|
236
|
+
timer.unref?.();
|
|
237
|
+
}
|
|
238
|
+
function forget() {
|
|
239
|
+
const had = cached !== null;
|
|
240
|
+
cached = null;
|
|
241
|
+
clearTimer();
|
|
242
|
+
if (had) emit(null);
|
|
243
|
+
}
|
|
244
|
+
function exchange() {
|
|
245
|
+
if (inflight) return inflight;
|
|
246
|
+
const pending = (async () => {
|
|
247
|
+
let res;
|
|
248
|
+
try {
|
|
249
|
+
res = await fetch(sessionUrl, {
|
|
250
|
+
method: "GET",
|
|
251
|
+
// The whole point: send the first-party cookie cross-origin (but
|
|
252
|
+
// same-site) to the login domain. Without this the cookie is not sent.
|
|
253
|
+
credentials: "include",
|
|
254
|
+
headers: { ...CSRF_HEADER }
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
return cached;
|
|
258
|
+
}
|
|
259
|
+
if (res.status === 401) {
|
|
260
|
+
forget();
|
|
261
|
+
config.onSignOut?.();
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (!res.ok) return cached;
|
|
265
|
+
let json;
|
|
266
|
+
try {
|
|
267
|
+
json = await res.json();
|
|
268
|
+
} catch {
|
|
269
|
+
return cached;
|
|
270
|
+
}
|
|
271
|
+
if (!json.access_token || !json.customer_id) return cached;
|
|
272
|
+
const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 0;
|
|
273
|
+
const next = {
|
|
274
|
+
token: json.access_token,
|
|
275
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
276
|
+
user: { customerId: json.customer_id, email: json.email ?? "" }
|
|
277
|
+
};
|
|
278
|
+
const changed = cached?.user.customerId !== next.user.customerId;
|
|
279
|
+
cached = next;
|
|
280
|
+
scheduleRefresh(next.expiresAt);
|
|
281
|
+
if (changed) emit(next.user);
|
|
282
|
+
return next;
|
|
283
|
+
})();
|
|
284
|
+
inflight = pending;
|
|
285
|
+
void pending.finally(() => {
|
|
286
|
+
if (inflight === pending) inflight = null;
|
|
287
|
+
});
|
|
288
|
+
return pending;
|
|
289
|
+
}
|
|
290
|
+
function fresh() {
|
|
291
|
+
if (!cached) return null;
|
|
292
|
+
return Date.now() < cached.expiresAt - skewMs ? cached : null;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
async getToken() {
|
|
296
|
+
const hit = fresh();
|
|
297
|
+
if (hit) return hit.token;
|
|
298
|
+
const got = await exchange();
|
|
299
|
+
return got?.token ?? null;
|
|
300
|
+
},
|
|
301
|
+
async getUser() {
|
|
302
|
+
const hit = fresh();
|
|
303
|
+
if (hit) return hit.user;
|
|
304
|
+
const got = await exchange();
|
|
305
|
+
return got?.user ?? null;
|
|
306
|
+
},
|
|
307
|
+
async signOut() {
|
|
308
|
+
try {
|
|
309
|
+
await fetch(logoutUrl, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
credentials: "include",
|
|
312
|
+
headers: { ...CSRF_HEADER }
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
forget();
|
|
317
|
+
config.onSignOut?.();
|
|
318
|
+
},
|
|
319
|
+
signInUrl(returnTo) {
|
|
320
|
+
const dest = new URL(trimSlash(config.loginOrigin));
|
|
321
|
+
const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
322
|
+
if (target && target !== "/") dest.searchParams.set("returnTo", target);
|
|
323
|
+
return dest.toString();
|
|
324
|
+
},
|
|
325
|
+
subscribe(listener) {
|
|
326
|
+
listeners.add(listener);
|
|
327
|
+
return () => listeners.delete(listener);
|
|
328
|
+
},
|
|
329
|
+
destroy() {
|
|
330
|
+
destroyed = true;
|
|
331
|
+
clearTimer();
|
|
332
|
+
listeners.clear();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function trimSlash(s) {
|
|
337
|
+
return s.replace(/\/+$/, "");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/react/localStorageAdapter.ts
|
|
341
|
+
function createLocalStorageAdapter() {
|
|
342
|
+
const store = () => {
|
|
343
|
+
try {
|
|
344
|
+
return typeof window !== "undefined" ? window.localStorage : null;
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
get: (key) => store()?.getItem(key) ?? null,
|
|
351
|
+
set: (key, value) => {
|
|
352
|
+
store()?.setItem(key, value);
|
|
353
|
+
},
|
|
354
|
+
unset: (key) => {
|
|
355
|
+
store()?.removeItem(key);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/react/FoirProvider.tsx
|
|
361
|
+
import { jsx } from "react/jsx-runtime";
|
|
362
|
+
var FoirContext = createContext(null);
|
|
363
|
+
function FoirProvider({
|
|
364
|
+
env,
|
|
365
|
+
adapter,
|
|
366
|
+
hostedSession,
|
|
367
|
+
children
|
|
368
|
+
}) {
|
|
369
|
+
if (process.env.NODE_ENV !== "production" && adapter && hostedSession) {
|
|
370
|
+
console.warn(
|
|
371
|
+
"[foir] FoirProvider: `adapter` is ignored when `hostedSession` is set \u2014 the hosted session keeps the access token in memory and never persists it."
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const envRef = useRef(env);
|
|
375
|
+
envRef.current = env;
|
|
376
|
+
const adapterRef = useRef(adapter ?? createLocalStorageAdapter());
|
|
377
|
+
if (adapter && adapter !== adapterRef.current) adapterRef.current = adapter;
|
|
378
|
+
const [status, setStatus] = useState("loading");
|
|
379
|
+
const [customer, setCustomer] = useState(null);
|
|
380
|
+
const apply = useCallback((c) => {
|
|
381
|
+
setCustomer(c);
|
|
382
|
+
setStatus(c ? "authenticated" : "unauthenticated");
|
|
383
|
+
}, []);
|
|
384
|
+
const loginOrigin = hostedSession?.loginOrigin;
|
|
385
|
+
const sessionPath = hostedSession?.sessionPath;
|
|
386
|
+
const logoutPath = hostedSession?.logoutPath;
|
|
387
|
+
const refreshSkewSeconds = hostedSession?.refreshSkewSeconds;
|
|
388
|
+
const hosted = useMemo(() => {
|
|
389
|
+
if (!loginOrigin) return null;
|
|
390
|
+
return createFoirSessionClient({
|
|
391
|
+
loginOrigin,
|
|
392
|
+
sessionPath,
|
|
393
|
+
logoutPath,
|
|
394
|
+
refreshSkewSeconds,
|
|
395
|
+
onSignOut: () => apply(null)
|
|
396
|
+
});
|
|
397
|
+
}, [loginOrigin, sessionPath, logoutPath, refreshSkewSeconds, apply]);
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
let cancelled = false;
|
|
400
|
+
void (async () => {
|
|
401
|
+
if (hosted) {
|
|
402
|
+
const u = await hosted.getUser();
|
|
403
|
+
if (!cancelled) apply(u ? { id: u.customerId, email: u.email } : null);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const c = await getSessionCustomer(adapterRef.current);
|
|
407
|
+
if (!cancelled) apply(c);
|
|
408
|
+
})();
|
|
409
|
+
return () => {
|
|
410
|
+
cancelled = true;
|
|
411
|
+
};
|
|
412
|
+
}, [apply, hosted]);
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (!hosted) return;
|
|
415
|
+
return () => hosted.destroy();
|
|
416
|
+
}, [hosted]);
|
|
417
|
+
const assertTokenMode = useCallback(
|
|
418
|
+
(action) => {
|
|
419
|
+
if (hosted) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`[foir] ${action}() is not available in hostedSession mode \u2014 the customer signs in at your Foir login domain. Redirect to hostedSignInUrl() instead.`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
[hosted]
|
|
426
|
+
);
|
|
427
|
+
const signIn = useCallback(
|
|
428
|
+
async (vars) => {
|
|
429
|
+
assertTokenMode("signIn");
|
|
430
|
+
const c = await loginWithPassword(adapterRef.current, envRef.current, vars);
|
|
431
|
+
apply(c);
|
|
432
|
+
return c;
|
|
433
|
+
},
|
|
434
|
+
[apply, assertTokenMode]
|
|
435
|
+
);
|
|
436
|
+
const register = useCallback(
|
|
437
|
+
async (vars) => {
|
|
438
|
+
assertTokenMode("register");
|
|
439
|
+
const c = await registerWithPassword(adapterRef.current, envRef.current, vars);
|
|
440
|
+
apply(c);
|
|
441
|
+
return c;
|
|
442
|
+
},
|
|
443
|
+
[apply, assertTokenMode]
|
|
444
|
+
);
|
|
445
|
+
const requestOtp = useCallback(
|
|
446
|
+
(email) => {
|
|
447
|
+
assertTokenMode("requestOtp");
|
|
448
|
+
return customerRequestOTP(envRef.current, email);
|
|
449
|
+
},
|
|
450
|
+
[assertTokenMode]
|
|
451
|
+
);
|
|
452
|
+
const signInWithOtp = useCallback(
|
|
453
|
+
async (vars) => {
|
|
454
|
+
assertTokenMode("signInWithOtp");
|
|
455
|
+
const c = await loginWithOTP(adapterRef.current, envRef.current, vars);
|
|
456
|
+
apply(c);
|
|
457
|
+
return c;
|
|
458
|
+
},
|
|
459
|
+
[apply, assertTokenMode]
|
|
460
|
+
);
|
|
461
|
+
const signInWithProvider = useCallback(
|
|
462
|
+
async (providerKey, opts) => {
|
|
463
|
+
assertTokenMode("signInWithProvider");
|
|
464
|
+
const res = await customerLoginWithProvider(envRef.current, {
|
|
465
|
+
providerKey,
|
|
466
|
+
...opts
|
|
467
|
+
});
|
|
468
|
+
if (res.redirectUrl && typeof window !== "undefined") {
|
|
469
|
+
window.location.assign(res.redirectUrl);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
[assertTokenMode]
|
|
473
|
+
);
|
|
474
|
+
const completeProviderLogin = useCallback(
|
|
475
|
+
async (input) => {
|
|
476
|
+
assertTokenMode("completeProviderLogin");
|
|
477
|
+
const result = await customerProviderCallback(envRef.current, input);
|
|
478
|
+
const c = await saveSession(adapterRef.current, result);
|
|
479
|
+
apply(c);
|
|
480
|
+
return c;
|
|
481
|
+
},
|
|
482
|
+
[apply, assertTokenMode]
|
|
483
|
+
);
|
|
484
|
+
const signOut = useCallback(async () => {
|
|
485
|
+
if (hosted) {
|
|
486
|
+
await hosted.signOut();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await clearSession(adapterRef.current);
|
|
490
|
+
apply(null);
|
|
491
|
+
}, [apply, hosted]);
|
|
492
|
+
const refresh = useCallback(async () => {
|
|
493
|
+
if (hosted) {
|
|
494
|
+
return await hosted.getToken() !== null;
|
|
495
|
+
}
|
|
496
|
+
const tokens = await refreshSession(adapterRef.current, envRef.current);
|
|
497
|
+
if (!tokens) {
|
|
498
|
+
apply(null);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
}, [apply, hosted]);
|
|
503
|
+
const getToken = useCallback(
|
|
504
|
+
() => hosted ? hosted.getToken() : getSessionToken(adapterRef.current),
|
|
505
|
+
[hosted]
|
|
506
|
+
);
|
|
507
|
+
const getProviders = useCallback(() => getAuthProviders(envRef.current), []);
|
|
508
|
+
const hostedSignInUrl = useMemo(
|
|
509
|
+
() => hosted ? (returnTo) => hosted.signInUrl(returnTo) : null,
|
|
510
|
+
[hosted]
|
|
511
|
+
);
|
|
512
|
+
const value = useMemo(
|
|
513
|
+
() => ({
|
|
514
|
+
status,
|
|
515
|
+
customer,
|
|
516
|
+
env,
|
|
517
|
+
adapter: adapterRef.current,
|
|
518
|
+
signIn,
|
|
519
|
+
register,
|
|
520
|
+
requestOtp,
|
|
521
|
+
signInWithOtp,
|
|
522
|
+
signInWithProvider,
|
|
523
|
+
completeProviderLogin,
|
|
524
|
+
signOut,
|
|
525
|
+
refresh,
|
|
526
|
+
getToken,
|
|
527
|
+
getProviders,
|
|
528
|
+
hostedSignInUrl
|
|
529
|
+
}),
|
|
530
|
+
[
|
|
531
|
+
status,
|
|
532
|
+
customer,
|
|
533
|
+
env,
|
|
534
|
+
signIn,
|
|
535
|
+
register,
|
|
536
|
+
requestOtp,
|
|
537
|
+
signInWithOtp,
|
|
538
|
+
signInWithProvider,
|
|
539
|
+
completeProviderLogin,
|
|
540
|
+
signOut,
|
|
541
|
+
refresh,
|
|
542
|
+
getToken,
|
|
543
|
+
getProviders,
|
|
544
|
+
hostedSignInUrl
|
|
545
|
+
]
|
|
546
|
+
);
|
|
547
|
+
return /* @__PURE__ */ jsx(FoirContext.Provider, { value, children });
|
|
548
|
+
}
|
|
549
|
+
function useFoirContext() {
|
|
550
|
+
const ctx = useContext(FoirContext);
|
|
551
|
+
if (!ctx) {
|
|
552
|
+
throw new Error("Foir hooks must be used inside a <FoirProvider>.");
|
|
553
|
+
}
|
|
554
|
+
return ctx;
|
|
555
|
+
}
|
|
556
|
+
function useFoirSession() {
|
|
557
|
+
const { status, customer } = useFoirContext();
|
|
558
|
+
return {
|
|
559
|
+
status,
|
|
560
|
+
customer,
|
|
561
|
+
isLoading: status === "loading",
|
|
562
|
+
isAuthenticated: status === "authenticated"
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function useFoirUser() {
|
|
566
|
+
return useFoirContext().customer;
|
|
567
|
+
}
|
|
568
|
+
function useFoirAuth() {
|
|
569
|
+
const {
|
|
570
|
+
signIn,
|
|
571
|
+
register,
|
|
572
|
+
requestOtp,
|
|
573
|
+
signInWithOtp,
|
|
574
|
+
signInWithProvider,
|
|
575
|
+
completeProviderLogin,
|
|
576
|
+
signOut,
|
|
577
|
+
refresh,
|
|
578
|
+
getToken,
|
|
579
|
+
getProviders,
|
|
580
|
+
hostedSignInUrl
|
|
581
|
+
} = useFoirContext();
|
|
582
|
+
return {
|
|
583
|
+
signIn,
|
|
584
|
+
register,
|
|
585
|
+
requestOtp,
|
|
586
|
+
signInWithOtp,
|
|
587
|
+
signInWithProvider,
|
|
588
|
+
completeProviderLogin,
|
|
589
|
+
signOut,
|
|
590
|
+
refresh,
|
|
591
|
+
getToken,
|
|
592
|
+
getProviders,
|
|
593
|
+
hostedSignInUrl
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/react/SignIn.tsx
|
|
598
|
+
import { useState as useState3 } from "react";
|
|
599
|
+
|
|
600
|
+
// src/react/OAuthButtons.tsx
|
|
601
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
602
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
603
|
+
var REDIRECT_TYPES = /* @__PURE__ */ new Set(["OAUTH2", "OIDC", "SAML"]);
|
|
604
|
+
var rowStyle = { display: "flex", flexDirection: "column", gap: 8 };
|
|
605
|
+
var btnStyle = {
|
|
606
|
+
padding: "8px 12px",
|
|
607
|
+
borderRadius: 6,
|
|
608
|
+
border: "1px solid currentColor",
|
|
609
|
+
background: "transparent",
|
|
610
|
+
cursor: "pointer",
|
|
611
|
+
font: "inherit"
|
|
612
|
+
};
|
|
613
|
+
function OAuthButtons({
|
|
614
|
+
returnTo,
|
|
615
|
+
redirectUri,
|
|
616
|
+
filter,
|
|
617
|
+
children,
|
|
618
|
+
className,
|
|
619
|
+
onError
|
|
620
|
+
}) {
|
|
621
|
+
const { getProviders, signInWithProvider } = useFoirAuth();
|
|
622
|
+
const [providers, setProviders] = useState2([]);
|
|
623
|
+
useEffect2(() => {
|
|
624
|
+
let cancelled = false;
|
|
625
|
+
void getProviders().then((list) => {
|
|
626
|
+
if (!cancelled) setProviders(list);
|
|
627
|
+
}).catch((err) => onError?.(err));
|
|
628
|
+
return () => {
|
|
629
|
+
cancelled = true;
|
|
630
|
+
};
|
|
631
|
+
}, [getProviders, onError]);
|
|
632
|
+
const shown = providers.filter(
|
|
633
|
+
(p) => filter ? filter(p) : REDIRECT_TYPES.has(p.type)
|
|
634
|
+
);
|
|
635
|
+
if (shown.length === 0) return null;
|
|
636
|
+
const start = (p) => {
|
|
637
|
+
void signInWithProvider(p.key, { returnTo, redirectUri }).catch(
|
|
638
|
+
(err) => onError?.(err)
|
|
639
|
+
);
|
|
640
|
+
};
|
|
641
|
+
return /* @__PURE__ */ jsx2("div", { className, "data-foir-oauth-buttons": true, style: className ? void 0 : rowStyle, children: shown.map(
|
|
642
|
+
(p) => children ? /* @__PURE__ */ jsx2("span", { children: children(p, () => start(p)) }, p.key) : /* @__PURE__ */ jsxs(
|
|
643
|
+
"button",
|
|
644
|
+
{
|
|
645
|
+
type: "button",
|
|
646
|
+
"data-foir-provider": p.key,
|
|
647
|
+
onClick: () => start(p),
|
|
648
|
+
style: className ? void 0 : btnStyle,
|
|
649
|
+
children: [
|
|
650
|
+
"Continue with ",
|
|
651
|
+
p.name
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
p.key
|
|
655
|
+
)
|
|
656
|
+
) });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/react/SignIn.tsx
|
|
660
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
661
|
+
var formStyle = { display: "flex", flexDirection: "column", gap: 10, maxWidth: 320 };
|
|
662
|
+
var inputStyle = {
|
|
663
|
+
padding: "8px 10px",
|
|
664
|
+
borderRadius: 6,
|
|
665
|
+
border: "1px solid currentColor",
|
|
666
|
+
background: "transparent",
|
|
667
|
+
font: "inherit"
|
|
668
|
+
};
|
|
669
|
+
var submitStyle = { ...inputStyle, cursor: "pointer", fontWeight: 600 };
|
|
670
|
+
var errorStyle = { color: "#c0392b", fontSize: 13, margin: 0 };
|
|
671
|
+
var dividerStyle = { fontSize: 12, opacity: 0.6, textAlign: "center" };
|
|
672
|
+
function SignIn({
|
|
673
|
+
method = "password",
|
|
674
|
+
hostedUrl,
|
|
675
|
+
oauth = true,
|
|
676
|
+
afterSignInUrl,
|
|
677
|
+
onSignIn,
|
|
678
|
+
className
|
|
679
|
+
}) {
|
|
680
|
+
const { signIn, requestOtp, signInWithOtp } = useFoirAuth();
|
|
681
|
+
const [email, setEmail] = useState3("");
|
|
682
|
+
const [password, setPassword] = useState3("");
|
|
683
|
+
const [otp, setOtp] = useState3("");
|
|
684
|
+
const [otpSent, setOtpSent] = useState3(false);
|
|
685
|
+
const [busy, setBusy] = useState3(false);
|
|
686
|
+
const [error, setError] = useState3(null);
|
|
687
|
+
const styled = !className;
|
|
688
|
+
if (hostedUrl) {
|
|
689
|
+
return /* @__PURE__ */ jsx3("div", { className, "data-foir-signin": true, children: /* @__PURE__ */ jsx3("a", { href: hostedUrl, "data-foir-hosted-login": true, style: styled ? submitStyle : void 0, children: "Sign in" }) });
|
|
690
|
+
}
|
|
691
|
+
const finish = (customer) => {
|
|
692
|
+
onSignIn?.(customer);
|
|
693
|
+
if (afterSignInUrl && typeof window !== "undefined") {
|
|
694
|
+
window.location.assign(afterSignInUrl);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const onSubmit = async (e) => {
|
|
698
|
+
e.preventDefault();
|
|
699
|
+
setError(null);
|
|
700
|
+
setBusy(true);
|
|
701
|
+
try {
|
|
702
|
+
if (method === "otp") {
|
|
703
|
+
if (!otpSent) {
|
|
704
|
+
await requestOtp(email);
|
|
705
|
+
setOtpSent(true);
|
|
706
|
+
} else {
|
|
707
|
+
finish(await signInWithOtp({ email, otp }));
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
finish(await signIn({ email, password }));
|
|
711
|
+
}
|
|
712
|
+
} catch (err) {
|
|
713
|
+
setError(err instanceof Error ? err.message : "Sign-in failed");
|
|
714
|
+
} finally {
|
|
715
|
+
setBusy(false);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
return /* @__PURE__ */ jsxs2("div", { className, "data-foir-signin": true, style: styled ? formStyle : void 0, children: [
|
|
719
|
+
/* @__PURE__ */ jsxs2("form", { onSubmit, style: styled ? formStyle : void 0, children: [
|
|
720
|
+
/* @__PURE__ */ jsx3(
|
|
721
|
+
"input",
|
|
722
|
+
{
|
|
723
|
+
type: "email",
|
|
724
|
+
name: "email",
|
|
725
|
+
autoComplete: "email",
|
|
726
|
+
placeholder: "Email",
|
|
727
|
+
required: true,
|
|
728
|
+
value: email,
|
|
729
|
+
onChange: (e) => setEmail(e.target.value),
|
|
730
|
+
disabled: busy || method === "otp" && otpSent,
|
|
731
|
+
style: styled ? inputStyle : void 0
|
|
732
|
+
}
|
|
733
|
+
),
|
|
734
|
+
method === "password" && /* @__PURE__ */ jsx3(
|
|
735
|
+
"input",
|
|
736
|
+
{
|
|
737
|
+
type: "password",
|
|
738
|
+
name: "password",
|
|
739
|
+
autoComplete: "current-password",
|
|
740
|
+
placeholder: "Password",
|
|
741
|
+
required: true,
|
|
742
|
+
value: password,
|
|
743
|
+
onChange: (e) => setPassword(e.target.value),
|
|
744
|
+
disabled: busy,
|
|
745
|
+
style: styled ? inputStyle : void 0
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
method === "otp" && otpSent && /* @__PURE__ */ jsx3(
|
|
749
|
+
"input",
|
|
750
|
+
{
|
|
751
|
+
type: "text",
|
|
752
|
+
name: "otp",
|
|
753
|
+
inputMode: "numeric",
|
|
754
|
+
autoComplete: "one-time-code",
|
|
755
|
+
placeholder: "One-time code",
|
|
756
|
+
required: true,
|
|
757
|
+
value: otp,
|
|
758
|
+
onChange: (e) => setOtp(e.target.value),
|
|
759
|
+
disabled: busy,
|
|
760
|
+
style: styled ? inputStyle : void 0
|
|
761
|
+
}
|
|
762
|
+
),
|
|
763
|
+
error && /* @__PURE__ */ jsx3("p", { "data-foir-error": true, style: styled ? errorStyle : void 0, children: error }),
|
|
764
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", disabled: busy, style: styled ? submitStyle : void 0, children: busy ? "Please wait\u2026" : method === "otp" ? otpSent ? "Verify code" : "Send code" : "Sign in" })
|
|
765
|
+
] }),
|
|
766
|
+
oauth && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
767
|
+
/* @__PURE__ */ jsx3("div", { "data-foir-divider": true, style: styled ? dividerStyle : void 0, children: "or" }),
|
|
768
|
+
/* @__PURE__ */ jsx3(OAuthButtons, { returnTo: afterSignInUrl, onError: (err) => setError(err instanceof Error ? err.message : "Sign-in failed") })
|
|
769
|
+
] })
|
|
770
|
+
] });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/react/UserButton.tsx
|
|
774
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
775
|
+
var rootStyle = { display: "flex", alignItems: "center", gap: 8 };
|
|
776
|
+
var emailStyle = { fontSize: 14 };
|
|
777
|
+
var btnStyle2 = {
|
|
778
|
+
padding: "4px 10px",
|
|
779
|
+
borderRadius: 6,
|
|
780
|
+
border: "1px solid currentColor",
|
|
781
|
+
background: "transparent",
|
|
782
|
+
cursor: "pointer",
|
|
783
|
+
font: "inherit",
|
|
784
|
+
fontSize: 13
|
|
785
|
+
};
|
|
786
|
+
function UserButton({ afterSignOut, afterSignOutUrl, className }) {
|
|
787
|
+
const user = useFoirUser();
|
|
788
|
+
const { signOut } = useFoirAuth();
|
|
789
|
+
if (!user) return null;
|
|
790
|
+
const styled = !className;
|
|
791
|
+
const onSignOut = async () => {
|
|
792
|
+
await signOut();
|
|
793
|
+
afterSignOut?.();
|
|
794
|
+
if (afterSignOutUrl && typeof window !== "undefined") {
|
|
795
|
+
window.location.assign(afterSignOutUrl);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
return /* @__PURE__ */ jsxs3("div", { className, "data-foir-user-button": true, style: styled ? rootStyle : void 0, children: [
|
|
799
|
+
/* @__PURE__ */ jsx4("span", { "data-foir-user-email": true, style: styled ? emailStyle : void 0, children: user.email }),
|
|
800
|
+
/* @__PURE__ */ jsx4("button", { type: "button", onClick: onSignOut, style: styled ? btnStyle2 : void 0, children: "Sign out" })
|
|
801
|
+
] });
|
|
802
|
+
}
|
|
803
|
+
export {
|
|
804
|
+
FoirProvider,
|
|
805
|
+
OAuthButtons,
|
|
806
|
+
SignIn,
|
|
807
|
+
UserButton,
|
|
808
|
+
createLocalStorageAdapter,
|
|
809
|
+
useFoirAuth,
|
|
810
|
+
useFoirSession,
|
|
811
|
+
useFoirUser
|
|
812
|
+
};
|