@sendoracloud/sdk-react-native 1.29.0 → 1.30.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 +61 -12
- package/dist/index.d.cts +75 -14
- package/dist/index.d.ts +75 -14
- package/dist/index.js +61 -12
- 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.30.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",
|
|
@@ -971,10 +997,11 @@ var Auth = class {
|
|
|
971
997
|
loginSocial(input) {
|
|
972
998
|
return this.serialize(async () => {
|
|
973
999
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
974
|
-
const { link, ...socialInput } = input;
|
|
1000
|
+
const { link, onCredentialInUse, ...socialInput } = input;
|
|
975
1001
|
const payload = { ...socialInput };
|
|
976
1002
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
977
1003
|
if (link) payload.linkAnonymous = true;
|
|
1004
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
978
1005
|
const res = await post(
|
|
979
1006
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
980
1007
|
"/api/v1/auth-service/login/social",
|
|
@@ -988,6 +1015,12 @@ var Auth = class {
|
|
|
988
1015
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
989
1016
|
}
|
|
990
1017
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1018
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1019
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1020
|
+
...readCollisionDetails(parsed),
|
|
1021
|
+
status: res.status
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
991
1024
|
throw new AuthError(
|
|
992
1025
|
parsed.error?.code ?? "SOCIAL_LOGIN_FAILED",
|
|
993
1026
|
parsed.error?.message ?? "Social login failed",
|
|
@@ -1023,26 +1056,24 @@ var Auth = class {
|
|
|
1023
1056
|
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
1024
1057
|
const path = "/api/v1/auth-service/login/game-center";
|
|
1025
1058
|
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
1026
|
-
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts
|
|
1059
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts);
|
|
1027
1060
|
}
|
|
1028
|
-
const link = payloadOrOpts?.link;
|
|
1029
1061
|
return this.gameIdentitySignIn(
|
|
1030
1062
|
path,
|
|
1031
1063
|
async () => await fetchGameCenterPayloadNative(),
|
|
1032
|
-
|
|
1064
|
+
payloadOrOpts
|
|
1033
1065
|
);
|
|
1034
1066
|
}
|
|
1035
1067
|
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
1036
1068
|
const path = "/api/v1/auth-service/login/play-games";
|
|
1037
1069
|
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
1038
1070
|
const { serverAuthCode } = payloadOrOpts;
|
|
1039
|
-
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts
|
|
1071
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts);
|
|
1040
1072
|
}
|
|
1041
|
-
const link = payloadOrOpts?.link;
|
|
1042
1073
|
return this.gameIdentitySignIn(
|
|
1043
1074
|
path,
|
|
1044
1075
|
async () => await fetchPlayGamesAuthCodeNative(),
|
|
1045
|
-
|
|
1076
|
+
payloadOrOpts
|
|
1046
1077
|
);
|
|
1047
1078
|
}
|
|
1048
1079
|
// ============================================================
|
|
@@ -1117,12 +1148,21 @@ var Auth = class {
|
|
|
1117
1148
|
}
|
|
1118
1149
|
if (!res.ok || !parsed.success || !parsed.data) {
|
|
1119
1150
|
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
1120
|
-
if (code === "CREDENTIAL_IN_USE")
|
|
1151
|
+
if (code === "CREDENTIAL_IN_USE") {
|
|
1152
|
+
throw new CredentialInUseError(parsed.error?.message, {
|
|
1153
|
+
...readCollisionDetails(parsed),
|
|
1154
|
+
status: res.status
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1121
1157
|
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential", {
|
|
1122
1158
|
status: res.status,
|
|
1123
1159
|
retryAfterSeconds: readRetryAfterSeconds(parsed)
|
|
1124
1160
|
});
|
|
1125
1161
|
}
|
|
1162
|
+
if (parsed.data.upgraded && isAuthApiResponse(parsed.data)) {
|
|
1163
|
+
this.persist(parsed.data);
|
|
1164
|
+
return parsed.data.user;
|
|
1165
|
+
}
|
|
1126
1166
|
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
1127
1167
|
return parsed.data.user ?? this.user;
|
|
1128
1168
|
}
|
|
@@ -1171,13 +1211,16 @@ var Auth = class {
|
|
|
1171
1211
|
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
1172
1212
|
* centrally from `retiredAnonUserId`.
|
|
1173
1213
|
*/
|
|
1174
|
-
gameIdentitySignIn(path, identityBody,
|
|
1214
|
+
gameIdentitySignIn(path, identityBody, opts) {
|
|
1215
|
+
const link = opts?.link;
|
|
1216
|
+
const onCredentialInUse = opts?.onCredentialInUse;
|
|
1175
1217
|
return this.serialize(async () => {
|
|
1176
1218
|
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
1177
1219
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
1178
1220
|
const payload = { ...body };
|
|
1179
1221
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
1180
1222
|
if (link) payload.linkAnonymous = true;
|
|
1223
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
1181
1224
|
const res = await post(
|
|
1182
1225
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
1183
1226
|
path,
|
|
@@ -1191,6 +1234,12 @@ var Auth = class {
|
|
|
1191
1234
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
1192
1235
|
}
|
|
1193
1236
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1237
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1238
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1239
|
+
...readCollisionDetails(parsed),
|
|
1240
|
+
status: res.status
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1194
1243
|
throw new AuthError(
|
|
1195
1244
|
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
1196
1245
|
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;
|
|
@@ -690,6 +752,13 @@ declare class Auth {
|
|
|
690
752
|
* another account (collision → falls back to takeover/merge).
|
|
691
753
|
*/
|
|
692
754
|
link?: boolean;
|
|
755
|
+
/**
|
|
756
|
+
* What to do if this social identity (or its verified email) already
|
|
757
|
+
* belongs to an account. Default `"adopt"` = sign in to it, retiring this
|
|
758
|
+
* device's anonymous account. `"reject"` throws `CredentialInUseError` and
|
|
759
|
+
* leaves everything untouched. See {@link CredentialCollisionPolicy}.
|
|
760
|
+
*/
|
|
761
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
693
762
|
}): Promise<AuthUser>;
|
|
694
763
|
signInWithGoogle(code: string, redirectUri: string, link?: boolean): Promise<AuthUser>;
|
|
695
764
|
signInWithGitHub(code: string, redirectUri: string): Promise<AuthUser>;
|
|
@@ -723,12 +792,8 @@ declare class Auth {
|
|
|
723
792
|
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
724
793
|
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
725
794
|
*/
|
|
726
|
-
signInWithGameCenter(opts?:
|
|
727
|
-
|
|
728
|
-
}): Promise<AuthUser>;
|
|
729
|
-
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
730
|
-
link?: boolean;
|
|
731
|
-
}): Promise<AuthUser>;
|
|
795
|
+
signInWithGameCenter(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
796
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
732
797
|
/**
|
|
733
798
|
* Google Play Games sign-in (email-less, Android-only).
|
|
734
799
|
*
|
|
@@ -744,14 +809,10 @@ declare class Auth {
|
|
|
744
809
|
*
|
|
745
810
|
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
746
811
|
*/
|
|
747
|
-
signInWithPlayGames(opts?:
|
|
748
|
-
link?: boolean;
|
|
749
|
-
}): Promise<AuthUser>;
|
|
812
|
+
signInWithPlayGames(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
750
813
|
signInWithPlayGames(payload: {
|
|
751
814
|
serverAuthCode: string;
|
|
752
|
-
}, opts?:
|
|
753
|
-
link?: boolean;
|
|
754
|
-
}): Promise<AuthUser>;
|
|
815
|
+
}, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
755
816
|
/** Link email + password to the current account (sub preserved). */
|
|
756
817
|
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
757
818
|
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
@@ -1823,4 +1884,4 @@ declare class SendoraSDK {
|
|
|
1823
1884
|
}
|
|
1824
1885
|
declare const SendoraCloud: SendoraSDK;
|
|
1825
1886
|
|
|
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 };
|
|
1887
|
+
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;
|
|
@@ -690,6 +752,13 @@ declare class Auth {
|
|
|
690
752
|
* another account (collision → falls back to takeover/merge).
|
|
691
753
|
*/
|
|
692
754
|
link?: boolean;
|
|
755
|
+
/**
|
|
756
|
+
* What to do if this social identity (or its verified email) already
|
|
757
|
+
* belongs to an account. Default `"adopt"` = sign in to it, retiring this
|
|
758
|
+
* device's anonymous account. `"reject"` throws `CredentialInUseError` and
|
|
759
|
+
* leaves everything untouched. See {@link CredentialCollisionPolicy}.
|
|
760
|
+
*/
|
|
761
|
+
onCredentialInUse?: CredentialCollisionPolicy;
|
|
693
762
|
}): Promise<AuthUser>;
|
|
694
763
|
signInWithGoogle(code: string, redirectUri: string, link?: boolean): Promise<AuthUser>;
|
|
695
764
|
signInWithGitHub(code: string, redirectUri: string): Promise<AuthUser>;
|
|
@@ -723,12 +792,8 @@ declare class Auth {
|
|
|
723
792
|
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
724
793
|
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
725
794
|
*/
|
|
726
|
-
signInWithGameCenter(opts?:
|
|
727
|
-
|
|
728
|
-
}): Promise<AuthUser>;
|
|
729
|
-
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
730
|
-
link?: boolean;
|
|
731
|
-
}): Promise<AuthUser>;
|
|
795
|
+
signInWithGameCenter(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
796
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
732
797
|
/**
|
|
733
798
|
* Google Play Games sign-in (email-less, Android-only).
|
|
734
799
|
*
|
|
@@ -744,14 +809,10 @@ declare class Auth {
|
|
|
744
809
|
*
|
|
745
810
|
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
746
811
|
*/
|
|
747
|
-
signInWithPlayGames(opts?:
|
|
748
|
-
link?: boolean;
|
|
749
|
-
}): Promise<AuthUser>;
|
|
812
|
+
signInWithPlayGames(opts?: GameSignInOptions): Promise<AuthUser>;
|
|
750
813
|
signInWithPlayGames(payload: {
|
|
751
814
|
serverAuthCode: string;
|
|
752
|
-
}, opts?:
|
|
753
|
-
link?: boolean;
|
|
754
|
-
}): Promise<AuthUser>;
|
|
815
|
+
}, opts?: GameSignInOptions): Promise<AuthUser>;
|
|
755
816
|
/** Link email + password to the current account (sub preserved). */
|
|
756
817
|
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
757
818
|
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
@@ -1823,4 +1884,4 @@ declare class SendoraSDK {
|
|
|
1823
1884
|
}
|
|
1824
1885
|
declare const SendoraCloud: SendoraSDK;
|
|
1825
1886
|
|
|
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 };
|
|
1887
|
+
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.30.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",
|
|
@@ -926,10 +952,11 @@ var Auth = class {
|
|
|
926
952
|
loginSocial(input) {
|
|
927
953
|
return this.serialize(async () => {
|
|
928
954
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
929
|
-
const { link, ...socialInput } = input;
|
|
955
|
+
const { link, onCredentialInUse, ...socialInput } = input;
|
|
930
956
|
const payload = { ...socialInput };
|
|
931
957
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
932
958
|
if (link) payload.linkAnonymous = true;
|
|
959
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
933
960
|
const res = await post(
|
|
934
961
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
935
962
|
"/api/v1/auth-service/login/social",
|
|
@@ -943,6 +970,12 @@ var Auth = class {
|
|
|
943
970
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
944
971
|
}
|
|
945
972
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
973
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
974
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
975
|
+
...readCollisionDetails(parsed),
|
|
976
|
+
status: res.status
|
|
977
|
+
});
|
|
978
|
+
}
|
|
946
979
|
throw new AuthError(
|
|
947
980
|
parsed.error?.code ?? "SOCIAL_LOGIN_FAILED",
|
|
948
981
|
parsed.error?.message ?? "Social login failed",
|
|
@@ -978,26 +1011,24 @@ var Auth = class {
|
|
|
978
1011
|
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
979
1012
|
const path = "/api/v1/auth-service/login/game-center";
|
|
980
1013
|
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
981
|
-
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts
|
|
1014
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts);
|
|
982
1015
|
}
|
|
983
|
-
const link = payloadOrOpts?.link;
|
|
984
1016
|
return this.gameIdentitySignIn(
|
|
985
1017
|
path,
|
|
986
1018
|
async () => await fetchGameCenterPayloadNative(),
|
|
987
|
-
|
|
1019
|
+
payloadOrOpts
|
|
988
1020
|
);
|
|
989
1021
|
}
|
|
990
1022
|
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
991
1023
|
const path = "/api/v1/auth-service/login/play-games";
|
|
992
1024
|
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
993
1025
|
const { serverAuthCode } = payloadOrOpts;
|
|
994
|
-
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts
|
|
1026
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts);
|
|
995
1027
|
}
|
|
996
|
-
const link = payloadOrOpts?.link;
|
|
997
1028
|
return this.gameIdentitySignIn(
|
|
998
1029
|
path,
|
|
999
1030
|
async () => await fetchPlayGamesAuthCodeNative(),
|
|
1000
|
-
|
|
1031
|
+
payloadOrOpts
|
|
1001
1032
|
);
|
|
1002
1033
|
}
|
|
1003
1034
|
// ============================================================
|
|
@@ -1072,12 +1103,21 @@ var Auth = class {
|
|
|
1072
1103
|
}
|
|
1073
1104
|
if (!res.ok || !parsed.success || !parsed.data) {
|
|
1074
1105
|
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
1075
|
-
if (code === "CREDENTIAL_IN_USE")
|
|
1106
|
+
if (code === "CREDENTIAL_IN_USE") {
|
|
1107
|
+
throw new CredentialInUseError(parsed.error?.message, {
|
|
1108
|
+
...readCollisionDetails(parsed),
|
|
1109
|
+
status: res.status
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1076
1112
|
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential", {
|
|
1077
1113
|
status: res.status,
|
|
1078
1114
|
retryAfterSeconds: readRetryAfterSeconds(parsed)
|
|
1079
1115
|
});
|
|
1080
1116
|
}
|
|
1117
|
+
if (parsed.data.upgraded && isAuthApiResponse(parsed.data)) {
|
|
1118
|
+
this.persist(parsed.data);
|
|
1119
|
+
return parsed.data.user;
|
|
1120
|
+
}
|
|
1081
1121
|
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
1082
1122
|
return parsed.data.user ?? this.user;
|
|
1083
1123
|
}
|
|
@@ -1126,13 +1166,16 @@ var Auth = class {
|
|
|
1126
1166
|
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
1127
1167
|
* centrally from `retiredAnonUserId`.
|
|
1128
1168
|
*/
|
|
1129
|
-
gameIdentitySignIn(path, identityBody,
|
|
1169
|
+
gameIdentitySignIn(path, identityBody, opts) {
|
|
1170
|
+
const link = opts?.link;
|
|
1171
|
+
const onCredentialInUse = opts?.onCredentialInUse;
|
|
1130
1172
|
return this.serialize(async () => {
|
|
1131
1173
|
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
1132
1174
|
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
1133
1175
|
const payload = { ...body };
|
|
1134
1176
|
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
1135
1177
|
if (link) payload.linkAnonymous = true;
|
|
1178
|
+
if (onCredentialInUse) payload.onCredentialInUse = onCredentialInUse;
|
|
1136
1179
|
const res = await post(
|
|
1137
1180
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
1138
1181
|
path,
|
|
@@ -1146,6 +1189,12 @@ var Auth = class {
|
|
|
1146
1189
|
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`, { status: res.status });
|
|
1147
1190
|
}
|
|
1148
1191
|
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
1192
|
+
if (parsed.error?.code === "CREDENTIAL_IN_USE") {
|
|
1193
|
+
throw new CredentialInUseError(parsed.error.message, {
|
|
1194
|
+
...readCollisionDetails(parsed),
|
|
1195
|
+
status: res.status
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1149
1198
|
throw new AuthError(
|
|
1150
1199
|
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
1151
1200
|
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.30.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",
|