@sendoracloud/sdk-react-native 1.29.0 → 1.31.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/dist/index.cjs +76 -15
- package/dist/index.d.cts +90 -17
- package/dist/index.d.ts +90 -17
- package/dist/index.js +76 -15
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -202,7 +202,7 @@ var Storage = class {
|
|
|
202
202
|
// package.json
|
|
203
203
|
var package_default = {
|
|
204
204
|
name: "@sendoracloud/sdk-react-native",
|
|
205
|
-
version: "1.
|
|
205
|
+
version: "1.31.0",
|
|
206
206
|
description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
207
207
|
type: "module",
|
|
208
208
|
main: "./dist/index.cjs",
|
|
@@ -258,7 +258,7 @@ var package_default = {
|
|
|
258
258
|
scripts: {
|
|
259
259
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
260
260
|
typecheck: "tsc --noEmit",
|
|
261
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts"
|
|
261
|
+
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts"
|
|
262
262
|
},
|
|
263
263
|
devDependencies: {
|
|
264
264
|
"@types/react": "^19.2.16",
|
|
@@ -435,11 +435,37 @@ var AlreadyIdentifiedError = class extends Error {
|
|
|
435
435
|
}
|
|
436
436
|
};
|
|
437
437
|
var CredentialInUseError = class extends Error {
|
|
438
|
-
constructor(message = "This credential is already linked to a different account") {
|
|
438
|
+
constructor(message = "This credential is already linked to a different account", opts) {
|
|
439
439
|
super(message);
|
|
440
|
+
/** Always `"CREDENTIAL_IN_USE"`. */
|
|
441
|
+
this.code = "CREDENTIAL_IN_USE";
|
|
442
|
+
/**
|
|
443
|
+
* Always `"credential_in_use"`. Carried so this can be handled in the same
|
|
444
|
+
* `switch (err.kind)` as every other auth failure.
|
|
445
|
+
*
|
|
446
|
+
* NOTE this class deliberately does NOT extend `AuthError` — widening
|
|
447
|
+
* `instanceof AuthError` to include it would silently re-route an upgrading
|
|
448
|
+
* app that checks `AuthError` before this class. The taxonomy fields are
|
|
449
|
+
* duplicated instead.
|
|
450
|
+
*/
|
|
451
|
+
this.kind = "credential_in_use";
|
|
452
|
+
/** Always false — a different account owns it; retrying changes nothing. */
|
|
453
|
+
this.retryable = false;
|
|
440
454
|
this.name = "CredentialInUseError";
|
|
455
|
+
this.provider = opts?.provider;
|
|
456
|
+
this.collision = opts?.collision;
|
|
457
|
+
this.status = opts?.status;
|
|
441
458
|
}
|
|
442
459
|
};
|
|
460
|
+
function readCollisionDetails(parsed) {
|
|
461
|
+
const details = parsed?.error?.details;
|
|
462
|
+
if (!details || typeof details !== "object") return {};
|
|
463
|
+
const d = details;
|
|
464
|
+
return {
|
|
465
|
+
provider: typeof d.provider === "string" ? d.provider : void 0,
|
|
466
|
+
collision: d.collision === "identity" || d.collision === "email" ? d.collision : void 0
|
|
467
|
+
};
|
|
468
|
+
}
|
|
443
469
|
var RETRYABLE_KINDS = /* @__PURE__ */ new Set([
|
|
444
470
|
"network",
|
|
445
471
|
"server",
|
|
@@ -671,9 +697,21 @@ var Auth = class {
|
|
|
671
697
|
* keyed by the anonymous id — your own users table, an analytics
|
|
672
698
|
* cache, an audience filter — without needing a webhook receiver.
|
|
673
699
|
*
|
|
674
|
-
*
|
|
675
|
-
* `
|
|
676
|
-
*
|
|
700
|
+
* Fires on EVERY identified sign-in path — `signIn`, `loginSocial`,
|
|
701
|
+
* `signInWithGameCenter`, `signInWithPlayGames`, `challengeMfa`,
|
|
702
|
+
* `verifyMagicLink`, `verifyEmailOtp`, passkey authentication, and SSO
|
|
703
|
+
* completion (`consumeSsoFragment`) — because it is fired centrally from
|
|
704
|
+
* `persist()` whenever the response carries `retiredAnonUserId`.
|
|
705
|
+
*
|
|
706
|
+
* ⚠ The two gaming paths are the ones that matter most here, and were
|
|
707
|
+
* missing from this list before 1.31.0. When the presented player identity
|
|
708
|
+
* already belongs to another account, the sign-in ADOPTS that account and
|
|
709
|
+
* the anonymous account on this device is retired — its row is DELETED
|
|
710
|
+
* server-side. This listener is the only client-side signal that happened;
|
|
711
|
+
* the call itself resolves successfully. If you would rather it not happen
|
|
712
|
+
* at all, pass `onCredentialInUse: "reject"`.
|
|
713
|
+
*
|
|
714
|
+
* Returns an unsubscribe function.
|
|
677
715
|
*
|
|
678
716
|
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
679
717
|
* + logged in dev mode. Auth state never depends on listener success.
|
|
@@ -971,10 +1009,11 @@ var Auth = class {
|
|
|
971
1009
|
loginSocial(input) {
|
|
972
1010
|
return this.serialize(async () => {
|
|
973
1011
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
974
|
-
const { link, ...socialInput } = input;
|
|
1012
|
+
const { link, onCredentialInUse, ...socialInput } = input;
|
|
975
1013
|
const payload = { ...socialInput };
|
|
976
1014
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
977
1015
|
if (link) payload.linkAnonymous = true;
|
|
1016
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
978
1017
|
const res = await post(
|
|
979
1018
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
980
1019
|
"/api/v1/auth-service/login/social",
|
|
@@ -988,6 +1027,12 @@ var Auth = class {
|
|
|
988
1027
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
989
1028
|
}
|
|
990
1029
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1030
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1031
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1032
|
+
...readCollisionDetails(parsed),
|
|
1033
|
+
status: res.status
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
991
1036
|
throw new AuthError(
|
|
992
1037
|
parsed.error?.code ?? "SOCIAL_LOGIN_FAILED",
|
|
993
1038
|
parsed.error?.message ?? "Social login failed",
|
|
@@ -1023,26 +1068,24 @@ var Auth = class {
|
|
|
1023
1068
|
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
1024
1069
|
const path = "/api/v1/auth-service/login/game-center";
|
|
1025
1070
|
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
1026
|
-
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts
|
|
1071
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts);
|
|
1027
1072
|
}
|
|
1028
|
-
const link = payloadOrOpts?.link;
|
|
1029
1073
|
return this.gameIdentitySignIn(
|
|
1030
1074
|
path,
|
|
1031
1075
|
async () => await fetchGameCenterPayloadNative(),
|
|
1032
|
-
|
|
1076
|
+
payloadOrOpts
|
|
1033
1077
|
);
|
|
1034
1078
|
}
|
|
1035
1079
|
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
1036
1080
|
const path = "/api/v1/auth-service/login/play-games";
|
|
1037
1081
|
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
1038
1082
|
const { serverAuthCode } = payloadOrOpts;
|
|
1039
|
-
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts
|
|
1083
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts);
|
|
1040
1084
|
}
|
|
1041
|
-
const link = payloadOrOpts?.link;
|
|
1042
1085
|
return this.gameIdentitySignIn(
|
|
1043
1086
|
path,
|
|
1044
1087
|
async () => await fetchPlayGamesAuthCodeNative(),
|
|
1045
|
-
|
|
1088
|
+
payloadOrOpts
|
|
1046
1089
|
);
|
|
1047
1090
|
}
|
|
1048
1091
|
// ============================================================
|
|
@@ -1117,12 +1160,21 @@ var Auth = class {
|
|
|
1117
1160
|
}
|
|
1118
1161
|
if (!res.ok || !parsed.success || !parsed.data) {
|
|
1119
1162
|
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
1120
|
-
if (code === "CREDENTIAL_IN_USE")
|
|
1163
|
+
if (code === "CREDENTIAL_IN_USE") {
|
|
1164
|
+
throw new CredentialInUseError(parsed.error?.message, {
|
|
1165
|
+
...readCollisionDetails(parsed),
|
|
1166
|
+
status: res.status
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1121
1169
|
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential", {
|
|
1122
1170
|
status: res.status,
|
|
1123
1171
|
retryAfterSeconds: readRetryAfterSeconds(parsed)
|
|
1124
1172
|
});
|
|
1125
1173
|
}
|
|
1174
|
+
if (parsed.data.upgraded && isAuthApiResponse(parsed.data)) {
|
|
1175
|
+
this.persist(parsed.data);
|
|
1176
|
+
return parsed.data.user;
|
|
1177
|
+
}
|
|
1126
1178
|
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
1127
1179
|
return parsed.data.user ?? this.user;
|
|
1128
1180
|
}
|
|
@@ -1171,13 +1223,16 @@ var Auth = class {
|
|
|
1171
1223
|
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
1172
1224
|
* centrally from `retiredAnonUserId`.
|
|
1173
1225
|
*/
|
|
1174
|
-
gameIdentitySignIn(path, identityBody,
|
|
1226
|
+
gameIdentitySignIn(path, identityBody, opts) {
|
|
1227
|
+
const link = opts?.link;
|
|
1228
|
+
const onCredentialInUse = opts?.onCredentialInUse;
|
|
1175
1229
|
return this.serialize(async () => {
|
|
1176
1230
|
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
1177
1231
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
1178
1232
|
const payload = { ...body };
|
|
1179
1233
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
1180
1234
|
if (link) payload.linkAnonymous = true;
|
|
1235
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
1181
1236
|
const res = await post(
|
|
1182
1237
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
1183
1238
|
path,
|
|
@@ -1191,6 +1246,12 @@ var Auth = class {
|
|
|
1191
1246
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
1192
1247
|
}
|
|
1193
1248
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1249
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1250
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1251
|
+
...readCollisionDetails(parsed),
|
|
1252
|
+
status: res.status
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1194
1255
|
throw new AuthError(
|
|
1195
1256
|
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
1196
1257
|
parsed.error?.message ?? "Game sign-in failed",
|
package/dist/index.d.cts
CHANGED
|
@@ -317,8 +317,70 @@ declare class AlreadyIdentifiedError extends Error {
|
|
|
317
317
|
* account instead, or link a different credential.
|
|
318
318
|
*/
|
|
319
319
|
declare class CredentialInUseError extends Error {
|
|
320
|
-
|
|
320
|
+
/** Always `"CREDENTIAL_IN_USE"`. */
|
|
321
|
+
readonly code = "CREDENTIAL_IN_USE";
|
|
322
|
+
/**
|
|
323
|
+
* Always `"credential_in_use"`. Carried so this can be handled in the same
|
|
324
|
+
* `switch (err.kind)` as every other auth failure.
|
|
325
|
+
*
|
|
326
|
+
* NOTE this class deliberately does NOT extend `AuthError` — widening
|
|
327
|
+
* `instanceof AuthError` to include it would silently re-route an upgrading
|
|
328
|
+
* app that checks `AuthError` before this class. The taxonomy fields are
|
|
329
|
+
* duplicated instead.
|
|
330
|
+
*/
|
|
331
|
+
readonly kind: "credential_in_use";
|
|
332
|
+
/** Always false — a different account owns it; retrying changes nothing. */
|
|
333
|
+
readonly retryable = false;
|
|
334
|
+
/** The provider whose credential collided, when the server named it. */
|
|
335
|
+
readonly provider?: string;
|
|
336
|
+
/**
|
|
337
|
+
* Which collision this was: `"identity"` = that exact provider identity is
|
|
338
|
+
* already linked to another account; `"email"` = the provider's verified
|
|
339
|
+
* email already belongs to one. Absent on older backends.
|
|
340
|
+
*/
|
|
341
|
+
readonly collision?: "identity" | "email";
|
|
342
|
+
readonly status?: number;
|
|
343
|
+
constructor(message?: string, opts?: {
|
|
344
|
+
provider?: string;
|
|
345
|
+
collision?: "identity" | "email";
|
|
346
|
+
status?: number;
|
|
347
|
+
});
|
|
321
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* What a sign-in should do when the credential you present already belongs to
|
|
351
|
+
* an account (1.30.0).
|
|
352
|
+
*
|
|
353
|
+
* - `"adopt"` (default — and what every earlier release did unconditionally):
|
|
354
|
+
* sign in to that account. This is the reinstall-recovery path: a Game Center
|
|
355
|
+
* / Play Games player identity survives a reinstall server-side, so the
|
|
356
|
+
* "collision" is usually the SAME person's earlier account. If this device is
|
|
357
|
+
* anonymous, that anonymous account is retired — **its row is deleted** — and
|
|
358
|
+
* `onDeviceTakeover` fires with its id.
|
|
359
|
+
* - `"reject"`: throw `CredentialInUseError` and change nothing at all. Use it
|
|
360
|
+
* when the local anonymous session holds progress you are not willing to
|
|
361
|
+
* trade. Mirrors Firebase `linkWithCredential`
|
|
362
|
+
* (`auth/credential-already-in-use`) and Supabase `linkIdentity`.
|
|
363
|
+
*
|
|
364
|
+
* `"reject"` blocks the switch; it does not merge the two accounts. To offer
|
|
365
|
+
* "use my other account", catch the error and re-call with `"adopt"`, then
|
|
366
|
+
* migrate your own data from `onDeviceTakeover`'s `retiredAnonUserId`.
|
|
367
|
+
*/
|
|
368
|
+
type CredentialCollisionPolicy = "adopt" | "reject";
|
|
369
|
+
/** Options for the Game Center / Play Games sign-in methods. */
|
|
370
|
+
type GameSignInOptions = {
|
|
371
|
+
/**
|
|
372
|
+
* ADR-025 link-in-place. When this device is anonymous and the player
|
|
373
|
+
* identity is BRAND NEW, the anonymous account is promoted in place and the
|
|
374
|
+
* user id (sub) is preserved. Has no effect on a collision — see
|
|
375
|
+
* `onCredentialInUse` for that case.
|
|
376
|
+
*/
|
|
377
|
+
link?: boolean;
|
|
378
|
+
/**
|
|
379
|
+
* What to do when the player identity already belongs to an account.
|
|
380
|
+
* Default `"adopt"`. See {@link CredentialCollisionPolicy}.
|
|
381
|
+
*/
|
|
382
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
383
|
+
};
|
|
322
384
|
/** One credential linked to an account (ADR-030 read side, s58.270). */
|
|
323
385
|
type LinkedIdentity = {
|
|
324
386
|
provider: string;
|
|
@@ -521,9 +583,21 @@ declare class Auth {
|
|
|
521
583
|
* keyed by the anonymous id — your own users table, an analytics
|
|
522
584
|
* cache, an audience filter — without needing a webhook receiver.
|
|
523
585
|
*
|
|
524
|
-
*
|
|
525
|
-
* `
|
|
526
|
-
*
|
|
586
|
+
* Fires on EVERY identified sign-in path — `signIn`, `loginSocial`,
|
|
587
|
+
* `signInWithGameCenter`, `signInWithPlayGames`, `challengeMfa`,
|
|
588
|
+
* `verifyMagicLink`, `verifyEmailOtp`, passkey authentication, and SSO
|
|
589
|
+
* completion (`consumeSsoFragment`) — because it is fired centrally from
|
|
590
|
+
* `persist()` whenever the response carries `retiredAnonUserId`.
|
|
591
|
+
*
|
|
592
|
+
* ⚠ The two gaming paths are the ones that matter most here, and were
|
|
593
|
+
* missing from this list before 1.31.0. When the presented player identity
|
|
594
|
+
* already belongs to another account, the sign-in ADOPTS that account and
|
|
595
|
+
* the anonymous account on this device is retired — its row is DELETED
|
|
596
|
+
* server-side. This listener is the only client-side signal that happened;
|
|
597
|
+
* the call itself resolves successfully. If you would rather it not happen
|
|
598
|
+
* at all, pass `onCredentialInUse: "reject"`.
|
|
599
|
+
*
|
|
600
|
+
* Returns an unsubscribe function.
|
|
527
601
|
*
|
|
528
602
|
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
529
603
|
* + logged in dev mode. Auth state never depends on listener success.
|
|
@@ -690,6 +764,13 @@ declare class Auth {
|
|
|
690
764
|
* another account (collision → falls back to takeover/merge).
|
|
691
765
|
*/
|
|
692
766
|
link?: boolean;
|
|
767
|
+
/**
|
|
768
|
+
* What to do if this social identity (or its verified email) already
|
|
769
|
+
* belongs to an account. Default `"adopt"` = sign in to it, retiring this
|
|
770
|
+
* device's anonymous account. `"reject"` throws `CredentialInUseError` and
|
|
771
|
+
* leaves everything untouched. See {@link CredentialCollisionPolicy}.
|
|
772
|
+
*/
|
|
773
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
693
774
|
}): Promise<AuthUser>;
|
|
694
775
|
signInWithGoogle(code: string, redirectUri: string, link?: boolean): Promise<AuthUser>;
|
|
695
776
|
signInWithGitHub(code: string, redirectUri: string): Promise<AuthUser>;
|
|
@@ -723,12 +804,8 @@ declare class Auth {
|
|
|
723
804
|
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
724
805
|
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
725
806
|
*/
|
|
726
|
-
signInWithGameCenter(opts?:
|
|
727
|
-
|
|
728
|
-
}): Promise<AuthUser>;
|
|
729
|
-
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
730
|
-
link?: boolean;
|
|
731
|
-
}): Promise<AuthUser>;
|
|
807
|
+
signInWithGameCenter(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
808
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
732
809
|
/**
|
|
733
810
|
* Google Play Games sign-in (email-less, Android-only).
|
|
734
811
|
*
|
|
@@ -744,14 +821,10 @@ declare class Auth {
|
|
|
744
821
|
*
|
|
745
822
|
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
746
823
|
*/
|
|
747
|
-
signInWithPlayGames(opts?:
|
|
748
|
-
link?: boolean;
|
|
749
|
-
}): Promise<AuthUser>;
|
|
824
|
+
signInWithPlayGames(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
750
825
|
signInWithPlayGames(payload: {
|
|
751
826
|
serverAuthCode: string;
|
|
752
|
-
}, opts?:
|
|
753
|
-
link?: boolean;
|
|
754
|
-
}): Promise<AuthUser>;
|
|
827
|
+
}, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
755
828
|
/** Link email + password to the current account (sub preserved). */
|
|
756
829
|
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
757
830
|
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
@@ -1823,4 +1896,4 @@ declare class SendoraSDK {
|
|
|
1823
1896
|
}
|
|
1824
1897
|
declare const SendoraCloud: SendoraSDK;
|
|
1825
1898
|
|
|
1826
|
-
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthErrorKind, type AuthSignedOutReason, type AuthStateChange, type AuthTokens, type AuthUser, CredentialInUseError, type DeletionCancelledEvent, type DeviceTakeoverEvent, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, type LinkedIdentitiesResponse, type LinkedIdentity, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
|
1899
|
+
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthErrorKind, type AuthSignedOutReason, type AuthStateChange, type AuthTokens, type AuthUser, type CredentialCollisionPolicy, CredentialInUseError, type DeletionCancelledEvent, type DeviceTakeoverEvent, EmailAlreadyTakenError, type GameSignInOptions, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, type LinkedIdentitiesResponse, type LinkedIdentity, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
package/dist/index.d.ts
CHANGED
|
@@ -317,8 +317,70 @@ declare class AlreadyIdentifiedError extends Error {
|
|
|
317
317
|
* account instead, or link a different credential.
|
|
318
318
|
*/
|
|
319
319
|
declare class CredentialInUseError extends Error {
|
|
320
|
-
|
|
320
|
+
/** Always `"CREDENTIAL_IN_USE"`. */
|
|
321
|
+
readonly code = "CREDENTIAL_IN_USE";
|
|
322
|
+
/**
|
|
323
|
+
* Always `"credential_in_use"`. Carried so this can be handled in the same
|
|
324
|
+
* `switch (err.kind)` as every other auth failure.
|
|
325
|
+
*
|
|
326
|
+
* NOTE this class deliberately does NOT extend `AuthError` — widening
|
|
327
|
+
* `instanceof AuthError` to include it would silently re-route an upgrading
|
|
328
|
+
* app that checks `AuthError` before this class. The taxonomy fields are
|
|
329
|
+
* duplicated instead.
|
|
330
|
+
*/
|
|
331
|
+
readonly kind: "credential_in_use";
|
|
332
|
+
/** Always false — a different account owns it; retrying changes nothing. */
|
|
333
|
+
readonly retryable = false;
|
|
334
|
+
/** The provider whose credential collided, when the server named it. */
|
|
335
|
+
readonly provider?: string;
|
|
336
|
+
/**
|
|
337
|
+
* Which collision this was: `"identity"` = that exact provider identity is
|
|
338
|
+
* already linked to another account; `"email"` = the provider's verified
|
|
339
|
+
* email already belongs to one. Absent on older backends.
|
|
340
|
+
*/
|
|
341
|
+
readonly collision?: "identity" | "email";
|
|
342
|
+
readonly status?: number;
|
|
343
|
+
constructor(message?: string, opts?: {
|
|
344
|
+
provider?: string;
|
|
345
|
+
collision?: "identity" | "email";
|
|
346
|
+
status?: number;
|
|
347
|
+
});
|
|
321
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* What a sign-in should do when the credential you present already belongs to
|
|
351
|
+
* an account (1.30.0).
|
|
352
|
+
*
|
|
353
|
+
* - `"adopt"` (default — and what every earlier release did unconditionally):
|
|
354
|
+
* sign in to that account. This is the reinstall-recovery path: a Game Center
|
|
355
|
+
* / Play Games player identity survives a reinstall server-side, so the
|
|
356
|
+
* "collision" is usually the SAME person's earlier account. If this device is
|
|
357
|
+
* anonymous, that anonymous account is retired — **its row is deleted** — and
|
|
358
|
+
* `onDeviceTakeover` fires with its id.
|
|
359
|
+
* - `"reject"`: throw `CredentialInUseError` and change nothing at all. Use it
|
|
360
|
+
* when the local anonymous session holds progress you are not willing to
|
|
361
|
+
* trade. Mirrors Firebase `linkWithCredential`
|
|
362
|
+
* (`auth/credential-already-in-use`) and Supabase `linkIdentity`.
|
|
363
|
+
*
|
|
364
|
+
* `"reject"` blocks the switch; it does not merge the two accounts. To offer
|
|
365
|
+
* "use my other account", catch the error and re-call with `"adopt"`, then
|
|
366
|
+
* migrate your own data from `onDeviceTakeover`'s `retiredAnonUserId`.
|
|
367
|
+
*/
|
|
368
|
+
type CredentialCollisionPolicy = "adopt" | "reject";
|
|
369
|
+
/** Options for the Game Center / Play Games sign-in methods. */
|
|
370
|
+
type GameSignInOptions = {
|
|
371
|
+
/**
|
|
372
|
+
* ADR-025 link-in-place. When this device is anonymous and the player
|
|
373
|
+
* identity is BRAND NEW, the anonymous account is promoted in place and the
|
|
374
|
+
* user id (sub) is preserved. Has no effect on a collision — see
|
|
375
|
+
* `onCredentialInUse` for that case.
|
|
376
|
+
*/
|
|
377
|
+
link?: boolean;
|
|
378
|
+
/**
|
|
379
|
+
* What to do when the player identity already belongs to an account.
|
|
380
|
+
* Default `"adopt"`. See {@link CredentialCollisionPolicy}.
|
|
381
|
+
*/
|
|
382
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
383
|
+
};
|
|
322
384
|
/** One credential linked to an account (ADR-030 read side, s58.270). */
|
|
323
385
|
type LinkedIdentity = {
|
|
324
386
|
provider: string;
|
|
@@ -521,9 +583,21 @@ declare class Auth {
|
|
|
521
583
|
* keyed by the anonymous id — your own users table, an analytics
|
|
522
584
|
* cache, an audience filter — without needing a webhook receiver.
|
|
523
585
|
*
|
|
524
|
-
*
|
|
525
|
-
* `
|
|
526
|
-
*
|
|
586
|
+
* Fires on EVERY identified sign-in path — `signIn`, `loginSocial`,
|
|
587
|
+
* `signInWithGameCenter`, `signInWithPlayGames`, `challengeMfa`,
|
|
588
|
+
* `verifyMagicLink`, `verifyEmailOtp`, passkey authentication, and SSO
|
|
589
|
+
* completion (`consumeSsoFragment`) — because it is fired centrally from
|
|
590
|
+
* `persist()` whenever the response carries `retiredAnonUserId`.
|
|
591
|
+
*
|
|
592
|
+
* ⚠ The two gaming paths are the ones that matter most here, and were
|
|
593
|
+
* missing from this list before 1.31.0. When the presented player identity
|
|
594
|
+
* already belongs to another account, the sign-in ADOPTS that account and
|
|
595
|
+
* the anonymous account on this device is retired — its row is DELETED
|
|
596
|
+
* server-side. This listener is the only client-side signal that happened;
|
|
597
|
+
* the call itself resolves successfully. If you would rather it not happen
|
|
598
|
+
* at all, pass `onCredentialInUse: "reject"`.
|
|
599
|
+
*
|
|
600
|
+
* Returns an unsubscribe function.
|
|
527
601
|
*
|
|
528
602
|
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
529
603
|
* + logged in dev mode. Auth state never depends on listener success.
|
|
@@ -690,6 +764,13 @@ declare class Auth {
|
|
|
690
764
|
* another account (collision → falls back to takeover/merge).
|
|
691
765
|
*/
|
|
692
766
|
link?: boolean;
|
|
767
|
+
/**
|
|
768
|
+
* What to do if this social identity (or its verified email) already
|
|
769
|
+
* belongs to an account. Default `"adopt"` = sign in to it, retiring this
|
|
770
|
+
* device's anonymous account. `"reject"` throws `CredentialInUseError` and
|
|
771
|
+
* leaves everything untouched. See {@link CredentialCollisionPolicy}.
|
|
772
|
+
*/
|
|
773
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
693
774
|
}): Promise<AuthUser>;
|
|
694
775
|
signInWithGoogle(code: string, redirectUri: string, link?: boolean): Promise<AuthUser>;
|
|
695
776
|
signInWithGitHub(code: string, redirectUri: string): Promise<AuthUser>;
|
|
@@ -723,12 +804,8 @@ declare class Auth {
|
|
|
723
804
|
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
724
805
|
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
725
806
|
*/
|
|
726
|
-
signInWithGameCenter(opts?:
|
|
727
|
-
|
|
728
|
-
}): Promise<AuthUser>;
|
|
729
|
-
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
730
|
-
link?: boolean;
|
|
731
|
-
}): Promise<AuthUser>;
|
|
807
|
+
signInWithGameCenter(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
808
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
732
809
|
/**
|
|
733
810
|
* Google Play Games sign-in (email-less, Android-only).
|
|
734
811
|
*
|
|
@@ -744,14 +821,10 @@ declare class Auth {
|
|
|
744
821
|
*
|
|
745
822
|
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
746
823
|
*/
|
|
747
|
-
signInWithPlayGames(opts?:
|
|
748
|
-
link?: boolean;
|
|
749
|
-
}): Promise<AuthUser>;
|
|
824
|
+
signInWithPlayGames(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
750
825
|
signInWithPlayGames(payload: {
|
|
751
826
|
serverAuthCode: string;
|
|
752
|
-
}, opts?:
|
|
753
|
-
link?: boolean;
|
|
754
|
-
}): Promise<AuthUser>;
|
|
827
|
+
}, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
755
828
|
/** Link email + password to the current account (sub preserved). */
|
|
756
829
|
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
757
830
|
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
@@ -1823,4 +1896,4 @@ declare class SendoraSDK {
|
|
|
1823
1896
|
}
|
|
1824
1897
|
declare const SendoraCloud: SendoraSDK;
|
|
1825
1898
|
|
|
1826
|
-
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthErrorKind, type AuthSignedOutReason, type AuthStateChange, type AuthTokens, type AuthUser, CredentialInUseError, type DeletionCancelledEvent, type DeviceTakeoverEvent, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, type LinkedIdentitiesResponse, type LinkedIdentity, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
|
1899
|
+
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthErrorKind, type AuthSignedOutReason, type AuthStateChange, type AuthTokens, type AuthUser, type CredentialCollisionPolicy, CredentialInUseError, type DeletionCancelledEvent, type DeviceTakeoverEvent, EmailAlreadyTakenError, type GameSignInOptions, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, type LinkedIdentitiesResponse, type LinkedIdentity, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ var Storage = class {
|
|
|
157
157
|
// package.json
|
|
158
158
|
var package_default = {
|
|
159
159
|
name: "@sendoracloud/sdk-react-native",
|
|
160
|
-
version: "1.
|
|
160
|
+
version: "1.31.0",
|
|
161
161
|
description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
162
162
|
type: "module",
|
|
163
163
|
main: "./dist/index.cjs",
|
|
@@ -213,7 +213,7 @@ var package_default = {
|
|
|
213
213
|
scripts: {
|
|
214
214
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
215
215
|
typecheck: "tsc --noEmit",
|
|
216
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts"
|
|
216
|
+
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts"
|
|
217
217
|
},
|
|
218
218
|
devDependencies: {
|
|
219
219
|
"@types/react": "^19.2.16",
|
|
@@ -390,11 +390,37 @@ var AlreadyIdentifiedError = class extends Error {
|
|
|
390
390
|
}
|
|
391
391
|
};
|
|
392
392
|
var CredentialInUseError = class extends Error {
|
|
393
|
-
constructor(message = "This credential is already linked to a different account") {
|
|
393
|
+
constructor(message = "This credential is already linked to a different account", opts) {
|
|
394
394
|
super(message);
|
|
395
|
+
/** Always `"CREDENTIAL_IN_USE"`. */
|
|
396
|
+
this.code = "CREDENTIAL_IN_USE";
|
|
397
|
+
/**
|
|
398
|
+
* Always `"credential_in_use"`. Carried so this can be handled in the same
|
|
399
|
+
* `switch (err.kind)` as every other auth failure.
|
|
400
|
+
*
|
|
401
|
+
* NOTE this class deliberately does NOT extend `AuthError` — widening
|
|
402
|
+
* `instanceof AuthError` to include it would silently re-route an upgrading
|
|
403
|
+
* app that checks `AuthError` before this class. The taxonomy fields are
|
|
404
|
+
* duplicated instead.
|
|
405
|
+
*/
|
|
406
|
+
this.kind = "credential_in_use";
|
|
407
|
+
/** Always false — a different account owns it; retrying changes nothing. */
|
|
408
|
+
this.retryable = false;
|
|
395
409
|
this.name = "CredentialInUseError";
|
|
410
|
+
this.provider = opts?.provider;
|
|
411
|
+
this.collision = opts?.collision;
|
|
412
|
+
this.status = opts?.status;
|
|
396
413
|
}
|
|
397
414
|
};
|
|
415
|
+
function readCollisionDetails(parsed) {
|
|
416
|
+
const details = parsed?.error?.details;
|
|
417
|
+
if (!details || typeof details !== "object") return {};
|
|
418
|
+
const d = details;
|
|
419
|
+
return {
|
|
420
|
+
provider: typeof d.provider === "string" ? d.provider : void 0,
|
|
421
|
+
collision: d.collision === "identity" || d.collision === "email" ? d.collision : void 0
|
|
422
|
+
};
|
|
423
|
+
}
|
|
398
424
|
var RETRYABLE_KINDS = /* @__PURE__ */ new Set([
|
|
399
425
|
"network",
|
|
400
426
|
"server",
|
|
@@ -626,9 +652,21 @@ var Auth = class {
|
|
|
626
652
|
* keyed by the anonymous id — your own users table, an analytics
|
|
627
653
|
* cache, an audience filter — without needing a webhook receiver.
|
|
628
654
|
*
|
|
629
|
-
*
|
|
630
|
-
* `
|
|
631
|
-
*
|
|
655
|
+
* Fires on EVERY identified sign-in path — `signIn`, `loginSocial`,
|
|
656
|
+
* `signInWithGameCenter`, `signInWithPlayGames`, `challengeMfa`,
|
|
657
|
+
* `verifyMagicLink`, `verifyEmailOtp`, passkey authentication, and SSO
|
|
658
|
+
* completion (`consumeSsoFragment`) — because it is fired centrally from
|
|
659
|
+
* `persist()` whenever the response carries `retiredAnonUserId`.
|
|
660
|
+
*
|
|
661
|
+
* ⚠ The two gaming paths are the ones that matter most here, and were
|
|
662
|
+
* missing from this list before 1.31.0. When the presented player identity
|
|
663
|
+
* already belongs to another account, the sign-in ADOPTS that account and
|
|
664
|
+
* the anonymous account on this device is retired — its row is DELETED
|
|
665
|
+
* server-side. This listener is the only client-side signal that happened;
|
|
666
|
+
* the call itself resolves successfully. If you would rather it not happen
|
|
667
|
+
* at all, pass `onCredentialInUse: "reject"`.
|
|
668
|
+
*
|
|
669
|
+
* Returns an unsubscribe function.
|
|
632
670
|
*
|
|
633
671
|
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
634
672
|
* + logged in dev mode. Auth state never depends on listener success.
|
|
@@ -926,10 +964,11 @@ var Auth = class {
|
|
|
926
964
|
loginSocial(input) {
|
|
927
965
|
return this.serialize(async () => {
|
|
928
966
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
929
|
-
const { link, ...socialInput } = input;
|
|
967
|
+
const { link, onCredentialInUse, ...socialInput } = input;
|
|
930
968
|
const payload = { ...socialInput };
|
|
931
969
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
932
970
|
if (link) payload.linkAnonymous = true;
|
|
971
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
933
972
|
const res = await post(
|
|
934
973
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
935
974
|
"/api/v1/auth-service/login/social",
|
|
@@ -943,6 +982,12 @@ var Auth = class {
|
|
|
943
982
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
944
983
|
}
|
|
945
984
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
985
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
986
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
987
|
+
...readCollisionDetails(parsed),
|
|
988
|
+
status: res.status
|
|
989
|
+
});
|
|
990
|
+
}
|
|
946
991
|
throw new AuthError(
|
|
947
992
|
parsed.error?.code ?? "SOCIAL_LOGIN_FAILED",
|
|
948
993
|
parsed.error?.message ?? "Social login failed",
|
|
@@ -978,26 +1023,24 @@ var Auth = class {
|
|
|
978
1023
|
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
979
1024
|
const path = "/api/v1/auth-service/login/game-center";
|
|
980
1025
|
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
981
|
-
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts
|
|
1026
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts);
|
|
982
1027
|
}
|
|
983
|
-
const link = payloadOrOpts?.link;
|
|
984
1028
|
return this.gameIdentitySignIn(
|
|
985
1029
|
path,
|
|
986
1030
|
async () => await fetchGameCenterPayloadNative(),
|
|
987
|
-
|
|
1031
|
+
payloadOrOpts
|
|
988
1032
|
);
|
|
989
1033
|
}
|
|
990
1034
|
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
991
1035
|
const path = "/api/v1/auth-service/login/play-games";
|
|
992
1036
|
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
993
1037
|
const { serverAuthCode } = payloadOrOpts;
|
|
994
|
-
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts
|
|
1038
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts);
|
|
995
1039
|
}
|
|
996
|
-
const link = payloadOrOpts?.link;
|
|
997
1040
|
return this.gameIdentitySignIn(
|
|
998
1041
|
path,
|
|
999
1042
|
async () => await fetchPlayGamesAuthCodeNative(),
|
|
1000
|
-
|
|
1043
|
+
payloadOrOpts
|
|
1001
1044
|
);
|
|
1002
1045
|
}
|
|
1003
1046
|
// ============================================================
|
|
@@ -1072,12 +1115,21 @@ var Auth = class {
|
|
|
1072
1115
|
}
|
|
1073
1116
|
if (!res.ok || !parsed.success || !parsed.data) {
|
|
1074
1117
|
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
1075
|
-
if (code === "CREDENTIAL_IN_USE")
|
|
1118
|
+
if (code === "CREDENTIAL_IN_USE") {
|
|
1119
|
+
throw new CredentialInUseError(parsed.error?.message, {
|
|
1120
|
+
...readCollisionDetails(parsed),
|
|
1121
|
+
status: res.status
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1076
1124
|
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential", {
|
|
1077
1125
|
status: res.status,
|
|
1078
1126
|
retryAfterSeconds: readRetryAfterSeconds(parsed)
|
|
1079
1127
|
});
|
|
1080
1128
|
}
|
|
1129
|
+
if (parsed.data.upgraded && isAuthApiResponse(parsed.data)) {
|
|
1130
|
+
this.persist(parsed.data);
|
|
1131
|
+
return parsed.data.user;
|
|
1132
|
+
}
|
|
1081
1133
|
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
1082
1134
|
return parsed.data.user ?? this.user;
|
|
1083
1135
|
}
|
|
@@ -1126,13 +1178,16 @@ var Auth = class {
|
|
|
1126
1178
|
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
1127
1179
|
* centrally from `retiredAnonUserId`.
|
|
1128
1180
|
*/
|
|
1129
|
-
gameIdentitySignIn(path, identityBody,
|
|
1181
|
+
gameIdentitySignIn(path, identityBody, opts) {
|
|
1182
|
+
const link = opts?.link;
|
|
1183
|
+
const onCredentialInUse = opts?.onCredentialInUse;
|
|
1130
1184
|
return this.serialize(async () => {
|
|
1131
1185
|
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
1132
1186
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
1133
1187
|
const payload = { ...body };
|
|
1134
1188
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
1135
1189
|
if (link) payload.linkAnonymous = true;
|
|
1190
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
1136
1191
|
const res = await post(
|
|
1137
1192
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
1138
1193
|
path,
|
|
@@ -1146,6 +1201,12 @@ var Auth = class {
|
|
|
1146
1201
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
1147
1202
|
}
|
|
1148
1203
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1204
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1205
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1206
|
+
...readCollisionDetails(parsed),
|
|
1207
|
+
status: res.status
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1149
1210
|
throw new AuthError(
|
|
1150
1211
|
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
1151
1212
|
parsed.error?.message ?? "Game sign-in failed",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Sendora Cloud React Native + Expo SDK
|
|
3
|
+
"version": "1.31.0",
|
|
4
|
+
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
58
58
|
"typecheck": "tsc --noEmit",
|
|
59
|
-
"test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts"
|
|
59
|
+
"test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/react": "^19.2.16",
|