@synqauth/react-native 0.0.0 → 0.2.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 +2 -0
- package/dist/index.d.ts +77 -2
- package/dist/index.js +156 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ const { signIn, signOut } = useAuth()
|
|
|
32
32
|
|
|
33
33
|
Sign-in opens the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never an embedded web view. The session, refresh token included, stays in the device's secure storage.
|
|
34
34
|
|
|
35
|
+
To sign people in on the app's own screens instead (own-UI sign-in, on for the app in the dashboard), use `useSignIn()`: `password.signIn({ email, password })`, `emailCode.send(email)` and `emailCode.verify(code)`, and the rest. Passkeys, and anything else only the hosted page can do, go on in the system browser with `continueInBrowser()`.
|
|
36
|
+
|
|
35
37
|
## License
|
|
36
38
|
|
|
37
39
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccountClient, AuthorizationUrlOptions, IdTokenClaims, Session, SynqClient, SynqError, SynqStorage } from "@synqauth/js";
|
|
1
|
+
import { AccountClient, AuthorizationUrlOptions, ChallengeMethod, ChallengeProblem, DeniedReason, IdTokenClaims, RedirectReason, Session, StartSignInOptions, SynqClient, SynqError, SynqStorage } from "@synqauth/js";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
//#region src/storage.d.ts
|
|
4
4
|
/** The part of `expo-secure-store` the SDK uses: pass the module itself. */
|
|
@@ -142,4 +142,79 @@ interface UseAuth {
|
|
|
142
142
|
/** Signing in and out. */
|
|
143
143
|
export declare function useAuth(): UseAuth;
|
|
144
144
|
//#endregion
|
|
145
|
-
|
|
145
|
+
//#region src/sign-in.d.ts
|
|
146
|
+
/** Where a sign-in is: what the screen shows. */
|
|
147
|
+
type SignInStep = 'start' | 'code' | 'verify-email' | 'done';
|
|
148
|
+
/** What went wrong with the last step: branch on `code`, never on the English. */
|
|
149
|
+
interface SignInProblem {
|
|
150
|
+
/**
|
|
151
|
+
* Synq's problem with the step (`wrong_credentials`, `wrong_code`…), or
|
|
152
|
+
* the sign-in's own: `expired` (it timed out: start again), `refused`
|
|
153
|
+
* (this person can't sign in here: see `reason`), `unavailable` (Synq
|
|
154
|
+
* couldn't be reached, or failed).
|
|
155
|
+
*/
|
|
156
|
+
readonly code: ChallengeProblem | 'expired' | 'refused' | 'unavailable';
|
|
157
|
+
/** Why, when `refused`. */
|
|
158
|
+
readonly reason?: DeniedReason;
|
|
159
|
+
/** Synq's words for it, in English: show your own for `code` in another language. */
|
|
160
|
+
readonly message: string;
|
|
161
|
+
/** Seconds to wait first, when Synq said. */
|
|
162
|
+
readonly retryAfter?: number;
|
|
163
|
+
}
|
|
164
|
+
/** What only the hosted page can do for this sign-in, and what to take there. */
|
|
165
|
+
interface SignInHandover {
|
|
166
|
+
readonly reason: RedirectReason;
|
|
167
|
+
readonly method?: ChallengeMethod;
|
|
168
|
+
readonly loginHint?: string;
|
|
169
|
+
}
|
|
170
|
+
/** What `useSignIn()` answers: the sign-in's step, what went wrong, and each method's steps. */
|
|
171
|
+
interface UseSignIn {
|
|
172
|
+
readonly step: SignInStep;
|
|
173
|
+
/** Whether a step is on its way to Synq. */
|
|
174
|
+
readonly pending: boolean;
|
|
175
|
+
readonly problem: SignInProblem | null;
|
|
176
|
+
/** The address a code went to. */
|
|
177
|
+
readonly email: string | undefined;
|
|
178
|
+
/** Whether a new account's address may be left unverified for now. */
|
|
179
|
+
readonly canSkip: boolean;
|
|
180
|
+
/** Set when only the hosted page can go on: `continueInBrowser()` goes there. */
|
|
181
|
+
readonly handover: SignInHandover | null;
|
|
182
|
+
readonly password: {
|
|
183
|
+
readonly signIn: (input: {
|
|
184
|
+
email: string;
|
|
185
|
+
password: string;
|
|
186
|
+
}) => Promise<void>;
|
|
187
|
+
readonly signUp: (input: {
|
|
188
|
+
email: string;
|
|
189
|
+
password: string;
|
|
190
|
+
}) => Promise<void>;
|
|
191
|
+
};
|
|
192
|
+
readonly emailCode: {
|
|
193
|
+
readonly send: (email: string) => Promise<void>;
|
|
194
|
+
readonly verify: (code: string) => Promise<void>;
|
|
195
|
+
readonly resend: () => Promise<void>;
|
|
196
|
+
};
|
|
197
|
+
readonly verifyEmail: {
|
|
198
|
+
readonly verify: (code: string) => Promise<void>;
|
|
199
|
+
readonly resend: () => Promise<void>;
|
|
200
|
+
readonly skip: () => Promise<void>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Goes on at the hosted page in the system browser, with the method and
|
|
204
|
+
* address Synq named, or these: a passkey, a token gate, a step-up. A
|
|
205
|
+
* closed browser is the error `cancelled`.
|
|
206
|
+
*/
|
|
207
|
+
readonly continueInBrowser: (options?: {
|
|
208
|
+
method?: string;
|
|
209
|
+
loginHint?: string;
|
|
210
|
+
}) => Promise<SynqResult<Session>>;
|
|
211
|
+
/** Drops the sign-in under way and goes back to the first step. */
|
|
212
|
+
readonly startOver: () => void;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* One sign-in on the app's own screens. The provider follows the session
|
|
216
|
+
* it ends with, as after `useAuth().signIn()`.
|
|
217
|
+
*/
|
|
218
|
+
export declare function useSignIn(options?: StartSignInOptions): UseSignIn;
|
|
219
|
+
//#endregion
|
|
220
|
+
export type { SecureStoreModule, SignInHandover, SignInProblem, SignInStep, SynqNative, SynqNativeOptions, SynqNativeProviderProps, SynqResult, UseAuth, UseSession, UseSignIn, UseUser, WebBrowserModule };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SynqError, accountApiUrl, createAccountClient, createSynqClient, isSynqError } from "@synqauth/js";
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/storage.ts
|
|
5
5
|
/**
|
|
@@ -181,6 +181,7 @@ function SynqNativeProvider({ synq, issuer, children }) {
|
|
|
181
181
|
children
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
/** The provider's client and session, for this package's other hooks. */
|
|
184
185
|
function useSynq() {
|
|
185
186
|
const context = useContext(SynqContext);
|
|
186
187
|
if (!context) throw new Error("Synq hooks need a <SynqNativeProvider> above them");
|
|
@@ -249,8 +250,161 @@ function useAuth() {
|
|
|
249
250
|
signOut
|
|
250
251
|
};
|
|
251
252
|
}
|
|
253
|
+
/** Whatever a call threw, as a `SynqError`. */
|
|
252
254
|
function asSynqError(error) {
|
|
253
255
|
return isSynqError(error) ? error : new SynqError("network_error", String(error), { cause: error });
|
|
254
256
|
}
|
|
255
257
|
//#endregion
|
|
256
|
-
|
|
258
|
+
//#region src/sign-in.ts
|
|
259
|
+
/**
|
|
260
|
+
* One sign-in on the app's own screens. The provider follows the session
|
|
261
|
+
* it ends with, as after `useAuth().signIn()`.
|
|
262
|
+
*/
|
|
263
|
+
function useSignIn(options = {}) {
|
|
264
|
+
const { synq } = useSynq();
|
|
265
|
+
const auth = useAuth();
|
|
266
|
+
const flow = useRef(null);
|
|
267
|
+
const [step, setStep] = useState("start");
|
|
268
|
+
const [pending, setPending] = useState(0);
|
|
269
|
+
const [problem, setProblem] = useState(null);
|
|
270
|
+
const [email, setEmail] = useState(void 0);
|
|
271
|
+
const [canSkip, setCanSkip] = useState(false);
|
|
272
|
+
const [handover, setHandover] = useState(null);
|
|
273
|
+
const start = useRef(options);
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
start.current = options;
|
|
276
|
+
});
|
|
277
|
+
const actions = useMemo(() => {
|
|
278
|
+
function show(result) {
|
|
279
|
+
switch (result.status) {
|
|
280
|
+
case "signed-in":
|
|
281
|
+
flow.current = null;
|
|
282
|
+
setStep("done");
|
|
283
|
+
setProblem(null);
|
|
284
|
+
return;
|
|
285
|
+
case "code-sent":
|
|
286
|
+
setStep("code");
|
|
287
|
+
setEmail(result.email);
|
|
288
|
+
setProblem(null);
|
|
289
|
+
return;
|
|
290
|
+
case "verify-email":
|
|
291
|
+
setStep("verify-email");
|
|
292
|
+
setEmail(result.email);
|
|
293
|
+
setCanSkip(result.canSkip);
|
|
294
|
+
setProblem(null);
|
|
295
|
+
return;
|
|
296
|
+
case "retry":
|
|
297
|
+
setProblem({
|
|
298
|
+
code: result.problem,
|
|
299
|
+
message: result.message,
|
|
300
|
+
...result.retryAfter === void 0 ? {} : { retryAfter: result.retryAfter }
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
case "hosted":
|
|
304
|
+
setHandover({
|
|
305
|
+
reason: result.reason,
|
|
306
|
+
...result.method ? { method: result.method } : {},
|
|
307
|
+
...result.loginHint ? { loginHint: result.loginHint } : {}
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
case "passkey":
|
|
311
|
+
setHandover({
|
|
312
|
+
reason: "passkey_unavailable",
|
|
313
|
+
method: "passkey"
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
case "refused":
|
|
317
|
+
flow.current = null;
|
|
318
|
+
setStep("start");
|
|
319
|
+
setProblem({
|
|
320
|
+
code: "refused",
|
|
321
|
+
reason: result.reason,
|
|
322
|
+
message: result.message
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function run(ask) {
|
|
328
|
+
flow.current ??= synq.client.startSignIn(start.current);
|
|
329
|
+
const current = flow.current;
|
|
330
|
+
setPending((n) => n + 1);
|
|
331
|
+
try {
|
|
332
|
+
show(await ask(current));
|
|
333
|
+
} catch (thrown) {
|
|
334
|
+
const error = asSynqError(thrown);
|
|
335
|
+
if (error.code === "invalid_session") {
|
|
336
|
+
flow.current = null;
|
|
337
|
+
setStep("start");
|
|
338
|
+
setProblem({
|
|
339
|
+
code: "expired",
|
|
340
|
+
message: error.message
|
|
341
|
+
});
|
|
342
|
+
} else if (error.code === "too_many_requests") setProblem({
|
|
343
|
+
code: "limited",
|
|
344
|
+
message: error.message,
|
|
345
|
+
...error.retryAfter === void 0 ? {} : { retryAfter: error.retryAfter }
|
|
346
|
+
});
|
|
347
|
+
else setProblem({
|
|
348
|
+
code: "unavailable",
|
|
349
|
+
message: error.message
|
|
350
|
+
});
|
|
351
|
+
} finally {
|
|
352
|
+
setPending((n) => n - 1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
password: {
|
|
357
|
+
signIn: (input) => run((f) => f.password.signIn(input)),
|
|
358
|
+
signUp: (input) => run((f) => f.password.signUp(input))
|
|
359
|
+
},
|
|
360
|
+
emailCode: {
|
|
361
|
+
send: (address) => run((f) => f.email.send(address)),
|
|
362
|
+
verify: (code) => run((f) => f.email.verify(code)),
|
|
363
|
+
resend: () => run((f) => f.email.resend())
|
|
364
|
+
},
|
|
365
|
+
verifyEmail: {
|
|
366
|
+
verify: (code) => run((f) => f.verifyEmail.verify(code)),
|
|
367
|
+
resend: () => run((f) => f.verifyEmail.resend()),
|
|
368
|
+
skip: () => run((f) => f.verifyEmail.skip())
|
|
369
|
+
},
|
|
370
|
+
startOver: () => {
|
|
371
|
+
flow.current = null;
|
|
372
|
+
setStep("start");
|
|
373
|
+
setProblem(null);
|
|
374
|
+
setEmail(void 0);
|
|
375
|
+
setCanSkip(false);
|
|
376
|
+
setHandover(null);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}, [synq]);
|
|
380
|
+
const continueInBrowser = useCallback(async (overrides = {}) => {
|
|
381
|
+
const method = overrides.method ?? handover?.method;
|
|
382
|
+
const loginHint = overrides.loginHint ?? handover?.loginHint ?? email;
|
|
383
|
+
const result = await auth.signIn({
|
|
384
|
+
...method ? { method } : {},
|
|
385
|
+
...loginHint ? { loginHint } : {}
|
|
386
|
+
});
|
|
387
|
+
if (!result.error) {
|
|
388
|
+
flow.current = null;
|
|
389
|
+
setHandover(null);
|
|
390
|
+
setStep("done");
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}, [
|
|
394
|
+
auth,
|
|
395
|
+
handover,
|
|
396
|
+
email
|
|
397
|
+
]);
|
|
398
|
+
return {
|
|
399
|
+
step,
|
|
400
|
+
pending: pending > 0,
|
|
401
|
+
problem,
|
|
402
|
+
email,
|
|
403
|
+
canSkip,
|
|
404
|
+
handover,
|
|
405
|
+
...actions,
|
|
406
|
+
continueInBrowser
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
export { CHUNK_SIZE, SynqNativeProvider, createSynqNative, secureStorage, useAuth, useSession, useSignIn, useUser };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synqauth/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Synq for React Native: sign-in through the system browser, with tokens in the device's secure storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@synqauth/js": "0.
|
|
30
|
+
"@synqauth/js": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=19.0.0"
|