@sendoracloud/sdk-react-native 1.34.0 → 1.35.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 CHANGED
@@ -208,7 +208,7 @@ var Storage = class {
208
208
  // package.json
209
209
  var package_default = {
210
210
  name: "@sendoracloud/sdk-react-native",
211
- version: "1.34.0",
211
+ version: "1.35.0",
212
212
  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`).",
213
213
  type: "module",
214
214
  main: "./dist/index.cjs",
@@ -264,7 +264,7 @@ var package_default = {
264
264
  scripts: {
265
265
  build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
266
266
  typecheck: "tsc --noEmit",
267
- 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 && tsx test/token-expiry.test.ts"
267
+ 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 && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
268
268
  },
269
269
  devDependencies: {
270
270
  "@types/react": "^19.2.16",
@@ -494,6 +494,8 @@ function classifyAuthErrorKind(code, status) {
494
494
  return "account_locked";
495
495
  case "CREDENTIAL_IN_USE":
496
496
  return "credential_in_use";
497
+ case "LAST_CREDENTIAL":
498
+ return "last_credential";
497
499
  case "NOT_ANONYMOUS":
498
500
  case "FORBIDDEN_NON_ANONYMOUS":
499
501
  return "already_identified";
@@ -1333,6 +1335,56 @@ var Auth = class {
1333
1335
  }
1334
1336
  return { identities: parsed.data.identities ?? [], hasPassword: Boolean(parsed.data.hasPassword) };
1335
1337
  }
1338
+ /**
1339
+ * Remove one linked provider from the signed-in account.
1340
+ *
1341
+ * The write half of `listLinkedIdentities()` — what a "Disconnect" button
1342
+ * next to each entry on a Connected Accounts screen calls.
1343
+ *
1344
+ * ⚠ The server REFUSES to remove the last way into the account, with
1345
+ * `AuthError` `code: "LAST_CREDENTIAL"` / `kind: "last_credential"`. An
1346
+ * account with no credentials still exists and still holds the user's data,
1347
+ * but nothing can ever authenticate into it again — so this is a permanent
1348
+ * lockout, not an inconvenience. A password counts as a credential.
1349
+ *
1350
+ * Prefer to prevent the tap rather than explain the refusal: read
1351
+ * `listLinkedIdentities()` and disable Disconnect when
1352
+ * `identities.length + (hasPassword ? 1 : 0) <= 1`. The error is the
1353
+ * backstop, not the UX.
1354
+ *
1355
+ * Throws `NOT_SIGNED_IN` when there is no session, and `NOT_FOUND` when the
1356
+ * provider is not linked to this account. Removing an already-absent
1357
+ * provider is NOT treated as success — a silent no-op would let a buggy
1358
+ * screen report "disconnected" for a credential that is still attached.
1359
+ */
1360
+ async unlink(provider) {
1361
+ const token = await this.getAccessToken();
1362
+ if (!token) throw new AuthError("NOT_SIGNED_IN", "Sign in before unlinking an identity");
1363
+ const res = await del(
1364
+ { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1365
+ `/api/v1/auth-service/me/identities/${encodeURIComponent(provider)}`,
1366
+ { Authorization: `Bearer ${token}` }
1367
+ );
1368
+ if (!res) throw new AuthError("NETWORK_ERROR", "Failed to unlink identity");
1369
+ let parsed;
1370
+ try {
1371
+ parsed = await res.json();
1372
+ } catch {
1373
+ throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
1374
+ }
1375
+ if (!parsed.success || !parsed.data) {
1376
+ throw new AuthError(
1377
+ parsed.error?.code ?? "UNLINK_FAILED",
1378
+ parsed.error?.message ?? "Failed to unlink identity",
1379
+ { status: res.status }
1380
+ );
1381
+ }
1382
+ return {
1383
+ provider: parsed.data.provider ?? provider,
1384
+ removed: Number(parsed.data.removed ?? 0),
1385
+ remaining: Number(parsed.data.remaining ?? 0)
1386
+ };
1387
+ }
1336
1388
  /**
1337
1389
  * Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
1338
1390
  * ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
package/dist/index.d.cts CHANGED
@@ -442,6 +442,19 @@ type LinkedIdentitiesResponse = {
442
442
  identities: LinkedIdentity[];
443
443
  hasPassword: boolean;
444
444
  };
445
+ /**
446
+ * Result of removing one provider from the signed-in account.
447
+ *
448
+ * `removed` can exceed 1: an account may hold more than one row for a provider
449
+ * (distinct provider user ids across projects), and unlinking takes all of them
450
+ * — the floor counts them all as leaving, so reporting only one would be a lie
451
+ * a caller could act on.
452
+ */
453
+ type UnlinkResult = {
454
+ provider: string;
455
+ removed: number;
456
+ remaining: number;
457
+ };
445
458
  /**
446
459
  * Closed set of auth failure categories (1.28.0). Branch on `kind` instead of
447
460
  * string-matching the ~20 raw `code`s the backend and transport can produce.
@@ -451,7 +464,7 @@ type LinkedIdentitiesResponse = {
451
464
  * OTP, a dismissed Game Center sheet and a 429 all leave `getUserSync()` and
452
465
  * the stored refresh token exactly as they were.
453
466
  */
454
- type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "already_identified" | "cancelled" | "config" | "unknown";
467
+ type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "already_identified" | "cancelled" | "config" | "unknown";
455
468
  declare class AuthError extends Error {
456
469
  code: string;
457
470
  /**
@@ -1034,6 +1047,29 @@ declare class Auth {
1034
1047
  * `deleteAccount`). Firebase `user.providerData` / Supabase `user.identities`.
1035
1048
  */
1036
1049
  listLinkedIdentities(): Promise<LinkedIdentitiesResponse>;
1050
+ /**
1051
+ * Remove one linked provider from the signed-in account.
1052
+ *
1053
+ * The write half of `listLinkedIdentities()` — what a "Disconnect" button
1054
+ * next to each entry on a Connected Accounts screen calls.
1055
+ *
1056
+ * ⚠ The server REFUSES to remove the last way into the account, with
1057
+ * `AuthError` `code: "LAST_CREDENTIAL"` / `kind: "last_credential"`. An
1058
+ * account with no credentials still exists and still holds the user's data,
1059
+ * but nothing can ever authenticate into it again — so this is a permanent
1060
+ * lockout, not an inconvenience. A password counts as a credential.
1061
+ *
1062
+ * Prefer to prevent the tap rather than explain the refusal: read
1063
+ * `listLinkedIdentities()` and disable Disconnect when
1064
+ * `identities.length + (hasPassword ? 1 : 0) <= 1`. The error is the
1065
+ * backstop, not the UX.
1066
+ *
1067
+ * Throws `NOT_SIGNED_IN` when there is no session, and `NOT_FOUND` when the
1068
+ * provider is not linked to this account. Removing an already-absent
1069
+ * provider is NOT treated as success — a silent no-op would let a buggy
1070
+ * screen report "disconnected" for a credential that is still attached.
1071
+ */
1072
+ unlink(provider: string): Promise<UnlinkResult>;
1037
1073
  /**
1038
1074
  * Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
1039
1075
  * ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
@@ -2038,4 +2074,4 @@ declare class SendoraSDK {
2038
2074
  }
2039
2075
  declare const SendoraCloud: SendoraSDK;
2040
2076
 
2041
- export { AlreadyIdentifiedError, type AnonRetirementOutcome, 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 };
2077
+ export { AlreadyIdentifiedError, type AnonRetirementOutcome, 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, type UnlinkResult, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
package/dist/index.d.ts CHANGED
@@ -442,6 +442,19 @@ type LinkedIdentitiesResponse = {
442
442
  identities: LinkedIdentity[];
443
443
  hasPassword: boolean;
444
444
  };
445
+ /**
446
+ * Result of removing one provider from the signed-in account.
447
+ *
448
+ * `removed` can exceed 1: an account may hold more than one row for a provider
449
+ * (distinct provider user ids across projects), and unlinking takes all of them
450
+ * — the floor counts them all as leaving, so reporting only one would be a lie
451
+ * a caller could act on.
452
+ */
453
+ type UnlinkResult = {
454
+ provider: string;
455
+ removed: number;
456
+ remaining: number;
457
+ };
445
458
  /**
446
459
  * Closed set of auth failure categories (1.28.0). Branch on `kind` instead of
447
460
  * string-matching the ~20 raw `code`s the backend and transport can produce.
@@ -451,7 +464,7 @@ type LinkedIdentitiesResponse = {
451
464
  * OTP, a dismissed Game Center sheet and a 429 all leave `getUserSync()` and
452
465
  * the stored refresh token exactly as they were.
453
466
  */
454
- type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "already_identified" | "cancelled" | "config" | "unknown";
467
+ type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "already_identified" | "cancelled" | "config" | "unknown";
455
468
  declare class AuthError extends Error {
456
469
  code: string;
457
470
  /**
@@ -1034,6 +1047,29 @@ declare class Auth {
1034
1047
  * `deleteAccount`). Firebase `user.providerData` / Supabase `user.identities`.
1035
1048
  */
1036
1049
  listLinkedIdentities(): Promise<LinkedIdentitiesResponse>;
1050
+ /**
1051
+ * Remove one linked provider from the signed-in account.
1052
+ *
1053
+ * The write half of `listLinkedIdentities()` — what a "Disconnect" button
1054
+ * next to each entry on a Connected Accounts screen calls.
1055
+ *
1056
+ * ⚠ The server REFUSES to remove the last way into the account, with
1057
+ * `AuthError` `code: "LAST_CREDENTIAL"` / `kind: "last_credential"`. An
1058
+ * account with no credentials still exists and still holds the user's data,
1059
+ * but nothing can ever authenticate into it again — so this is a permanent
1060
+ * lockout, not an inconvenience. A password counts as a credential.
1061
+ *
1062
+ * Prefer to prevent the tap rather than explain the refusal: read
1063
+ * `listLinkedIdentities()` and disable Disconnect when
1064
+ * `identities.length + (hasPassword ? 1 : 0) <= 1`. The error is the
1065
+ * backstop, not the UX.
1066
+ *
1067
+ * Throws `NOT_SIGNED_IN` when there is no session, and `NOT_FOUND` when the
1068
+ * provider is not linked to this account. Removing an already-absent
1069
+ * provider is NOT treated as success — a silent no-op would let a buggy
1070
+ * screen report "disconnected" for a credential that is still attached.
1071
+ */
1072
+ unlink(provider: string): Promise<UnlinkResult>;
1037
1073
  /**
1038
1074
  * Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
1039
1075
  * ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
@@ -2038,4 +2074,4 @@ declare class SendoraSDK {
2038
2074
  }
2039
2075
  declare const SendoraCloud: SendoraSDK;
2040
2076
 
2041
- export { AlreadyIdentifiedError, type AnonRetirementOutcome, 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 };
2077
+ export { AlreadyIdentifiedError, type AnonRetirementOutcome, 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, type UnlinkResult, computeDeviceFingerprint, SendoraCloud as default, extractShortcodeFromUrl, getPlayInstallReferrer };
package/dist/index.js CHANGED
@@ -163,7 +163,7 @@ var Storage = class {
163
163
  // package.json
164
164
  var package_default = {
165
165
  name: "@sendoracloud/sdk-react-native",
166
- version: "1.34.0",
166
+ version: "1.35.0",
167
167
  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`).",
168
168
  type: "module",
169
169
  main: "./dist/index.cjs",
@@ -219,7 +219,7 @@ var package_default = {
219
219
  scripts: {
220
220
  build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
221
221
  typecheck: "tsc --noEmit",
222
- 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 && tsx test/token-expiry.test.ts"
222
+ 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 && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
223
223
  },
224
224
  devDependencies: {
225
225
  "@types/react": "^19.2.16",
@@ -449,6 +449,8 @@ function classifyAuthErrorKind(code, status) {
449
449
  return "account_locked";
450
450
  case "CREDENTIAL_IN_USE":
451
451
  return "credential_in_use";
452
+ case "LAST_CREDENTIAL":
453
+ return "last_credential";
452
454
  case "NOT_ANONYMOUS":
453
455
  case "FORBIDDEN_NON_ANONYMOUS":
454
456
  return "already_identified";
@@ -1288,6 +1290,56 @@ var Auth = class {
1288
1290
  }
1289
1291
  return { identities: parsed.data.identities ?? [], hasPassword: Boolean(parsed.data.hasPassword) };
1290
1292
  }
1293
+ /**
1294
+ * Remove one linked provider from the signed-in account.
1295
+ *
1296
+ * The write half of `listLinkedIdentities()` — what a "Disconnect" button
1297
+ * next to each entry on a Connected Accounts screen calls.
1298
+ *
1299
+ * ⚠ The server REFUSES to remove the last way into the account, with
1300
+ * `AuthError` `code: "LAST_CREDENTIAL"` / `kind: "last_credential"`. An
1301
+ * account with no credentials still exists and still holds the user's data,
1302
+ * but nothing can ever authenticate into it again — so this is a permanent
1303
+ * lockout, not an inconvenience. A password counts as a credential.
1304
+ *
1305
+ * Prefer to prevent the tap rather than explain the refusal: read
1306
+ * `listLinkedIdentities()` and disable Disconnect when
1307
+ * `identities.length + (hasPassword ? 1 : 0) <= 1`. The error is the
1308
+ * backstop, not the UX.
1309
+ *
1310
+ * Throws `NOT_SIGNED_IN` when there is no session, and `NOT_FOUND` when the
1311
+ * provider is not linked to this account. Removing an already-absent
1312
+ * provider is NOT treated as success — a silent no-op would let a buggy
1313
+ * screen report "disconnected" for a credential that is still attached.
1314
+ */
1315
+ async unlink(provider) {
1316
+ const token = await this.getAccessToken();
1317
+ if (!token) throw new AuthError("NOT_SIGNED_IN", "Sign in before unlinking an identity");
1318
+ const res = await del(
1319
+ { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1320
+ `/api/v1/auth-service/me/identities/${encodeURIComponent(provider)}`,
1321
+ { Authorization: `Bearer ${token}` }
1322
+ );
1323
+ if (!res) throw new AuthError("NETWORK_ERROR", "Failed to unlink identity");
1324
+ let parsed;
1325
+ try {
1326
+ parsed = await res.json();
1327
+ } catch {
1328
+ throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
1329
+ }
1330
+ if (!parsed.success || !parsed.data) {
1331
+ throw new AuthError(
1332
+ parsed.error?.code ?? "UNLINK_FAILED",
1333
+ parsed.error?.message ?? "Failed to unlink identity",
1334
+ { status: res.status }
1335
+ );
1336
+ }
1337
+ return {
1338
+ provider: parsed.data.provider ?? provider,
1339
+ removed: Number(parsed.data.removed ?? 0),
1340
+ remaining: Number(parsed.data.remaining ?? 0)
1341
+ };
1342
+ }
1291
1343
  /**
1292
1344
  * Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
1293
1345
  * ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.34.0",
3
+ "version": "1.35.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 && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.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 && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/react": "^19.2.16",