@synqauth/react-native 0.0.0 → 0.3.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
@@ -4,7 +4,7 @@ Synq for React Native: sign-in through the system browser, with tokens in the de
4
4
 
5
5
  Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at [docs.synq.build](https://docs.synq.build).
6
6
 
7
- This package is at 0.x and hasn't been published yet: its API can change in any release until Synq's first customer depends on it.
7
+ This package is at 0.x, so a minor release can change its API: see its CHANGELOG.md.
8
8
 
9
9
  ## Usage
10
10
 
@@ -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, AvatarInfo, AvatarPerson, AvatarStyle, 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. */
@@ -128,6 +128,26 @@ interface UseUser {
128
128
  }
129
129
  /** Who is signed in, and their account. */
130
130
  export declare function useUser(): UseUser;
131
+ /** What `useAvatar()` answers: what the avatar is drawn from, and Synq's own default as SVG. */
132
+ interface UseAvatar extends AvatarInfo {
133
+ /**
134
+ * Synq's own default for them, as SVG markup (`avatarSvg`): draw it with
135
+ * react-native-svg's `SvgXml`, clipped to a circle, where no picture of
136
+ * theirs or of the app's own shows.
137
+ */
138
+ readonly svg: string;
139
+ }
140
+ /**
141
+ * What the avatar of `user` is drawn from (AERO-2704): the signed-in person
142
+ * by default, from their ID token, or anyone as Synq names them (`id` and
143
+ * `username` from the management API work too). Show `picture` when its
144
+ * `source` is one the app shows; else the app's own art from `seed`
145
+ * (`defaultPick` over its set); else `defaultPicture`; else `svg`. Null
146
+ * while there is nobody.
147
+ */
148
+ export declare function useAvatar(user?: AvatarPerson | null, options?: {
149
+ style?: AvatarStyle;
150
+ }): UseAvatar | null;
131
151
  /** What `useAuth()` answers: signing in and out, each answering `{ data, error }`. */
132
152
  interface UseAuth {
133
153
  readonly isLoaded: boolean;
@@ -142,4 +162,79 @@ interface UseAuth {
142
162
  /** Signing in and out. */
143
163
  export declare function useAuth(): UseAuth;
144
164
  //#endregion
145
- export type { SecureStoreModule, SynqNative, SynqNativeOptions, SynqNativeProviderProps, SynqResult, UseAuth, UseSession, UseUser, WebBrowserModule };
165
+ //#region src/sign-in.d.ts
166
+ /** Where a sign-in is: what the screen shows. */
167
+ type SignInStep = 'start' | 'code' | 'verify-email' | 'done';
168
+ /** What went wrong with the last step: branch on `code`, never on the English. */
169
+ interface SignInProblem {
170
+ /**
171
+ * Synq's problem with the step (`wrong_credentials`, `wrong_code`…), or
172
+ * the sign-in's own: `expired` (it timed out: start again), `refused`
173
+ * (this person can't sign in here: see `reason`), `unavailable` (Synq
174
+ * couldn't be reached, or failed).
175
+ */
176
+ readonly code: ChallengeProblem | 'expired' | 'refused' | 'unavailable';
177
+ /** Why, when `refused`. */
178
+ readonly reason?: DeniedReason;
179
+ /** Synq's words for it, in English: show your own for `code` in another language. */
180
+ readonly message: string;
181
+ /** Seconds to wait first, when Synq said. */
182
+ readonly retryAfter?: number;
183
+ }
184
+ /** What only the hosted page can do for this sign-in, and what to take there. */
185
+ interface SignInHandover {
186
+ readonly reason: RedirectReason;
187
+ readonly method?: ChallengeMethod;
188
+ readonly loginHint?: string;
189
+ }
190
+ /** What `useSignIn()` answers: the sign-in's step, what went wrong, and each method's steps. */
191
+ interface UseSignIn {
192
+ readonly step: SignInStep;
193
+ /** Whether a step is on its way to Synq. */
194
+ readonly pending: boolean;
195
+ readonly problem: SignInProblem | null;
196
+ /** The address a code went to. */
197
+ readonly email: string | undefined;
198
+ /** Whether a new account's address may be left unverified for now. */
199
+ readonly canSkip: boolean;
200
+ /** Set when only the hosted page can go on: `continueInBrowser()` goes there. */
201
+ readonly handover: SignInHandover | null;
202
+ readonly password: {
203
+ readonly signIn: (input: {
204
+ email: string;
205
+ password: string;
206
+ }) => Promise<void>;
207
+ readonly signUp: (input: {
208
+ email: string;
209
+ password: string;
210
+ }) => Promise<void>;
211
+ };
212
+ readonly emailCode: {
213
+ readonly send: (email: string) => Promise<void>;
214
+ readonly verify: (code: string) => Promise<void>;
215
+ readonly resend: () => Promise<void>;
216
+ };
217
+ readonly verifyEmail: {
218
+ readonly verify: (code: string) => Promise<void>;
219
+ readonly resend: () => Promise<void>;
220
+ readonly skip: () => Promise<void>;
221
+ };
222
+ /**
223
+ * Goes on at the hosted page in the system browser, with the method and
224
+ * address Synq named, or these: a passkey, a token gate, a step-up. A
225
+ * closed browser is the error `cancelled`.
226
+ */
227
+ readonly continueInBrowser: (options?: {
228
+ method?: string;
229
+ loginHint?: string;
230
+ }) => Promise<SynqResult<Session>>;
231
+ /** Drops the sign-in under way and goes back to the first step. */
232
+ readonly startOver: () => void;
233
+ }
234
+ /**
235
+ * One sign-in on the app's own screens. The provider follows the session
236
+ * it ends with, as after `useAuth().signIn()`.
237
+ */
238
+ export declare function useSignIn(options?: StartSignInOptions): UseSignIn;
239
+ //#endregion
240
+ export type { SecureStoreModule, SignInHandover, SignInProblem, SignInStep, SynqNative, SynqNativeOptions, SynqNativeProviderProps, SynqResult, UseAuth, UseAvatar, UseSession, UseSignIn, UseUser, WebBrowserModule };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { SynqError, accountApiUrl, createAccountClient, createSynqClient, isSynqError } from "@synqauth/js";
2
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
1
+ import { SynqError, accountApiUrl, avatarOf, avatarSvg, createAccountClient, createSynqClient, isSynqError } from "@synqauth/js";
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");
@@ -212,6 +213,29 @@ function useUser() {
212
213
  account
213
214
  };
214
215
  }
216
+ /**
217
+ * What the avatar of `user` is drawn from (AERO-2704): the signed-in person
218
+ * by default, from their ID token, or anyone as Synq names them (`id` and
219
+ * `username` from the management API work too). Show `picture` when its
220
+ * `source` is one the app shows; else the app's own art from `seed`
221
+ * (`defaultPick` over its set); else `defaultPicture`; else `svg`. Null
222
+ * while there is nobody.
223
+ */
224
+ function useAvatar(user, options = {}) {
225
+ const { state } = useSynq();
226
+ const person = user ?? state.session?.claims ?? null;
227
+ return useMemo(() => {
228
+ if (!person) return null;
229
+ const avatar = avatarOf(person);
230
+ return {
231
+ ...avatar,
232
+ svg: avatarSvg(avatar.seed, {
233
+ style: options.style,
234
+ initials: avatar.initials
235
+ })
236
+ };
237
+ }, [person, options.style]);
238
+ }
215
239
  /** Signing in and out. */
216
240
  function useAuth() {
217
241
  const { synq, state } = useSynq();
@@ -249,8 +273,161 @@ function useAuth() {
249
273
  signOut
250
274
  };
251
275
  }
276
+ /** Whatever a call threw, as a `SynqError`. */
252
277
  function asSynqError(error) {
253
278
  return isSynqError(error) ? error : new SynqError("network_error", String(error), { cause: error });
254
279
  }
255
280
  //#endregion
256
- export { CHUNK_SIZE, SynqNativeProvider, createSynqNative, secureStorage, useAuth, useSession, useUser };
281
+ //#region src/sign-in.ts
282
+ /**
283
+ * One sign-in on the app's own screens. The provider follows the session
284
+ * it ends with, as after `useAuth().signIn()`.
285
+ */
286
+ function useSignIn(options = {}) {
287
+ const { synq } = useSynq();
288
+ const auth = useAuth();
289
+ const flow = useRef(null);
290
+ const [step, setStep] = useState("start");
291
+ const [pending, setPending] = useState(0);
292
+ const [problem, setProblem] = useState(null);
293
+ const [email, setEmail] = useState(void 0);
294
+ const [canSkip, setCanSkip] = useState(false);
295
+ const [handover, setHandover] = useState(null);
296
+ const start = useRef(options);
297
+ useEffect(() => {
298
+ start.current = options;
299
+ });
300
+ const actions = useMemo(() => {
301
+ function show(result) {
302
+ switch (result.status) {
303
+ case "signed-in":
304
+ flow.current = null;
305
+ setStep("done");
306
+ setProblem(null);
307
+ return;
308
+ case "code-sent":
309
+ setStep("code");
310
+ setEmail(result.email);
311
+ setProblem(null);
312
+ return;
313
+ case "verify-email":
314
+ setStep("verify-email");
315
+ setEmail(result.email);
316
+ setCanSkip(result.canSkip);
317
+ setProblem(null);
318
+ return;
319
+ case "retry":
320
+ setProblem({
321
+ code: result.problem,
322
+ message: result.message,
323
+ ...result.retryAfter === void 0 ? {} : { retryAfter: result.retryAfter }
324
+ });
325
+ return;
326
+ case "hosted":
327
+ setHandover({
328
+ reason: result.reason,
329
+ ...result.method ? { method: result.method } : {},
330
+ ...result.loginHint ? { loginHint: result.loginHint } : {}
331
+ });
332
+ return;
333
+ case "passkey":
334
+ setHandover({
335
+ reason: "passkey_unavailable",
336
+ method: "passkey"
337
+ });
338
+ return;
339
+ case "refused":
340
+ flow.current = null;
341
+ setStep("start");
342
+ setProblem({
343
+ code: "refused",
344
+ reason: result.reason,
345
+ message: result.message
346
+ });
347
+ return;
348
+ }
349
+ }
350
+ async function run(ask) {
351
+ flow.current ??= synq.client.startSignIn(start.current);
352
+ const current = flow.current;
353
+ setPending((n) => n + 1);
354
+ try {
355
+ show(await ask(current));
356
+ } catch (thrown) {
357
+ const error = asSynqError(thrown);
358
+ if (error.code === "invalid_session") {
359
+ flow.current = null;
360
+ setStep("start");
361
+ setProblem({
362
+ code: "expired",
363
+ message: error.message
364
+ });
365
+ } else if (error.code === "too_many_requests") setProblem({
366
+ code: "limited",
367
+ message: error.message,
368
+ ...error.retryAfter === void 0 ? {} : { retryAfter: error.retryAfter }
369
+ });
370
+ else setProblem({
371
+ code: "unavailable",
372
+ message: error.message
373
+ });
374
+ } finally {
375
+ setPending((n) => n - 1);
376
+ }
377
+ }
378
+ return {
379
+ password: {
380
+ signIn: (input) => run((f) => f.password.signIn(input)),
381
+ signUp: (input) => run((f) => f.password.signUp(input))
382
+ },
383
+ emailCode: {
384
+ send: (address) => run((f) => f.email.send(address)),
385
+ verify: (code) => run((f) => f.email.verify(code)),
386
+ resend: () => run((f) => f.email.resend())
387
+ },
388
+ verifyEmail: {
389
+ verify: (code) => run((f) => f.verifyEmail.verify(code)),
390
+ resend: () => run((f) => f.verifyEmail.resend()),
391
+ skip: () => run((f) => f.verifyEmail.skip())
392
+ },
393
+ startOver: () => {
394
+ flow.current = null;
395
+ setStep("start");
396
+ setProblem(null);
397
+ setEmail(void 0);
398
+ setCanSkip(false);
399
+ setHandover(null);
400
+ }
401
+ };
402
+ }, [synq]);
403
+ const continueInBrowser = useCallback(async (overrides = {}) => {
404
+ const method = overrides.method ?? handover?.method;
405
+ const loginHint = overrides.loginHint ?? handover?.loginHint ?? email;
406
+ const result = await auth.signIn({
407
+ ...method ? { method } : {},
408
+ ...loginHint ? { loginHint } : {}
409
+ });
410
+ if (!result.error) {
411
+ flow.current = null;
412
+ setHandover(null);
413
+ setStep("done");
414
+ }
415
+ return result;
416
+ }, [
417
+ auth,
418
+ handover,
419
+ email
420
+ ]);
421
+ return {
422
+ step,
423
+ pending: pending > 0,
424
+ problem,
425
+ email,
426
+ canSkip,
427
+ handover,
428
+ ...actions,
429
+ continueInBrowser
430
+ };
431
+ }
432
+ //#endregion
433
+ export { CHUNK_SIZE, SynqNativeProvider, createSynqNative, secureStorage, useAuth, useAvatar, 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.3.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.3.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=19.0.0"