@sendoracloud/sdk-react-native 1.23.1 → 1.25.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 +110 -3
- package/dist/index.d.cts +87 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +108 -3
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,8 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AlreadyIdentifiedError: () => AlreadyIdentifiedError,
|
|
33
34
|
Auth: () => Auth,
|
|
34
35
|
AuthError: () => AuthError,
|
|
36
|
+
CredentialInUseError: () => CredentialInUseError,
|
|
35
37
|
EmailAlreadyTakenError: () => EmailAlreadyTakenError,
|
|
36
38
|
LinkError: () => LinkError,
|
|
37
39
|
Links: () => Links,
|
|
@@ -200,7 +202,7 @@ var Storage = class {
|
|
|
200
202
|
// package.json
|
|
201
203
|
var package_default = {
|
|
202
204
|
name: "@sendoracloud/sdk-react-native",
|
|
203
|
-
version: "1.
|
|
205
|
+
version: "1.25.0",
|
|
204
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`).",
|
|
205
207
|
type: "module",
|
|
206
208
|
main: "./dist/index.cjs",
|
|
@@ -256,7 +258,7 @@ var package_default = {
|
|
|
256
258
|
scripts: {
|
|
257
259
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
258
260
|
typecheck: "tsc --noEmit",
|
|
259
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.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"
|
|
260
262
|
},
|
|
261
263
|
devDependencies: {
|
|
262
264
|
"@types/react": "^19.2.16",
|
|
@@ -416,6 +418,8 @@ function isAuthApiResponse(v) {
|
|
|
416
418
|
if (user.email !== void 0 && user.email !== null && typeof user.email !== "string") return false;
|
|
417
419
|
if (user.emailVerified !== void 0 && typeof user.emailVerified !== "boolean") return false;
|
|
418
420
|
if (user.name !== void 0 && user.name !== null && typeof user.name !== "string") return false;
|
|
421
|
+
if (user.signupMethod !== void 0 && user.signupMethod !== null && typeof user.signupMethod !== "string") return false;
|
|
422
|
+
if (user.lastLoginMethod !== void 0 && user.lastLoginMethod !== null && typeof user.lastLoginMethod !== "string") return false;
|
|
419
423
|
return true;
|
|
420
424
|
}
|
|
421
425
|
var EmailAlreadyTakenError = class extends Error {
|
|
@@ -424,6 +428,18 @@ var EmailAlreadyTakenError = class extends Error {
|
|
|
424
428
|
this.name = "EmailAlreadyTakenError";
|
|
425
429
|
}
|
|
426
430
|
};
|
|
431
|
+
var AlreadyIdentifiedError = class extends Error {
|
|
432
|
+
constructor(message = "Already signed in with an identity; use link*() to add a credential, or sign out first") {
|
|
433
|
+
super(message);
|
|
434
|
+
this.name = "AlreadyIdentifiedError";
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
var CredentialInUseError = class extends Error {
|
|
438
|
+
constructor(message = "This credential is already linked to a different account") {
|
|
439
|
+
super(message);
|
|
440
|
+
this.name = "CredentialInUseError";
|
|
441
|
+
}
|
|
442
|
+
};
|
|
427
443
|
var AuthError = class extends Error {
|
|
428
444
|
constructor(code, message) {
|
|
429
445
|
super(message);
|
|
@@ -694,10 +710,13 @@ var Auth = class {
|
|
|
694
710
|
this.persist(data);
|
|
695
711
|
return data.user;
|
|
696
712
|
} catch (err) {
|
|
713
|
+
if (err instanceof AuthError && (err.code === "NOT_ANONYMOUS" || err.code === "FORBIDDEN_NON_ANONYMOUS")) {
|
|
714
|
+
throw new AlreadyIdentifiedError(err.message);
|
|
715
|
+
}
|
|
697
716
|
if (err instanceof AuthError && (err.code === "CONFLICT" || err.code === "EMAIL_ALREADY_TAKEN")) {
|
|
698
717
|
throw new EmailAlreadyTakenError(err.message);
|
|
699
718
|
}
|
|
700
|
-
if (err instanceof AuthError && (err.code === "
|
|
719
|
+
if (err instanceof AuthError && (err.code === "INVALID_REFRESH" || err.code === "UNAUTHORIZED" || err.code === "REFRESH_EXPIRED")) {
|
|
701
720
|
await this.wipeLocalIdentity();
|
|
702
721
|
} else {
|
|
703
722
|
throw err;
|
|
@@ -847,6 +866,92 @@ var Auth = class {
|
|
|
847
866
|
link
|
|
848
867
|
);
|
|
849
868
|
}
|
|
869
|
+
// ============================================================
|
|
870
|
+
// IDENTITY LINKING (ADR-030) — attach a SECOND credential to the
|
|
871
|
+
// CURRENT signed-in account, preserving the same user id (sub).
|
|
872
|
+
// ============================================================
|
|
873
|
+
//
|
|
874
|
+
// Unlike signUp() / loginSocial({link}) — which only preserve the sub from
|
|
875
|
+
// an ANONYMOUS session — these operate on an ALREADY-IDENTIFIED account.
|
|
876
|
+
// No token rotation (the session is unchanged); they refresh the cached user
|
|
877
|
+
// object in place. Require a signed-in user (throws NOT_SIGNED_IN otherwise).
|
|
878
|
+
//
|
|
879
|
+
// Primary use: ONE account across platforms. A Game Center player on iOS
|
|
880
|
+
// links email/Google; on Android they sign in with that same credential and
|
|
881
|
+
// land on the SAME sub, progress intact. Collision (the credential already
|
|
882
|
+
// belongs to another account) → CredentialInUseError (never merges).
|
|
883
|
+
/** Link email + password to the current account (sub preserved). */
|
|
884
|
+
linkEmailPassword(email, password) {
|
|
885
|
+
return this.linkCredential("/api/v1/auth-service/me/link/email", { email, password });
|
|
886
|
+
}
|
|
887
|
+
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
888
|
+
linkSocial(input) {
|
|
889
|
+
return this.linkCredential("/api/v1/auth-service/me/link/social", input);
|
|
890
|
+
}
|
|
891
|
+
/** Convenience: link a Google identity (native idToken, or web code+redirectUri). */
|
|
892
|
+
linkGoogle(input) {
|
|
893
|
+
return this.linkSocial({ provider: "google", ...input });
|
|
894
|
+
}
|
|
895
|
+
/** Convenience: link an Apple identity (native idToken, or web code+redirectUri). */
|
|
896
|
+
linkApple(input) {
|
|
897
|
+
return this.linkSocial({ provider: "apple", ...input });
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Link an Apple Game Center identity to the current account. No-arg drives the
|
|
901
|
+
* native GameKit fetch (needs `@sendoracloud/sdk-react-native-gaming`); or pass
|
|
902
|
+
* an explicit payload obtained from your own GameKit bridge.
|
|
903
|
+
*/
|
|
904
|
+
async linkGameCenter(payload) {
|
|
905
|
+
const body = payload ?? await fetchGameCenterPayloadNative();
|
|
906
|
+
return this.linkCredential("/api/v1/auth-service/me/link/game-center", { ...body });
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Link a Google Play Games identity to the current account. No-arg drives the
|
|
910
|
+
* native Play Games fetch; or pass an explicit `{ serverAuthCode }`.
|
|
911
|
+
*/
|
|
912
|
+
async linkPlayGames(payload) {
|
|
913
|
+
const body = payload ?? await fetchPlayGamesAuthCodeNative();
|
|
914
|
+
return this.linkCredential("/api/v1/auth-service/me/link/play-games", { ...body });
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Shared link executor. POSTs the verified credential with the caller's
|
|
918
|
+
* end-user Bearer token, then refreshes the cached user object IN PLACE (no
|
|
919
|
+
* token rotation — the sub is unchanged). Deliberately NOT wrapped in
|
|
920
|
+
* serialize(): getAccessToken() serializes internally, so re-entering would
|
|
921
|
+
* deadlock — same non-serialized posture as deleteAccount().
|
|
922
|
+
*/
|
|
923
|
+
async linkCredential(path, body) {
|
|
924
|
+
const token = await this.getAccessToken();
|
|
925
|
+
if (!token) throw new AuthError("NOT_SIGNED_IN", "Sign in before linking a credential");
|
|
926
|
+
const res = await post(
|
|
927
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
928
|
+
path,
|
|
929
|
+
body,
|
|
930
|
+
{ Authorization: `Bearer ${token}` }
|
|
931
|
+
);
|
|
932
|
+
if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
|
|
933
|
+
let parsed;
|
|
934
|
+
try {
|
|
935
|
+
parsed = await res.json();
|
|
936
|
+
} catch {
|
|
937
|
+
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
|
|
938
|
+
}
|
|
939
|
+
if (!res.ok || !parsed.success || !parsed.data) {
|
|
940
|
+
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
941
|
+
if (code === "CREDENTIAL_IN_USE") throw new CredentialInUseError(parsed.error?.message);
|
|
942
|
+
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential");
|
|
943
|
+
}
|
|
944
|
+
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
945
|
+
return parsed.data.user ?? this.user;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Refresh the cached user after a link — the sub is unchanged, so only the
|
|
949
|
+
* user object + its storage copy are updated (no token/identity rotation).
|
|
950
|
+
*/
|
|
951
|
+
updateLocalUser(user) {
|
|
952
|
+
this.user = user;
|
|
953
|
+
this.hooks.storage.set(USER_KEY, JSON.stringify(user));
|
|
954
|
+
}
|
|
850
955
|
/**
|
|
851
956
|
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
852
957
|
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
@@ -3212,8 +3317,10 @@ var SendoraCloud = new SendoraSDK();
|
|
|
3212
3317
|
var index_default = SendoraCloud;
|
|
3213
3318
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3214
3319
|
0 && (module.exports = {
|
|
3320
|
+
AlreadyIdentifiedError,
|
|
3215
3321
|
Auth,
|
|
3216
3322
|
AuthError,
|
|
3323
|
+
CredentialInUseError,
|
|
3217
3324
|
EmailAlreadyTakenError,
|
|
3218
3325
|
LinkError,
|
|
3219
3326
|
Links,
|
package/dist/index.d.cts
CHANGED
|
@@ -254,6 +254,18 @@ interface AuthUser {
|
|
|
254
254
|
emailVerified: boolean;
|
|
255
255
|
name: string | null;
|
|
256
256
|
isAnonymous: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* How this account was FIRST created (`signupMethod`, immutable) and how it
|
|
259
|
+
* MOST RECENTLY authenticated (`lastLoginMethod`). Free-form provider tokens:
|
|
260
|
+
* `password | anonymous | google | apple | github | microsoft | linkedin |
|
|
261
|
+
* facebook | discord | gamecenter | playgames | magic_link | email_otp |
|
|
262
|
+
* passkey | oidc | saml | scim | import`. Read-only, display-only (routing /
|
|
263
|
+
* UI) — never an authorization signal. Optional: `undefined` against a backend
|
|
264
|
+
* older than s58.266, and `null` for an end-user row created before that
|
|
265
|
+
* backend shipped (backfilled on their next sign-in). SDK 1.24.0+.
|
|
266
|
+
*/
|
|
267
|
+
signupMethod?: string | null;
|
|
268
|
+
lastLoginMethod?: string | null;
|
|
257
269
|
}
|
|
258
270
|
interface AuthTokens {
|
|
259
271
|
accessToken: string;
|
|
@@ -280,6 +292,24 @@ interface GameCenterPayload {
|
|
|
280
292
|
declare class EmailAlreadyTakenError extends Error {
|
|
281
293
|
constructor(message?: string);
|
|
282
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* `signUp()` was called on a session that is ALREADY signed in with an
|
|
297
|
+
* identity (not anonymous). Creating a second account would orphan the
|
|
298
|
+
* current one — use `linkEmailPassword()` / `linkGoogle()` / … (ADR-030) to
|
|
299
|
+
* add the credential to THIS account, or sign out first. Distinct from
|
|
300
|
+
* `EmailAlreadyTakenError`: the email isn't the problem, the live session is.
|
|
301
|
+
*/
|
|
302
|
+
declare class AlreadyIdentifiedError extends Error {
|
|
303
|
+
constructor(message?: string);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A credential you tried to LINK (ADR-030) is already attached to a DIFFERENT
|
|
307
|
+
* account. Sendora never auto-merges two real accounts — sign in to that
|
|
308
|
+
* account instead, or link a different credential.
|
|
309
|
+
*/
|
|
310
|
+
declare class CredentialInUseError extends Error {
|
|
311
|
+
constructor(message?: string);
|
|
312
|
+
}
|
|
283
313
|
declare class AuthError extends Error {
|
|
284
314
|
code: string;
|
|
285
315
|
constructor(code: string, message: string);
|
|
@@ -564,6 +594,62 @@ declare class Auth {
|
|
|
564
594
|
}, opts?: {
|
|
565
595
|
link?: boolean;
|
|
566
596
|
}): Promise<AuthUser>;
|
|
597
|
+
/** Link email + password to the current account (sub preserved). */
|
|
598
|
+
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
599
|
+
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
600
|
+
linkSocial(input: {
|
|
601
|
+
provider: "google" | "github" | "apple" | "microsoft" | "linkedin" | "facebook" | "twitter" | "discord";
|
|
602
|
+
code?: string;
|
|
603
|
+
idToken?: string;
|
|
604
|
+
redirectUri?: string;
|
|
605
|
+
codeVerifier?: string;
|
|
606
|
+
appleName?: {
|
|
607
|
+
firstName?: string;
|
|
608
|
+
lastName?: string;
|
|
609
|
+
};
|
|
610
|
+
}): Promise<AuthUser>;
|
|
611
|
+
/** Convenience: link a Google identity (native idToken, or web code+redirectUri). */
|
|
612
|
+
linkGoogle(input: {
|
|
613
|
+
idToken?: string;
|
|
614
|
+
code?: string;
|
|
615
|
+
redirectUri?: string;
|
|
616
|
+
}): Promise<AuthUser>;
|
|
617
|
+
/** Convenience: link an Apple identity (native idToken, or web code+redirectUri). */
|
|
618
|
+
linkApple(input: {
|
|
619
|
+
idToken?: string;
|
|
620
|
+
code?: string;
|
|
621
|
+
redirectUri?: string;
|
|
622
|
+
appleName?: {
|
|
623
|
+
firstName?: string;
|
|
624
|
+
lastName?: string;
|
|
625
|
+
};
|
|
626
|
+
}): Promise<AuthUser>;
|
|
627
|
+
/**
|
|
628
|
+
* Link an Apple Game Center identity to the current account. No-arg drives the
|
|
629
|
+
* native GameKit fetch (needs `@sendoracloud/sdk-react-native-gaming`); or pass
|
|
630
|
+
* an explicit payload obtained from your own GameKit bridge.
|
|
631
|
+
*/
|
|
632
|
+
linkGameCenter(payload?: GameCenterPayload): Promise<AuthUser>;
|
|
633
|
+
/**
|
|
634
|
+
* Link a Google Play Games identity to the current account. No-arg drives the
|
|
635
|
+
* native Play Games fetch; or pass an explicit `{ serverAuthCode }`.
|
|
636
|
+
*/
|
|
637
|
+
linkPlayGames(payload?: {
|
|
638
|
+
serverAuthCode: string;
|
|
639
|
+
}): Promise<AuthUser>;
|
|
640
|
+
/**
|
|
641
|
+
* Shared link executor. POSTs the verified credential with the caller's
|
|
642
|
+
* end-user Bearer token, then refreshes the cached user object IN PLACE (no
|
|
643
|
+
* token rotation — the sub is unchanged). Deliberately NOT wrapped in
|
|
644
|
+
* serialize(): getAccessToken() serializes internally, so re-entering would
|
|
645
|
+
* deadlock — same non-serialized posture as deleteAccount().
|
|
646
|
+
*/
|
|
647
|
+
private linkCredential;
|
|
648
|
+
/**
|
|
649
|
+
* Refresh the cached user after a link — the sub is unchanged, so only the
|
|
650
|
+
* user object + its storage copy are updated (no token/identity rotation).
|
|
651
|
+
*/
|
|
652
|
+
private updateLocalUser;
|
|
567
653
|
/**
|
|
568
654
|
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
569
655
|
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
@@ -1539,4 +1625,4 @@ declare class SendoraSDK {
|
|
|
1539
1625
|
}
|
|
1540
1626
|
declare const SendoraCloud: SendoraSDK;
|
|
1541
1627
|
|
|
1542
|
-
export { type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
|
1628
|
+
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, CredentialInUseError, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
package/dist/index.d.ts
CHANGED
|
@@ -254,6 +254,18 @@ interface AuthUser {
|
|
|
254
254
|
emailVerified: boolean;
|
|
255
255
|
name: string | null;
|
|
256
256
|
isAnonymous: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* How this account was FIRST created (`signupMethod`, immutable) and how it
|
|
259
|
+
* MOST RECENTLY authenticated (`lastLoginMethod`). Free-form provider tokens:
|
|
260
|
+
* `password | anonymous | google | apple | github | microsoft | linkedin |
|
|
261
|
+
* facebook | discord | gamecenter | playgames | magic_link | email_otp |
|
|
262
|
+
* passkey | oidc | saml | scim | import`. Read-only, display-only (routing /
|
|
263
|
+
* UI) — never an authorization signal. Optional: `undefined` against a backend
|
|
264
|
+
* older than s58.266, and `null` for an end-user row created before that
|
|
265
|
+
* backend shipped (backfilled on their next sign-in). SDK 1.24.0+.
|
|
266
|
+
*/
|
|
267
|
+
signupMethod?: string | null;
|
|
268
|
+
lastLoginMethod?: string | null;
|
|
257
269
|
}
|
|
258
270
|
interface AuthTokens {
|
|
259
271
|
accessToken: string;
|
|
@@ -280,6 +292,24 @@ interface GameCenterPayload {
|
|
|
280
292
|
declare class EmailAlreadyTakenError extends Error {
|
|
281
293
|
constructor(message?: string);
|
|
282
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* `signUp()` was called on a session that is ALREADY signed in with an
|
|
297
|
+
* identity (not anonymous). Creating a second account would orphan the
|
|
298
|
+
* current one — use `linkEmailPassword()` / `linkGoogle()` / … (ADR-030) to
|
|
299
|
+
* add the credential to THIS account, or sign out first. Distinct from
|
|
300
|
+
* `EmailAlreadyTakenError`: the email isn't the problem, the live session is.
|
|
301
|
+
*/
|
|
302
|
+
declare class AlreadyIdentifiedError extends Error {
|
|
303
|
+
constructor(message?: string);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A credential you tried to LINK (ADR-030) is already attached to a DIFFERENT
|
|
307
|
+
* account. Sendora never auto-merges two real accounts — sign in to that
|
|
308
|
+
* account instead, or link a different credential.
|
|
309
|
+
*/
|
|
310
|
+
declare class CredentialInUseError extends Error {
|
|
311
|
+
constructor(message?: string);
|
|
312
|
+
}
|
|
283
313
|
declare class AuthError extends Error {
|
|
284
314
|
code: string;
|
|
285
315
|
constructor(code: string, message: string);
|
|
@@ -564,6 +594,62 @@ declare class Auth {
|
|
|
564
594
|
}, opts?: {
|
|
565
595
|
link?: boolean;
|
|
566
596
|
}): Promise<AuthUser>;
|
|
597
|
+
/** Link email + password to the current account (sub preserved). */
|
|
598
|
+
linkEmailPassword(email: string, password: string): Promise<AuthUser>;
|
|
599
|
+
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
600
|
+
linkSocial(input: {
|
|
601
|
+
provider: "google" | "github" | "apple" | "microsoft" | "linkedin" | "facebook" | "twitter" | "discord";
|
|
602
|
+
code?: string;
|
|
603
|
+
idToken?: string;
|
|
604
|
+
redirectUri?: string;
|
|
605
|
+
codeVerifier?: string;
|
|
606
|
+
appleName?: {
|
|
607
|
+
firstName?: string;
|
|
608
|
+
lastName?: string;
|
|
609
|
+
};
|
|
610
|
+
}): Promise<AuthUser>;
|
|
611
|
+
/** Convenience: link a Google identity (native idToken, or web code+redirectUri). */
|
|
612
|
+
linkGoogle(input: {
|
|
613
|
+
idToken?: string;
|
|
614
|
+
code?: string;
|
|
615
|
+
redirectUri?: string;
|
|
616
|
+
}): Promise<AuthUser>;
|
|
617
|
+
/** Convenience: link an Apple identity (native idToken, or web code+redirectUri). */
|
|
618
|
+
linkApple(input: {
|
|
619
|
+
idToken?: string;
|
|
620
|
+
code?: string;
|
|
621
|
+
redirectUri?: string;
|
|
622
|
+
appleName?: {
|
|
623
|
+
firstName?: string;
|
|
624
|
+
lastName?: string;
|
|
625
|
+
};
|
|
626
|
+
}): Promise<AuthUser>;
|
|
627
|
+
/**
|
|
628
|
+
* Link an Apple Game Center identity to the current account. No-arg drives the
|
|
629
|
+
* native GameKit fetch (needs `@sendoracloud/sdk-react-native-gaming`); or pass
|
|
630
|
+
* an explicit payload obtained from your own GameKit bridge.
|
|
631
|
+
*/
|
|
632
|
+
linkGameCenter(payload?: GameCenterPayload): Promise<AuthUser>;
|
|
633
|
+
/**
|
|
634
|
+
* Link a Google Play Games identity to the current account. No-arg drives the
|
|
635
|
+
* native Play Games fetch; or pass an explicit `{ serverAuthCode }`.
|
|
636
|
+
*/
|
|
637
|
+
linkPlayGames(payload?: {
|
|
638
|
+
serverAuthCode: string;
|
|
639
|
+
}): Promise<AuthUser>;
|
|
640
|
+
/**
|
|
641
|
+
* Shared link executor. POSTs the verified credential with the caller's
|
|
642
|
+
* end-user Bearer token, then refreshes the cached user object IN PLACE (no
|
|
643
|
+
* token rotation — the sub is unchanged). Deliberately NOT wrapped in
|
|
644
|
+
* serialize(): getAccessToken() serializes internally, so re-entering would
|
|
645
|
+
* deadlock — same non-serialized posture as deleteAccount().
|
|
646
|
+
*/
|
|
647
|
+
private linkCredential;
|
|
648
|
+
/**
|
|
649
|
+
* Refresh the cached user after a link — the sub is unchanged, so only the
|
|
650
|
+
* user object + its storage copy are updated (no token/identity rotation).
|
|
651
|
+
*/
|
|
652
|
+
private updateLocalUser;
|
|
567
653
|
/**
|
|
568
654
|
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
569
655
|
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
@@ -1539,4 +1625,4 @@ declare class SendoraSDK {
|
|
|
1539
1625
|
}
|
|
1540
1626
|
declare const SendoraCloud: SendoraSDK;
|
|
1541
1627
|
|
|
1542
|
-
export { type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, Links, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
|
|
1628
|
+
export { AlreadyIdentifiedError, type AttachLinkingApiOptions, Auth, AuthError, type AuthTokens, type AuthUser, CredentialInUseError, EmailAlreadyTakenError, type IdentifyTraits, type LinkCreateInput, type LinkCreateResult, type LinkData, LinkError, type LinkErrorCode, type LinkMatchInput, type LinkOpenedEvent, type LinkOpenedHandler, type LinkStats, 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.25.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"
|
|
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"
|
|
217
217
|
},
|
|
218
218
|
devDependencies: {
|
|
219
219
|
"@types/react": "^19.2.16",
|
|
@@ -373,6 +373,8 @@ function isAuthApiResponse(v) {
|
|
|
373
373
|
if (user.email !== void 0 && user.email !== null && typeof user.email !== "string") return false;
|
|
374
374
|
if (user.emailVerified !== void 0 && typeof user.emailVerified !== "boolean") return false;
|
|
375
375
|
if (user.name !== void 0 && user.name !== null && typeof user.name !== "string") return false;
|
|
376
|
+
if (user.signupMethod !== void 0 && user.signupMethod !== null && typeof user.signupMethod !== "string") return false;
|
|
377
|
+
if (user.lastLoginMethod !== void 0 && user.lastLoginMethod !== null && typeof user.lastLoginMethod !== "string") return false;
|
|
376
378
|
return true;
|
|
377
379
|
}
|
|
378
380
|
var EmailAlreadyTakenError = class extends Error {
|
|
@@ -381,6 +383,18 @@ var EmailAlreadyTakenError = class extends Error {
|
|
|
381
383
|
this.name = "EmailAlreadyTakenError";
|
|
382
384
|
}
|
|
383
385
|
};
|
|
386
|
+
var AlreadyIdentifiedError = class extends Error {
|
|
387
|
+
constructor(message = "Already signed in with an identity; use link*() to add a credential, or sign out first") {
|
|
388
|
+
super(message);
|
|
389
|
+
this.name = "AlreadyIdentifiedError";
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var CredentialInUseError = class extends Error {
|
|
393
|
+
constructor(message = "This credential is already linked to a different account") {
|
|
394
|
+
super(message);
|
|
395
|
+
this.name = "CredentialInUseError";
|
|
396
|
+
}
|
|
397
|
+
};
|
|
384
398
|
var AuthError = class extends Error {
|
|
385
399
|
constructor(code, message) {
|
|
386
400
|
super(message);
|
|
@@ -651,10 +665,13 @@ var Auth = class {
|
|
|
651
665
|
this.persist(data);
|
|
652
666
|
return data.user;
|
|
653
667
|
} catch (err) {
|
|
668
|
+
if (err instanceof AuthError && (err.code === "NOT_ANONYMOUS" || err.code === "FORBIDDEN_NON_ANONYMOUS")) {
|
|
669
|
+
throw new AlreadyIdentifiedError(err.message);
|
|
670
|
+
}
|
|
654
671
|
if (err instanceof AuthError && (err.code === "CONFLICT" || err.code === "EMAIL_ALREADY_TAKEN")) {
|
|
655
672
|
throw new EmailAlreadyTakenError(err.message);
|
|
656
673
|
}
|
|
657
|
-
if (err instanceof AuthError && (err.code === "
|
|
674
|
+
if (err instanceof AuthError && (err.code === "INVALID_REFRESH" || err.code === "UNAUTHORIZED" || err.code === "REFRESH_EXPIRED")) {
|
|
658
675
|
await this.wipeLocalIdentity();
|
|
659
676
|
} else {
|
|
660
677
|
throw err;
|
|
@@ -804,6 +821,92 @@ var Auth = class {
|
|
|
804
821
|
link
|
|
805
822
|
);
|
|
806
823
|
}
|
|
824
|
+
// ============================================================
|
|
825
|
+
// IDENTITY LINKING (ADR-030) — attach a SECOND credential to the
|
|
826
|
+
// CURRENT signed-in account, preserving the same user id (sub).
|
|
827
|
+
// ============================================================
|
|
828
|
+
//
|
|
829
|
+
// Unlike signUp() / loginSocial({link}) — which only preserve the sub from
|
|
830
|
+
// an ANONYMOUS session — these operate on an ALREADY-IDENTIFIED account.
|
|
831
|
+
// No token rotation (the session is unchanged); they refresh the cached user
|
|
832
|
+
// object in place. Require a signed-in user (throws NOT_SIGNED_IN otherwise).
|
|
833
|
+
//
|
|
834
|
+
// Primary use: ONE account across platforms. A Game Center player on iOS
|
|
835
|
+
// links email/Google; on Android they sign in with that same credential and
|
|
836
|
+
// land on the SAME sub, progress intact. Collision (the credential already
|
|
837
|
+
// belongs to another account) → CredentialInUseError (never merges).
|
|
838
|
+
/** Link email + password to the current account (sub preserved). */
|
|
839
|
+
linkEmailPassword(email, password) {
|
|
840
|
+
return this.linkCredential("/api/v1/auth-service/me/link/email", { email, password });
|
|
841
|
+
}
|
|
842
|
+
/** Link an OAuth social identity (verified idToken or code) to the current account. */
|
|
843
|
+
linkSocial(input) {
|
|
844
|
+
return this.linkCredential("/api/v1/auth-service/me/link/social", input);
|
|
845
|
+
}
|
|
846
|
+
/** Convenience: link a Google identity (native idToken, or web code+redirectUri). */
|
|
847
|
+
linkGoogle(input) {
|
|
848
|
+
return this.linkSocial({ provider: "google", ...input });
|
|
849
|
+
}
|
|
850
|
+
/** Convenience: link an Apple identity (native idToken, or web code+redirectUri). */
|
|
851
|
+
linkApple(input) {
|
|
852
|
+
return this.linkSocial({ provider: "apple", ...input });
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Link an Apple Game Center identity to the current account. No-arg drives the
|
|
856
|
+
* native GameKit fetch (needs `@sendoracloud/sdk-react-native-gaming`); or pass
|
|
857
|
+
* an explicit payload obtained from your own GameKit bridge.
|
|
858
|
+
*/
|
|
859
|
+
async linkGameCenter(payload) {
|
|
860
|
+
const body = payload ?? await fetchGameCenterPayloadNative();
|
|
861
|
+
return this.linkCredential("/api/v1/auth-service/me/link/game-center", { ...body });
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Link a Google Play Games identity to the current account. No-arg drives the
|
|
865
|
+
* native Play Games fetch; or pass an explicit `{ serverAuthCode }`.
|
|
866
|
+
*/
|
|
867
|
+
async linkPlayGames(payload) {
|
|
868
|
+
const body = payload ?? await fetchPlayGamesAuthCodeNative();
|
|
869
|
+
return this.linkCredential("/api/v1/auth-service/me/link/play-games", { ...body });
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Shared link executor. POSTs the verified credential with the caller's
|
|
873
|
+
* end-user Bearer token, then refreshes the cached user object IN PLACE (no
|
|
874
|
+
* token rotation — the sub is unchanged). Deliberately NOT wrapped in
|
|
875
|
+
* serialize(): getAccessToken() serializes internally, so re-entering would
|
|
876
|
+
* deadlock — same non-serialized posture as deleteAccount().
|
|
877
|
+
*/
|
|
878
|
+
async linkCredential(path, body) {
|
|
879
|
+
const token = await this.getAccessToken();
|
|
880
|
+
if (!token) throw new AuthError("NOT_SIGNED_IN", "Sign in before linking a credential");
|
|
881
|
+
const res = await post(
|
|
882
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
883
|
+
path,
|
|
884
|
+
body,
|
|
885
|
+
{ Authorization: `Bearer ${token}` }
|
|
886
|
+
);
|
|
887
|
+
if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
|
|
888
|
+
let parsed;
|
|
889
|
+
try {
|
|
890
|
+
parsed = await res.json();
|
|
891
|
+
} catch {
|
|
892
|
+
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
|
|
893
|
+
}
|
|
894
|
+
if (!res.ok || !parsed.success || !parsed.data) {
|
|
895
|
+
const code = parsed.error?.code ?? `HTTP_${res.status}`;
|
|
896
|
+
if (code === "CREDENTIAL_IN_USE") throw new CredentialInUseError(parsed.error?.message);
|
|
897
|
+
throw new AuthError(code, parsed.error?.message ?? "Failed to link credential");
|
|
898
|
+
}
|
|
899
|
+
if (parsed.data.user) this.updateLocalUser(parsed.data.user);
|
|
900
|
+
return parsed.data.user ?? this.user;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Refresh the cached user after a link — the sub is unchanged, so only the
|
|
904
|
+
* user object + its storage copy are updated (no token/identity rotation).
|
|
905
|
+
*/
|
|
906
|
+
updateLocalUser(user) {
|
|
907
|
+
this.user = user;
|
|
908
|
+
this.hooks.storage.set(USER_KEY, JSON.stringify(user));
|
|
909
|
+
}
|
|
807
910
|
/**
|
|
808
911
|
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
809
912
|
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
@@ -3173,8 +3276,10 @@ var SendoraSDK = class {
|
|
|
3173
3276
|
var SendoraCloud = new SendoraSDK();
|
|
3174
3277
|
var index_default = SendoraCloud;
|
|
3175
3278
|
export {
|
|
3279
|
+
AlreadyIdentifiedError,
|
|
3176
3280
|
Auth,
|
|
3177
3281
|
AuthError,
|
|
3282
|
+
CredentialInUseError,
|
|
3178
3283
|
EmailAlreadyTakenError,
|
|
3179
3284
|
LinkError,
|
|
3180
3285
|
Links,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
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",
|
|
@@ -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"
|
|
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"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/react": "^19.2.16",
|