@sendoracloud/sdk-react-native 1.39.1 → 1.41.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/CHANGELOG.md +747 -0
- package/README.md +81 -21
- package/dist/index.cjs +2287 -1010
- package/dist/index.d.cts +1007 -529
- package/dist/index.d.ts +1007 -529
- package/dist/index.js +2280 -1009
- package/package.json +3 -6
package/README.md
CHANGED
|
@@ -192,38 +192,90 @@ Full paste-ready recipes live in [`examples/links-share.tsx`](./examples/links-s
|
|
|
192
192
|
|
|
193
193
|
`SendoraCloud.auth` covers anonymous sign-in, email + password, magic link, email OTP, TOTP MFA, recovery codes, OIDC / SAML SSO, Sign in with Apple, Google, Microsoft, LinkedIn, Facebook, Discord. See [auth quickstart](https://sendoracloud.com/docs/quickstart-react-native#auth).
|
|
194
194
|
|
|
195
|
-
###
|
|
195
|
+
### Auth status — `status()` is the only way to tell `withheld` from `signed_out`
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
```ts
|
|
198
|
+
await SendoraCloud.auth.authStateReady(); // resolves after the disk hydrate; never waits on the network
|
|
199
|
+
switch (SendoraCloud.auth.status()) {
|
|
200
|
+
case "anonymous": /* a guest session */ break;
|
|
201
|
+
case "identified": /* a signed-in account */ break;
|
|
202
|
+
case "signed_out": /* no session */ break;
|
|
203
|
+
case "unknown": /* a refresh token exists but the user is not known yet */ break;
|
|
204
|
+
case "withheld": /* the stored session belongs to another project — see below */ break;
|
|
205
|
+
default: /* ⚠ REQUIRED: a later SDK may add a status (opt-in gated or in a MAJOR) */ break;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`getUserSync()` is `null` and `isReady()` is `true` in `signed_out`, `unknown` AND `withheld` — only
|
|
210
|
+
`status()` tells them apart. Always keep a `default` branch.
|
|
211
|
+
|
|
212
|
+
### Anonymous → identified (1.41.0, ADR-033 "One Slot, Two Verbs")
|
|
198
213
|
|
|
199
|
-
|
|
214
|
+
The SDK creates an anonymous (guest) user on first `ensureSession()` / `signInAnonymously()`. From there
|
|
215
|
+
there are exactly two verbs:
|
|
216
|
+
|
|
217
|
+
**Link — the only upgrade (same `user.id`).** `signUp(email, password)` from a guest attaches the email to
|
|
218
|
+
the SAME row (`/me/link/email`) — the id is preserved, no funnel break. `signInWith*({ link: true })`,
|
|
219
|
+
`loginSocial({ link: true })`, `link*()` and the typed `auth.link(credential)` do the same for a social /
|
|
220
|
+
Game Center / Play Games identity. Over an already-identified session `link:true` **attaches or errors —
|
|
221
|
+
it never switches accounts**:
|
|
200
222
|
|
|
201
223
|
```ts
|
|
202
|
-
const
|
|
203
|
-
|
|
224
|
+
const r = await SendoraCloud.auth.link({ kind: "gameCenter", ...payload });
|
|
225
|
+
switch (r.status) {
|
|
226
|
+
case "done": /* r.upgraded (guest promoted) / r.alreadyLinked (nothing changed) */ break;
|
|
227
|
+
case "credential_in_use": // owned by another account — nothing changed
|
|
228
|
+
// await r.switchAccount() → sign in to that account (bound to THIS credential); or r.keepCurrent()
|
|
229
|
+
break;
|
|
230
|
+
case "recent_auth_required": /* re-authenticate, then retry */ break;
|
|
231
|
+
}
|
|
204
232
|
```
|
|
205
233
|
|
|
206
|
-
|
|
234
|
+
`signUp()` over an identified session throws `AlreadyIdentifiedError` locally; an email already owned by
|
|
235
|
+
another account throws `EmailAlreadyTakenError`.
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
237
|
+
**Sign in — a switch that HOLDS the guest.** `signIn()`, `loginSocial()`, `signInWithGameCenter()` /
|
|
238
|
+
`PlayGames()`, magic link, email OTP, SSO and `auth.signInWith(credential)` never delete, adopt or merge
|
|
239
|
+
the guest any more. The guest's live tokens are **held on the device** (up to 5) and the account is
|
|
240
|
+
installed: `guest_held{userId,count}` then ONE `signed_in{cause:'sign_in'}` (no `signed_out`). The guest's
|
|
241
|
+
server row survives.
|
|
213
242
|
|
|
214
|
-
|
|
215
|
-
const { user } = await SendoraCloud.auth.signIn(email, password);
|
|
243
|
+
### Held guests
|
|
216
244
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
245
|
+
```ts
|
|
246
|
+
const held = SendoraCloud.auth.getHeldGuests(); // newest first: [{ userId, heldAt, ... }]
|
|
247
|
+
await SendoraCloud.auth.restoreHeldGuest(id); // sign back in AS the guest (current account is held if it was a guest)
|
|
248
|
+
await SendoraCloud.auth.discardHeldGuest(id); // delete the guest server-side (real erasure)
|
|
249
|
+
await SendoraCloud.auth.claimHeldGuest(id); // OPTIONAL + destructive: merge the guest INTO the current account
|
|
222
250
|
```
|
|
223
251
|
|
|
224
|
-
|
|
252
|
+
- `id` may be omitted only when exactly one guest is held (else `AMBIGUOUS_HELD_GUEST`).
|
|
253
|
+
- `claimHeldGuest()` reassigns Sendora-side data and deletes the guest row (fires `auth.user_merged`); it
|
|
254
|
+
does not touch YOUR backend's data keyed on the guest id. **Never call it from an app whose backend does
|
|
255
|
+
not act on `auth.user_merged`** — most apps should restore or discard instead.
|
|
256
|
+
- `init({ autoRestoreHeldGuest })` (default `true`): `ensureSession()` in `signed_out` restores the newest
|
|
257
|
+
held guest instead of minting, visibly (`signed_in{cause:'restore'}` + `guest_released{reason:'restored'}`).
|
|
258
|
+
A dead slot is released (`guest_released{reason:'dead'}`) and the next tried, then it mints.
|
|
259
|
+
- `signInAnonymously({ forceNew: true })` from a guest holds it; with 5 held it throws `HeldGuestLimitError`.
|
|
260
|
+
- `deleteAccount()` keeps held guests (never auto-restored afterwards);
|
|
261
|
+
`deleteAccount({ alsoDiscardHeldGuests: true })` deletes them too.
|
|
262
|
+
- `SendoraCloud.reset()` keeps held guests (a guest's refresh token is its only credential);
|
|
263
|
+
`reset({ includeHeldGuests: true })` releases them locally (no server delete).
|
|
264
|
+
- The deprecated `merge(token)` routes a token matching a held guest to `claimHeldGuest()`.
|
|
265
|
+
|
|
266
|
+
### Project guard (`expectedProjectId`, opt-in)
|
|
225
267
|
|
|
226
|
-
|
|
268
|
+
```ts
|
|
269
|
+
await SendoraCloud.init({ apiKey, projectId: PROJECT_ID, expectedProjectId: PROJECT_ID });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A stored, refreshed or restored session whose JWT `project` claim names a different project is
|
|
273
|
+
**`withheld`**: nothing is written to storage, every auth verb rejects
|
|
274
|
+
`AuthError{ kind: 'config', code: 'SESSION_WITHHELD' }`, `getAccessToken()` returns `null`, and
|
|
275
|
+
`auth.getWithheld()` says whose session it was. Exits: relaunch with the matching key, or
|
|
276
|
+
`auth.discardWithheld()` (clears the session only — held guests, the event queue and the analytics id are
|
|
277
|
+
kept). A freshly minted / signed-in session that disagrees is rejected with `PROJECT_MISMATCH` and revoked.
|
|
278
|
+
Subscribe with `SendoraCloud.onIdentityWithheld(cb)` to record the old user id before discarding.
|
|
227
279
|
|
|
228
280
|
## Config
|
|
229
281
|
|
|
@@ -240,12 +292,20 @@ Full reference + edge-case writeup: [/docs/quickstart-react-native#anon-existing
|
|
|
240
292
|
androidPackageName?: string;
|
|
241
293
|
linkDomain?: string; // Custom share host (e.g. "pulse.link"). Defaults to Sendora's.
|
|
242
294
|
debug?: boolean;
|
|
295
|
+
expectedProjectId?: string; // ADR-033 project guard (opt-in) — see "Project guard".
|
|
296
|
+
autoRestoreHeldGuest?: boolean; // Default true — see "Held guests".
|
|
243
297
|
}
|
|
244
298
|
```
|
|
245
299
|
|
|
246
300
|
## Stability
|
|
247
301
|
|
|
248
|
-
This SDK is on the **1.x line**. Patch bumps (1.x.y → 1.x.z) are backwards-compatible; minor bumps may include opt-in feature additions. Breaking changes only on major bumps — always read the
|
|
302
|
+
This SDK is on the **1.x line**. Patch bumps (1.x.y → 1.x.z) are backwards-compatible; minor bumps may include opt-in feature additions. Breaking changes only on major bumps — always read `CHANGELOG.md`, which ships inside the package (`node_modules/@sendoracloud/sdk-react-native/CHANGELOG.md`), before upgrading.
|
|
303
|
+
|
|
304
|
+
### Auth state is a transition stream, not a health signal
|
|
305
|
+
|
|
306
|
+
`auth.onAuthStateChanged()` reports confirmed transitions only — `signed_in` (`cause`: `mint` | `sign_in` | `restore` | `link`), `signed_out` (reason `user` | `session_expired` | `account_deleted` | `revoked` | `evicted` | `deleted`), `user_updated`, `guest_held`, `guest_released`, `session_withheld`, `device_takeover`, `deletion_cancelled`. Each subscriber first receives one `initial{status, user}` (1.41.0); with a live session it is followed by the same `signed_in{user}` replay 1.28–1.40 sent (no `cause`), so existing listeners keep working. New code: read `initial` and treat a cause-less `signed_in` right after it as the replay. A transient failure of the background token refresh (offline, timeout, HTTP 429, 5xx) emits **nothing**: the stored refresh token is kept and the refresh retried with backoff, so no state changed. "No event" means "no confirmed transition", never "the session is fine". Only a server-rejected refresh token produces `signed_out(session_expired)`. For liveness, read `getAccessToken()` — `null` means no usable token right now.
|
|
307
|
+
|
|
308
|
+
`signInAnonymously()` returns the **current** session when one exists, anonymous or identified — it never replaces a signed-in account with a guest. Pass `forceNew: true` to deliberately leave the current session and mint a fresh anonymous one (a guest left this way is held, not lost).
|
|
249
309
|
|
|
250
310
|
## JWT verification (custom backend)
|
|
251
311
|
|