@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 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
- export type { SecureStoreModule, SynqNative, SynqNativeOptions, SynqNativeProviderProps, SynqResult, UseAuth, UseSession, UseUser, WebBrowserModule };
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
- export { CHUNK_SIZE, SynqNativeProvider, createSynqNative, secureStorage, useAuth, useSession, useUser };
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.0.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.0.0"
30
+ "@synqauth/js": "0.2.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=19.0.0"