@sendoracloud/sdk-react-native 1.20.0 → 1.23.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/app.plugin.js +85 -0
- package/dist/index.cjs +154 -38
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +142 -26
- package/package.json +6 -4
- package/plugin/apply.js +73 -0
package/app.plugin.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Sendora Expo config plugin — auto-wires deep-link associated domains so games
|
|
2
|
+
// (and any RN app) don't hand-edit iOS entitlements / Android manifests.
|
|
3
|
+
//
|
|
4
|
+
// Usage in app.json / app.config.js:
|
|
5
|
+
// { "plugins": [["@sendoracloud/sdk-react-native", { "linkSubdomain": "wordhurdle" }]] }
|
|
6
|
+
// // → applinks:wordhurdle.sendoracloud.app (iOS) + autoVerify App Link (Android)
|
|
7
|
+
// Full host / BYO custom domain (combines with linkSubdomain):
|
|
8
|
+
// [["@sendoracloud/sdk-react-native", { "linkDomains": ["links.wordhurdle.com"] }]]
|
|
9
|
+
//
|
|
10
|
+
// - iOS: adds `applinks:<host>` to com.apple.developer.associated-domains.
|
|
11
|
+
// - Android: adds an `autoVerify` App Links <intent-filter> (https + host) to
|
|
12
|
+
// the main activity.
|
|
13
|
+
//
|
|
14
|
+
// Game Center / Play Games native config is a SEPARATE plugin in the optional
|
|
15
|
+
// companion package `@sendoracloud/sdk-react-native-gaming` (so non-gaming apps
|
|
16
|
+
// don't pull the Play Games dependency).
|
|
17
|
+
//
|
|
18
|
+
// SAFE / backward-compatible: opt-in + build-time only (runs during
|
|
19
|
+
// `expo prebuild`), so it never touches an already-built or already-installed
|
|
20
|
+
// app. Both platforms MERGE + dedupe — an app that already declared its domains
|
|
21
|
+
// (or lists others) keeps them; adopting the plugin re-adds the same entries
|
|
22
|
+
// idempotently. `createRunOncePlugin` guards against double-application.
|
|
23
|
+
//
|
|
24
|
+
// ESM (the package is `type: module`) — requires Expo SDK 50+ (ESM config
|
|
25
|
+
// plugins). The bug-prone transforms live in ./plugin/apply.js (pure,
|
|
26
|
+
// unit-tested); this file just wires them into the Expo mod pipeline.
|
|
27
|
+
import {
|
|
28
|
+
withEntitlementsPlist,
|
|
29
|
+
withAndroidManifest,
|
|
30
|
+
AndroidConfig,
|
|
31
|
+
createRunOncePlugin,
|
|
32
|
+
WarningAggregator,
|
|
33
|
+
} from "@expo/config-plugins";
|
|
34
|
+
import { createRequire } from "node:module";
|
|
35
|
+
import {
|
|
36
|
+
resolveHosts,
|
|
37
|
+
mergeAssociatedDomains,
|
|
38
|
+
findMainActivity,
|
|
39
|
+
addAppLinkIntentFilters,
|
|
40
|
+
} from "./plugin/apply.js";
|
|
41
|
+
|
|
42
|
+
const pkg = createRequire(import.meta.url)("./package.json");
|
|
43
|
+
const ASSOCIATED_DOMAINS_KEY = "com.apple.developer.associated-domains";
|
|
44
|
+
|
|
45
|
+
function withSendoraIosAssociatedDomains(config, hosts) {
|
|
46
|
+
return withEntitlementsPlist(config, (cfg) => {
|
|
47
|
+
cfg.modResults[ASSOCIATED_DOMAINS_KEY] = mergeAssociatedDomains(
|
|
48
|
+
cfg.modResults[ASSOCIATED_DOMAINS_KEY],
|
|
49
|
+
hosts,
|
|
50
|
+
);
|
|
51
|
+
return cfg;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withSendoraAndroidAppLinks(config, hosts) {
|
|
56
|
+
return withAndroidManifest(config, (cfg) => {
|
|
57
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(cfg.modResults);
|
|
58
|
+
const main = findMainActivity(app.activity);
|
|
59
|
+
if (!main) {
|
|
60
|
+
WarningAggregator.addWarningAndroid(
|
|
61
|
+
pkg.name,
|
|
62
|
+
"No main activity found — skipped App Links intent filters.",
|
|
63
|
+
);
|
|
64
|
+
return cfg;
|
|
65
|
+
}
|
|
66
|
+
addAppLinkIntentFilters(main, hosts);
|
|
67
|
+
return cfg;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const withSendora = (config, props) => {
|
|
72
|
+
const hosts = resolveHosts(props);
|
|
73
|
+
if (hosts.length === 0) {
|
|
74
|
+
WarningAggregator.addWarningIOS(
|
|
75
|
+
pkg.name,
|
|
76
|
+
"No linkSubdomain / linkDomains configured — no associated domains added. Pass { linkSubdomain: '<handle>' } to wire deep links. (Game Center / Play Games config lives in @sendoracloud/sdk-react-native-gaming.)",
|
|
77
|
+
);
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
config = withSendoraIosAssociatedDomains(config, hosts);
|
|
81
|
+
config = withSendoraAndroidAppLinks(config, hosts);
|
|
82
|
+
return config;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default createRunOncePlugin(withSendora, pkg.name, pkg.version);
|
package/dist/index.cjs
CHANGED
|
@@ -43,7 +43,7 @@ __export(index_exports, {
|
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(index_exports);
|
|
45
45
|
var import_react_native_get_random_values = require("react-native-get-random-values");
|
|
46
|
-
var
|
|
46
|
+
var import_react_native5 = require("react-native");
|
|
47
47
|
|
|
48
48
|
// src/storage.ts
|
|
49
49
|
var PREFIX = "sendora_";
|
|
@@ -200,8 +200,8 @@ var Storage = class {
|
|
|
200
200
|
// package.json
|
|
201
201
|
var package_default = {
|
|
202
202
|
name: "@sendoracloud/sdk-react-native",
|
|
203
|
-
version: "1.
|
|
204
|
-
description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push
|
|
203
|
+
version: "1.23.0",
|
|
204
|
+
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
205
|
type: "module",
|
|
206
206
|
main: "./dist/index.cjs",
|
|
207
207
|
module: "./dist/index.js",
|
|
@@ -222,7 +222,9 @@ var package_default = {
|
|
|
222
222
|
"dist",
|
|
223
223
|
"examples",
|
|
224
224
|
"README.md",
|
|
225
|
-
"LICENSE"
|
|
225
|
+
"LICENSE",
|
|
226
|
+
"app.plugin.js",
|
|
227
|
+
"plugin"
|
|
226
228
|
],
|
|
227
229
|
keywords: [
|
|
228
230
|
"sendora",
|
|
@@ -254,7 +256,7 @@ var package_default = {
|
|
|
254
256
|
scripts: {
|
|
255
257
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
256
258
|
typecheck: "tsc --noEmit",
|
|
257
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts"
|
|
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"
|
|
258
260
|
},
|
|
259
261
|
devDependencies: {
|
|
260
262
|
"@types/react": "^19.2.16",
|
|
@@ -367,8 +369,34 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
|
|
|
367
369
|
return res;
|
|
368
370
|
}
|
|
369
371
|
|
|
370
|
-
// src/
|
|
372
|
+
// src/native.ts
|
|
371
373
|
var import_react_native = require("react-native");
|
|
374
|
+
function requireOptionalNativeModule(name) {
|
|
375
|
+
try {
|
|
376
|
+
const g = globalThis;
|
|
377
|
+
const m = g?.expo?.modules?.[name];
|
|
378
|
+
if (m && typeof m === "object") return m;
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const g = globalThis;
|
|
383
|
+
const nm = import_react_native.NativeModules;
|
|
384
|
+
const proxy = g?.expo?.modules?.NativeModulesProxy ?? nm?.NativeUnimoduleProxy;
|
|
385
|
+
const consts = proxy?.modulesConstants?.[name];
|
|
386
|
+
if (consts && typeof consts === "object") return consts;
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const nm = import_react_native.NativeModules;
|
|
391
|
+
const m = nm?.[name];
|
|
392
|
+
if (m && typeof m === "object") return m;
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/auth.ts
|
|
399
|
+
var import_react_native2 = require("react-native");
|
|
372
400
|
var TOKEN_KEY = "auth_access_token";
|
|
373
401
|
var TOKEN_EXPIRES_KEY = "auth_access_expires";
|
|
374
402
|
var REFRESH_KEY = "auth_refresh_token";
|
|
@@ -403,6 +431,48 @@ var AuthError = class extends Error {
|
|
|
403
431
|
this.code = code;
|
|
404
432
|
}
|
|
405
433
|
};
|
|
434
|
+
var GAME_AUTH_NATIVE_MODULE = "SendoraGameAuth";
|
|
435
|
+
async function fetchGameCenterPayloadNative() {
|
|
436
|
+
const mod = requireOptionalNativeModule(GAME_AUTH_NATIVE_MODULE);
|
|
437
|
+
const fn = mod?.fetchGameCenterIdentity;
|
|
438
|
+
if (typeof fn !== "function") {
|
|
439
|
+
throw new AuthError(
|
|
440
|
+
"GAME_CENTER_UNAVAILABLE",
|
|
441
|
+
"Game Center native module not found. Install @sendoracloud/sdk-react-native-gaming, add its config plugin ({ gameCenter: true }), and run `npx expo prebuild` (or EAS Build) \u2014 Game Center sign-in needs a Dev Client, not Expo Go. Or pass a payload from your own GameKit module."
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
const raw = await fn.call(mod);
|
|
445
|
+
return coerceGameCenterPayload(raw);
|
|
446
|
+
}
|
|
447
|
+
function coerceGameCenterPayload(raw) {
|
|
448
|
+
const r = raw ?? {};
|
|
449
|
+
const publicKeyUrl = typeof r.publicKeyUrl === "string" ? r.publicKeyUrl : "";
|
|
450
|
+
const signature = typeof r.signature === "string" ? r.signature : "";
|
|
451
|
+
const salt = typeof r.salt === "string" ? r.salt : "";
|
|
452
|
+
const teamPlayerId = typeof r.teamPlayerId === "string" ? r.teamPlayerId : "";
|
|
453
|
+
const bundleId = typeof r.bundleId === "string" ? r.bundleId : "";
|
|
454
|
+
const timestamp = typeof r.timestamp === "number" ? r.timestamp : Number(r.timestamp);
|
|
455
|
+
if (!publicKeyUrl || !signature || !salt || !teamPlayerId || !bundleId || !Number.isFinite(timestamp)) {
|
|
456
|
+
throw new AuthError("GAME_CENTER_INVALID_PAYLOAD", "Game Center returned an incomplete identity payload.");
|
|
457
|
+
}
|
|
458
|
+
return { publicKeyUrl, signature, salt, timestamp, teamPlayerId, bundleId };
|
|
459
|
+
}
|
|
460
|
+
async function fetchPlayGamesAuthCodeNative() {
|
|
461
|
+
const mod = requireOptionalNativeModule(GAME_AUTH_NATIVE_MODULE);
|
|
462
|
+
const fn = mod?.fetchServerAuthCode;
|
|
463
|
+
if (typeof fn !== "function") {
|
|
464
|
+
throw new AuthError(
|
|
465
|
+
"PLAY_GAMES_UNAVAILABLE",
|
|
466
|
+
"Play Games native module not found. Install @sendoracloud/sdk-react-native-gaming, add its config plugin ({ playGamesWebClientId, playGamesProjectId }), and run `npx expo prebuild` (or EAS Build) \u2014 Play Games sign-in needs a Dev Client, not Expo Go. Or pass a serverAuthCode from your own Play Games module."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const raw = await fn.call(mod);
|
|
470
|
+
const serverAuthCode = typeof raw?.serverAuthCode === "string" ? raw.serverAuthCode : "";
|
|
471
|
+
if (!serverAuthCode) {
|
|
472
|
+
throw new AuthError("PLAY_GAMES_INVALID_PAYLOAD", "Play Games returned no server auth code.");
|
|
473
|
+
}
|
|
474
|
+
return { serverAuthCode };
|
|
475
|
+
}
|
|
406
476
|
var Auth = class {
|
|
407
477
|
constructor(hooks) {
|
|
408
478
|
this.user = null;
|
|
@@ -752,6 +822,72 @@ var Auth = class {
|
|
|
752
822
|
signInWithDiscord(code, redirectUri) {
|
|
753
823
|
return this.loginSocial({ provider: "discord", code, redirectUri });
|
|
754
824
|
}
|
|
825
|
+
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
826
|
+
const path = "/api/v1/auth-service/login/game-center";
|
|
827
|
+
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
828
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts?.link);
|
|
829
|
+
}
|
|
830
|
+
const link = payloadOrOpts?.link;
|
|
831
|
+
return this.gameIdentitySignIn(
|
|
832
|
+
path,
|
|
833
|
+
async () => await fetchGameCenterPayloadNative(),
|
|
834
|
+
link
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
838
|
+
const path = "/api/v1/auth-service/login/play-games";
|
|
839
|
+
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
840
|
+
const { serverAuthCode } = payloadOrOpts;
|
|
841
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts?.link);
|
|
842
|
+
}
|
|
843
|
+
const link = payloadOrOpts?.link;
|
|
844
|
+
return this.gameIdentitySignIn(
|
|
845
|
+
path,
|
|
846
|
+
async () => await fetchPlayGamesAuthCodeNative(),
|
|
847
|
+
link
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
852
|
+
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
853
|
+
* identity, forwards it as `prevAnonRefreshToken`, and opts into
|
|
854
|
+
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
855
|
+
* centrally from `retiredAnonUserId`.
|
|
856
|
+
*/
|
|
857
|
+
gameIdentitySignIn(path, identityBody, link) {
|
|
858
|
+
return this.serialize(async () => {
|
|
859
|
+
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
860
|
+
let prevAnonRefreshToken;
|
|
861
|
+
if (this.user?.isAnonymous) {
|
|
862
|
+
const stashed = this.hooks.storage.get(REFRESH_KEY);
|
|
863
|
+
if (stashed) prevAnonRefreshToken = stashed;
|
|
864
|
+
}
|
|
865
|
+
if (this.user !== null) await this.wipeLocalIdentity();
|
|
866
|
+
const payload = { ...body };
|
|
867
|
+
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
868
|
+
if (link) payload.linkAnonymous = true;
|
|
869
|
+
const res = await post(
|
|
870
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
871
|
+
path,
|
|
872
|
+
payload
|
|
873
|
+
);
|
|
874
|
+
if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
|
|
875
|
+
let parsed;
|
|
876
|
+
try {
|
|
877
|
+
parsed = await res.json();
|
|
878
|
+
} catch {
|
|
879
|
+
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
|
|
880
|
+
}
|
|
881
|
+
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
882
|
+
throw new AuthError(
|
|
883
|
+
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
884
|
+
parsed.error?.message ?? "Game sign-in failed"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
this.persist(parsed.data);
|
|
888
|
+
return parsed.data.user;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
755
891
|
/**
|
|
756
892
|
* Read the stored anon refresh token if (and only if) the local
|
|
757
893
|
* subject is currently anonymous. Used by every identified-session
|
|
@@ -1231,7 +1367,7 @@ var Auth = class {
|
|
|
1231
1367
|
void tick();
|
|
1232
1368
|
}, TICK_MS);
|
|
1233
1369
|
try {
|
|
1234
|
-
const subscription =
|
|
1370
|
+
const subscription = import_react_native2.AppState?.addEventListener?.("change", (state) => {
|
|
1235
1371
|
if (state === "active") {
|
|
1236
1372
|
this.resetRefreshState();
|
|
1237
1373
|
void tick();
|
|
@@ -1392,7 +1528,7 @@ function decodeJwtPayload(token) {
|
|
|
1392
1528
|
}
|
|
1393
1529
|
|
|
1394
1530
|
// src/links.ts
|
|
1395
|
-
var
|
|
1531
|
+
var import_react_native3 = require("react-native");
|
|
1396
1532
|
var LinkError = class extends Error {
|
|
1397
1533
|
constructor(code, message, statusCode = 0, details) {
|
|
1398
1534
|
super(message);
|
|
@@ -1471,10 +1607,10 @@ function dynRequire(id) {
|
|
|
1471
1607
|
}
|
|
1472
1608
|
function loadRn() {
|
|
1473
1609
|
return {
|
|
1474
|
-
Platform:
|
|
1475
|
-
Dimensions:
|
|
1476
|
-
NativeModules:
|
|
1477
|
-
Linking:
|
|
1610
|
+
Platform: import_react_native3.Platform,
|
|
1611
|
+
Dimensions: import_react_native3.Dimensions,
|
|
1612
|
+
NativeModules: import_react_native3.NativeModules,
|
|
1613
|
+
Linking: import_react_native3.Linking
|
|
1478
1614
|
};
|
|
1479
1615
|
}
|
|
1480
1616
|
function readTimezone() {
|
|
@@ -1970,7 +2106,7 @@ var Links = class {
|
|
|
1970
2106
|
};
|
|
1971
2107
|
|
|
1972
2108
|
// src/auto-track.ts
|
|
1973
|
-
var
|
|
2109
|
+
var import_react_native4 = require("react-native");
|
|
1974
2110
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1975
2111
|
function resolveFlags(cfg) {
|
|
1976
2112
|
if (cfg === false) {
|
|
@@ -2015,7 +2151,7 @@ function installAutoTrack(args) {
|
|
|
2015
2151
|
args.fire("app.opened", { sessionId });
|
|
2016
2152
|
}
|
|
2017
2153
|
if (flags.appBackground || flags.engagement) {
|
|
2018
|
-
const AppState =
|
|
2154
|
+
const AppState = import_react_native4.AppState;
|
|
2019
2155
|
if (AppState && typeof AppState.addEventListener === "function") {
|
|
2020
2156
|
const handler = (state) => {
|
|
2021
2157
|
ensureSession(true);
|
|
@@ -2114,13 +2250,13 @@ function byteLength(s) {
|
|
|
2114
2250
|
}
|
|
2115
2251
|
function buildDeviceContext(appVersion, appBuild) {
|
|
2116
2252
|
try {
|
|
2117
|
-
const os =
|
|
2253
|
+
const os = import_react_native5.Platform?.OS;
|
|
2118
2254
|
if (os !== "ios" && os !== "android") return void 0;
|
|
2119
|
-
const isPad = os === "ios" &&
|
|
2255
|
+
const isPad = os === "ios" && import_react_native5.Platform.isPad === true;
|
|
2120
2256
|
return {
|
|
2121
2257
|
type: isPad ? "tablet" : "mobile",
|
|
2122
2258
|
os: os === "ios" ? "iOS" : "Android",
|
|
2123
|
-
osVersion: String(
|
|
2259
|
+
osVersion: String(import_react_native5.Platform.Version ?? ""),
|
|
2124
2260
|
model: expoDeviceModel(),
|
|
2125
2261
|
appVersion: resolveAppVersion(appVersion),
|
|
2126
2262
|
appBuild: resolveAppBuild(appBuild)
|
|
@@ -2130,27 +2266,7 @@ function buildDeviceContext(appVersion, appBuild) {
|
|
|
2130
2266
|
}
|
|
2131
2267
|
}
|
|
2132
2268
|
function expoModule(name) {
|
|
2133
|
-
|
|
2134
|
-
const g = globalThis;
|
|
2135
|
-
const m = g?.expo?.modules?.[name];
|
|
2136
|
-
if (m && typeof m === "object") return m;
|
|
2137
|
-
} catch {
|
|
2138
|
-
}
|
|
2139
|
-
try {
|
|
2140
|
-
const g = globalThis;
|
|
2141
|
-
const nm = import_react_native4.NativeModules;
|
|
2142
|
-
const proxy = g?.expo?.modules?.NativeModulesProxy ?? nm?.NativeUnimoduleProxy;
|
|
2143
|
-
const consts = proxy?.modulesConstants?.[name];
|
|
2144
|
-
if (consts && typeof consts === "object") return consts;
|
|
2145
|
-
} catch {
|
|
2146
|
-
}
|
|
2147
|
-
try {
|
|
2148
|
-
const nm = import_react_native4.NativeModules;
|
|
2149
|
-
const m = nm?.[name];
|
|
2150
|
-
if (m && typeof m === "object") return m;
|
|
2151
|
-
} catch {
|
|
2152
|
-
}
|
|
2153
|
-
return void 0;
|
|
2269
|
+
return requireOptionalNativeModule(name);
|
|
2154
2270
|
}
|
|
2155
2271
|
function expoApplicationVersion() {
|
|
2156
2272
|
const a = expoModule("ExpoApplication");
|
package/dist/index.d.cts
CHANGED
|
@@ -261,6 +261,22 @@ interface AuthTokens {
|
|
|
261
261
|
expiresIn: number;
|
|
262
262
|
tokenType: string;
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Apple Game Center identity payload — the exact shape the Sendora backend
|
|
266
|
+
* verifies, produced by `GKLocalPlayer.local.fetchItems(forIdentityVerification-
|
|
267
|
+
* Signature:)`. `signature`/`salt` are base64; `timestamp` is GameKit's
|
|
268
|
+
* ms-since-epoch. The bundled native module returns this from
|
|
269
|
+
* `fetchGameCenterIdentity()`; you can also build it from your own GameKit
|
|
270
|
+
* module and pass it to `signInWithGameCenter(payload)`.
|
|
271
|
+
*/
|
|
272
|
+
interface GameCenterPayload {
|
|
273
|
+
publicKeyUrl: string;
|
|
274
|
+
signature: string;
|
|
275
|
+
salt: string;
|
|
276
|
+
timestamp: number;
|
|
277
|
+
teamPlayerId: string;
|
|
278
|
+
bundleId: string;
|
|
279
|
+
}
|
|
264
280
|
declare class EmailAlreadyTakenError extends Error {
|
|
265
281
|
constructor(message?: string);
|
|
266
282
|
}
|
|
@@ -504,6 +520,58 @@ declare class Auth {
|
|
|
504
520
|
signInWithLinkedIn(code: string, redirectUri: string): Promise<AuthUser>;
|
|
505
521
|
signInWithFacebook(code: string, redirectUri: string): Promise<AuthUser>;
|
|
506
522
|
signInWithDiscord(code: string, redirectUri: string): Promise<AuthUser>;
|
|
523
|
+
/**
|
|
524
|
+
* Apple Game Center sign-in (email-less, iOS-only).
|
|
525
|
+
*
|
|
526
|
+
* **No-arg (1.23.0, recommended):** `signInWithGameCenter()` drives Game
|
|
527
|
+
* Center natively via the `@sendoracloud/sdk-react-native-gaming` companion
|
|
528
|
+
* (`fetchItems(forIdentityVerificationSignature:)`, base64 signature/salt,
|
|
529
|
+
* `teamPlayerID`, bundle id) and posts the result. Needs that companion + its
|
|
530
|
+
* config plugin `{ gameCenter: true }` + a Dev Client — NOT Expo Go.
|
|
531
|
+
*
|
|
532
|
+
* **Explicit:** `signInWithGameCenter(payload)` posts a payload you obtained
|
|
533
|
+
* from your OWN GameKit module (works anywhere, incl. bare RN).
|
|
534
|
+
*
|
|
535
|
+
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
536
|
+
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
537
|
+
*/
|
|
538
|
+
signInWithGameCenter(opts?: {
|
|
539
|
+
link?: boolean;
|
|
540
|
+
}): Promise<AuthUser>;
|
|
541
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
542
|
+
link?: boolean;
|
|
543
|
+
}): Promise<AuthUser>;
|
|
544
|
+
/**
|
|
545
|
+
* Google Play Games sign-in (email-less, Android-only).
|
|
546
|
+
*
|
|
547
|
+
* **No-arg (1.23.0, recommended):** `signInWithPlayGames()` drives Play Games
|
|
548
|
+
* natively via the `@sendoracloud/sdk-react-native-gaming` companion
|
|
549
|
+
* (`requestServerSideAccess(webClientId, false)`, web client id read from the
|
|
550
|
+
* config-plugin manifest meta-data) and posts the `serverAuthCode`. Needs that
|
|
551
|
+
* companion + its plugin `{ playGamesWebClientId, playGamesProjectId }` + a Dev
|
|
552
|
+
* Client — NOT Expo Go.
|
|
553
|
+
*
|
|
554
|
+
* **Explicit:** `signInWithPlayGames({ serverAuthCode })` posts a code you
|
|
555
|
+
* obtained from your OWN Play Games module.
|
|
556
|
+
*
|
|
557
|
+
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
558
|
+
*/
|
|
559
|
+
signInWithPlayGames(opts?: {
|
|
560
|
+
link?: boolean;
|
|
561
|
+
}): Promise<AuthUser>;
|
|
562
|
+
signInWithPlayGames(payload: {
|
|
563
|
+
serverAuthCode: string;
|
|
564
|
+
}, opts?: {
|
|
565
|
+
link?: boolean;
|
|
566
|
+
}): Promise<AuthUser>;
|
|
567
|
+
/**
|
|
568
|
+
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
569
|
+
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
570
|
+
* identity, forwards it as `prevAnonRefreshToken`, and opts into
|
|
571
|
+
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
572
|
+
* centrally from `retiredAnonUserId`.
|
|
573
|
+
*/
|
|
574
|
+
private gameIdentitySignIn;
|
|
507
575
|
/**
|
|
508
576
|
* Read the stored anon refresh token if (and only if) the local
|
|
509
577
|
* subject is currently anonymous. Used by every identified-session
|
package/dist/index.d.ts
CHANGED
|
@@ -261,6 +261,22 @@ interface AuthTokens {
|
|
|
261
261
|
expiresIn: number;
|
|
262
262
|
tokenType: string;
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Apple Game Center identity payload — the exact shape the Sendora backend
|
|
266
|
+
* verifies, produced by `GKLocalPlayer.local.fetchItems(forIdentityVerification-
|
|
267
|
+
* Signature:)`. `signature`/`salt` are base64; `timestamp` is GameKit's
|
|
268
|
+
* ms-since-epoch. The bundled native module returns this from
|
|
269
|
+
* `fetchGameCenterIdentity()`; you can also build it from your own GameKit
|
|
270
|
+
* module and pass it to `signInWithGameCenter(payload)`.
|
|
271
|
+
*/
|
|
272
|
+
interface GameCenterPayload {
|
|
273
|
+
publicKeyUrl: string;
|
|
274
|
+
signature: string;
|
|
275
|
+
salt: string;
|
|
276
|
+
timestamp: number;
|
|
277
|
+
teamPlayerId: string;
|
|
278
|
+
bundleId: string;
|
|
279
|
+
}
|
|
264
280
|
declare class EmailAlreadyTakenError extends Error {
|
|
265
281
|
constructor(message?: string);
|
|
266
282
|
}
|
|
@@ -504,6 +520,58 @@ declare class Auth {
|
|
|
504
520
|
signInWithLinkedIn(code: string, redirectUri: string): Promise<AuthUser>;
|
|
505
521
|
signInWithFacebook(code: string, redirectUri: string): Promise<AuthUser>;
|
|
506
522
|
signInWithDiscord(code: string, redirectUri: string): Promise<AuthUser>;
|
|
523
|
+
/**
|
|
524
|
+
* Apple Game Center sign-in (email-less, iOS-only).
|
|
525
|
+
*
|
|
526
|
+
* **No-arg (1.23.0, recommended):** `signInWithGameCenter()` drives Game
|
|
527
|
+
* Center natively via the `@sendoracloud/sdk-react-native-gaming` companion
|
|
528
|
+
* (`fetchItems(forIdentityVerificationSignature:)`, base64 signature/salt,
|
|
529
|
+
* `teamPlayerID`, bundle id) and posts the result. Needs that companion + its
|
|
530
|
+
* config plugin `{ gameCenter: true }` + a Dev Client — NOT Expo Go.
|
|
531
|
+
*
|
|
532
|
+
* **Explicit:** `signInWithGameCenter(payload)` posts a payload you obtained
|
|
533
|
+
* from your OWN GameKit module (works anywhere, incl. bare RN).
|
|
534
|
+
*
|
|
535
|
+
* `link: true` KEEPS the same user id when upgrading an anonymous device
|
|
536
|
+
* (ADR-025 link-in-place); no effect off-anon or on a collision.
|
|
537
|
+
*/
|
|
538
|
+
signInWithGameCenter(opts?: {
|
|
539
|
+
link?: boolean;
|
|
540
|
+
}): Promise<AuthUser>;
|
|
541
|
+
signInWithGameCenter(payload: GameCenterPayload, opts?: {
|
|
542
|
+
link?: boolean;
|
|
543
|
+
}): Promise<AuthUser>;
|
|
544
|
+
/**
|
|
545
|
+
* Google Play Games sign-in (email-less, Android-only).
|
|
546
|
+
*
|
|
547
|
+
* **No-arg (1.23.0, recommended):** `signInWithPlayGames()` drives Play Games
|
|
548
|
+
* natively via the `@sendoracloud/sdk-react-native-gaming` companion
|
|
549
|
+
* (`requestServerSideAccess(webClientId, false)`, web client id read from the
|
|
550
|
+
* config-plugin manifest meta-data) and posts the `serverAuthCode`. Needs that
|
|
551
|
+
* companion + its plugin `{ playGamesWebClientId, playGamesProjectId }` + a Dev
|
|
552
|
+
* Client — NOT Expo Go.
|
|
553
|
+
*
|
|
554
|
+
* **Explicit:** `signInWithPlayGames({ serverAuthCode })` posts a code you
|
|
555
|
+
* obtained from your OWN Play Games module.
|
|
556
|
+
*
|
|
557
|
+
* `link: true` = ADR-025 link-in-place (same user id on an anon upgrade).
|
|
558
|
+
*/
|
|
559
|
+
signInWithPlayGames(opts?: {
|
|
560
|
+
link?: boolean;
|
|
561
|
+
}): Promise<AuthUser>;
|
|
562
|
+
signInWithPlayGames(payload: {
|
|
563
|
+
serverAuthCode: string;
|
|
564
|
+
}, opts?: {
|
|
565
|
+
link?: boolean;
|
|
566
|
+
}): Promise<AuthUser>;
|
|
567
|
+
/**
|
|
568
|
+
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
569
|
+
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
570
|
+
* identity, forwards it as `prevAnonRefreshToken`, and opts into
|
|
571
|
+
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
572
|
+
* centrally from `retiredAnonUserId`.
|
|
573
|
+
*/
|
|
574
|
+
private gameIdentitySignIn;
|
|
507
575
|
/**
|
|
508
576
|
* Read the stored anon refresh token if (and only if) the local
|
|
509
577
|
* subject is currently anonymous. Used by every identified-session
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import "react-native-get-random-values";
|
|
3
|
-
import { Platform
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
4
|
|
|
5
5
|
// src/storage.ts
|
|
6
6
|
var PREFIX = "sendora_";
|
|
@@ -157,8 +157,8 @@ var Storage = class {
|
|
|
157
157
|
// package.json
|
|
158
158
|
var package_default = {
|
|
159
159
|
name: "@sendoracloud/sdk-react-native",
|
|
160
|
-
version: "1.
|
|
161
|
-
description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push
|
|
160
|
+
version: "1.23.0",
|
|
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",
|
|
164
164
|
module: "./dist/index.js",
|
|
@@ -179,7 +179,9 @@ var package_default = {
|
|
|
179
179
|
"dist",
|
|
180
180
|
"examples",
|
|
181
181
|
"README.md",
|
|
182
|
-
"LICENSE"
|
|
182
|
+
"LICENSE",
|
|
183
|
+
"app.plugin.js",
|
|
184
|
+
"plugin"
|
|
183
185
|
],
|
|
184
186
|
keywords: [
|
|
185
187
|
"sendora",
|
|
@@ -211,7 +213,7 @@ var package_default = {
|
|
|
211
213
|
scripts: {
|
|
212
214
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
213
215
|
typecheck: "tsc --noEmit",
|
|
214
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.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"
|
|
215
217
|
},
|
|
216
218
|
devDependencies: {
|
|
217
219
|
"@types/react": "^19.2.16",
|
|
@@ -324,6 +326,32 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
|
|
|
324
326
|
return res;
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
// src/native.ts
|
|
330
|
+
import { NativeModules } from "react-native";
|
|
331
|
+
function requireOptionalNativeModule(name) {
|
|
332
|
+
try {
|
|
333
|
+
const g = globalThis;
|
|
334
|
+
const m = g?.expo?.modules?.[name];
|
|
335
|
+
if (m && typeof m === "object") return m;
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const g = globalThis;
|
|
340
|
+
const nm = NativeModules;
|
|
341
|
+
const proxy = g?.expo?.modules?.NativeModulesProxy ?? nm?.NativeUnimoduleProxy;
|
|
342
|
+
const consts = proxy?.modulesConstants?.[name];
|
|
343
|
+
if (consts && typeof consts === "object") return consts;
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const nm = NativeModules;
|
|
348
|
+
const m = nm?.[name];
|
|
349
|
+
if (m && typeof m === "object") return m;
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
327
355
|
// src/auth.ts
|
|
328
356
|
import { AppState as RnAppState } from "react-native";
|
|
329
357
|
var TOKEN_KEY = "auth_access_token";
|
|
@@ -360,6 +388,48 @@ var AuthError = class extends Error {
|
|
|
360
388
|
this.code = code;
|
|
361
389
|
}
|
|
362
390
|
};
|
|
391
|
+
var GAME_AUTH_NATIVE_MODULE = "SendoraGameAuth";
|
|
392
|
+
async function fetchGameCenterPayloadNative() {
|
|
393
|
+
const mod = requireOptionalNativeModule(GAME_AUTH_NATIVE_MODULE);
|
|
394
|
+
const fn = mod?.fetchGameCenterIdentity;
|
|
395
|
+
if (typeof fn !== "function") {
|
|
396
|
+
throw new AuthError(
|
|
397
|
+
"GAME_CENTER_UNAVAILABLE",
|
|
398
|
+
"Game Center native module not found. Install @sendoracloud/sdk-react-native-gaming, add its config plugin ({ gameCenter: true }), and run `npx expo prebuild` (or EAS Build) \u2014 Game Center sign-in needs a Dev Client, not Expo Go. Or pass a payload from your own GameKit module."
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const raw = await fn.call(mod);
|
|
402
|
+
return coerceGameCenterPayload(raw);
|
|
403
|
+
}
|
|
404
|
+
function coerceGameCenterPayload(raw) {
|
|
405
|
+
const r = raw ?? {};
|
|
406
|
+
const publicKeyUrl = typeof r.publicKeyUrl === "string" ? r.publicKeyUrl : "";
|
|
407
|
+
const signature = typeof r.signature === "string" ? r.signature : "";
|
|
408
|
+
const salt = typeof r.salt === "string" ? r.salt : "";
|
|
409
|
+
const teamPlayerId = typeof r.teamPlayerId === "string" ? r.teamPlayerId : "";
|
|
410
|
+
const bundleId = typeof r.bundleId === "string" ? r.bundleId : "";
|
|
411
|
+
const timestamp = typeof r.timestamp === "number" ? r.timestamp : Number(r.timestamp);
|
|
412
|
+
if (!publicKeyUrl || !signature || !salt || !teamPlayerId || !bundleId || !Number.isFinite(timestamp)) {
|
|
413
|
+
throw new AuthError("GAME_CENTER_INVALID_PAYLOAD", "Game Center returned an incomplete identity payload.");
|
|
414
|
+
}
|
|
415
|
+
return { publicKeyUrl, signature, salt, timestamp, teamPlayerId, bundleId };
|
|
416
|
+
}
|
|
417
|
+
async function fetchPlayGamesAuthCodeNative() {
|
|
418
|
+
const mod = requireOptionalNativeModule(GAME_AUTH_NATIVE_MODULE);
|
|
419
|
+
const fn = mod?.fetchServerAuthCode;
|
|
420
|
+
if (typeof fn !== "function") {
|
|
421
|
+
throw new AuthError(
|
|
422
|
+
"PLAY_GAMES_UNAVAILABLE",
|
|
423
|
+
"Play Games native module not found. Install @sendoracloud/sdk-react-native-gaming, add its config plugin ({ playGamesWebClientId, playGamesProjectId }), and run `npx expo prebuild` (or EAS Build) \u2014 Play Games sign-in needs a Dev Client, not Expo Go. Or pass a serverAuthCode from your own Play Games module."
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const raw = await fn.call(mod);
|
|
427
|
+
const serverAuthCode = typeof raw?.serverAuthCode === "string" ? raw.serverAuthCode : "";
|
|
428
|
+
if (!serverAuthCode) {
|
|
429
|
+
throw new AuthError("PLAY_GAMES_INVALID_PAYLOAD", "Play Games returned no server auth code.");
|
|
430
|
+
}
|
|
431
|
+
return { serverAuthCode };
|
|
432
|
+
}
|
|
363
433
|
var Auth = class {
|
|
364
434
|
constructor(hooks) {
|
|
365
435
|
this.user = null;
|
|
@@ -709,6 +779,72 @@ var Auth = class {
|
|
|
709
779
|
signInWithDiscord(code, redirectUri) {
|
|
710
780
|
return this.loginSocial({ provider: "discord", code, redirectUri });
|
|
711
781
|
}
|
|
782
|
+
signInWithGameCenter(payloadOrOpts, maybeOpts) {
|
|
783
|
+
const path = "/api/v1/auth-service/login/game-center";
|
|
784
|
+
if (payloadOrOpts && "publicKeyUrl" in payloadOrOpts) {
|
|
785
|
+
return this.gameIdentitySignIn(path, { ...payloadOrOpts }, maybeOpts?.link);
|
|
786
|
+
}
|
|
787
|
+
const link = payloadOrOpts?.link;
|
|
788
|
+
return this.gameIdentitySignIn(
|
|
789
|
+
path,
|
|
790
|
+
async () => await fetchGameCenterPayloadNative(),
|
|
791
|
+
link
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
signInWithPlayGames(payloadOrOpts, maybeOpts) {
|
|
795
|
+
const path = "/api/v1/auth-service/login/play-games";
|
|
796
|
+
if (payloadOrOpts && "serverAuthCode" in payloadOrOpts) {
|
|
797
|
+
const { serverAuthCode } = payloadOrOpts;
|
|
798
|
+
return this.gameIdentitySignIn(path, { serverAuthCode }, maybeOpts?.link);
|
|
799
|
+
}
|
|
800
|
+
const link = payloadOrOpts?.link;
|
|
801
|
+
return this.gameIdentitySignIn(
|
|
802
|
+
path,
|
|
803
|
+
async () => await fetchPlayGamesAuthCodeNative(),
|
|
804
|
+
link
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Shared gaming-identity sign-in. Mirrors `loginSocial`'s anon-takeover +
|
|
809
|
+
* ADR-025 link-in-place handling: reads the anon refresh BEFORE wiping local
|
|
810
|
+
* identity, forwards it as `prevAnonRefreshToken`, and opts into
|
|
811
|
+
* `linkAnonymous` when `link` is set. `persist()` fires `onDeviceTakeover`
|
|
812
|
+
* centrally from `retiredAnonUserId`.
|
|
813
|
+
*/
|
|
814
|
+
gameIdentitySignIn(path, identityBody, link) {
|
|
815
|
+
return this.serialize(async () => {
|
|
816
|
+
const body = typeof identityBody === "function" ? await identityBody() : identityBody;
|
|
817
|
+
let prevAnonRefreshToken;
|
|
818
|
+
if (this.user?.isAnonymous) {
|
|
819
|
+
const stashed = this.hooks.storage.get(REFRESH_KEY);
|
|
820
|
+
if (stashed) prevAnonRefreshToken = stashed;
|
|
821
|
+
}
|
|
822
|
+
if (this.user !== null) await this.wipeLocalIdentity();
|
|
823
|
+
const payload = { ...body };
|
|
824
|
+
if (prevAnonRefreshToken) payload.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
825
|
+
if (link) payload.linkAnonymous = true;
|
|
826
|
+
const res = await post(
|
|
827
|
+
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
828
|
+
path,
|
|
829
|
+
payload
|
|
830
|
+
);
|
|
831
|
+
if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
|
|
832
|
+
let parsed;
|
|
833
|
+
try {
|
|
834
|
+
parsed = await res.json();
|
|
835
|
+
} catch {
|
|
836
|
+
throw new AuthError("PARSE_ERROR", `HTTP ${res.status}`);
|
|
837
|
+
}
|
|
838
|
+
if (!res.ok || !parsed.success || !isAuthApiResponse(parsed.data)) {
|
|
839
|
+
throw new AuthError(
|
|
840
|
+
parsed.error?.code ?? "GAME_LOGIN_FAILED",
|
|
841
|
+
parsed.error?.message ?? "Game sign-in failed"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
this.persist(parsed.data);
|
|
845
|
+
return parsed.data.user;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
712
848
|
/**
|
|
713
849
|
* Read the stored anon refresh token if (and only if) the local
|
|
714
850
|
* subject is currently anonymous. Used by every identified-session
|
|
@@ -2092,27 +2228,7 @@ function buildDeviceContext(appVersion, appBuild) {
|
|
|
2092
2228
|
}
|
|
2093
2229
|
}
|
|
2094
2230
|
function expoModule(name) {
|
|
2095
|
-
|
|
2096
|
-
const g = globalThis;
|
|
2097
|
-
const m = g?.expo?.modules?.[name];
|
|
2098
|
-
if (m && typeof m === "object") return m;
|
|
2099
|
-
} catch {
|
|
2100
|
-
}
|
|
2101
|
-
try {
|
|
2102
|
-
const g = globalThis;
|
|
2103
|
-
const nm = NativeModules;
|
|
2104
|
-
const proxy = g?.expo?.modules?.NativeModulesProxy ?? nm?.NativeUnimoduleProxy;
|
|
2105
|
-
const consts = proxy?.modulesConstants?.[name];
|
|
2106
|
-
if (consts && typeof consts === "object") return consts;
|
|
2107
|
-
} catch {
|
|
2108
|
-
}
|
|
2109
|
-
try {
|
|
2110
|
-
const nm = NativeModules;
|
|
2111
|
-
const m = nm?.[name];
|
|
2112
|
-
if (m && typeof m === "object") return m;
|
|
2113
|
-
} catch {
|
|
2114
|
-
}
|
|
2115
|
-
return void 0;
|
|
2231
|
+
return requireOptionalNativeModule(name);
|
|
2116
2232
|
}
|
|
2117
2233
|
function expoApplicationVersion() {
|
|
2118
2234
|
const a = expoModule("ExpoApplication");
|
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 — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push
|
|
3
|
+
"version": "1.23.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",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"dist",
|
|
23
23
|
"examples",
|
|
24
24
|
"README.md",
|
|
25
|
-
"LICENSE"
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"app.plugin.js",
|
|
27
|
+
"plugin"
|
|
26
28
|
],
|
|
27
29
|
"keywords": [
|
|
28
30
|
"sendora",
|
|
@@ -54,7 +56,7 @@
|
|
|
54
56
|
"scripts": {
|
|
55
57
|
"build": "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
56
58
|
"typecheck": "tsc --noEmit",
|
|
57
|
-
"test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.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"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
62
|
"@types/react": "^19.2.16",
|
package/plugin/apply.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Pure, dependency-free helpers for the Sendora Expo config plugin
|
|
2
|
+
// (../app.plugin.js). Extracted so the bug-prone logic (host resolution,
|
|
3
|
+
// merge/dedupe, no-clobber) is unit-testable WITHOUT `@expo/config-plugins`,
|
|
4
|
+
// which only exists in the consumer's Expo prebuild environment.
|
|
5
|
+
//
|
|
6
|
+
// ESM — the package is `type: module`.
|
|
7
|
+
|
|
8
|
+
/** Resolve the deduped host list from the plugin props. */
|
|
9
|
+
export function resolveHosts(props) {
|
|
10
|
+
const hosts = new Set();
|
|
11
|
+
if (props && typeof props.linkSubdomain === "string" && props.linkSubdomain.trim()) {
|
|
12
|
+
hosts.add(`${props.linkSubdomain.trim()}.sendoracloud.app`);
|
|
13
|
+
}
|
|
14
|
+
if (props && Array.isArray(props.linkDomains)) {
|
|
15
|
+
for (const d of props.linkDomains) {
|
|
16
|
+
if (typeof d === "string" && d.trim()) hosts.add(d.trim());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Array.from(hosts);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merge `applinks:<host>` entries into an existing associated-domains array,
|
|
24
|
+
* preserving anything already there (no clobber) and deduping.
|
|
25
|
+
*/
|
|
26
|
+
export function mergeAssociatedDomains(existing, hosts) {
|
|
27
|
+
const base = Array.isArray(existing) ? existing : [];
|
|
28
|
+
const applinks = hosts.map((h) => `applinks:${h}`);
|
|
29
|
+
return Array.from(new Set([...base, ...applinks]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Pick the launcher/main activity from an AndroidManifest application node. */
|
|
33
|
+
export function findMainActivity(activities) {
|
|
34
|
+
const list = Array.isArray(activities) ? activities : [];
|
|
35
|
+
return (
|
|
36
|
+
list.find(
|
|
37
|
+
(a) =>
|
|
38
|
+
(a.$ && a.$["android:name"] === ".MainActivity") ||
|
|
39
|
+
(a["intent-filter"] || []).some((f) =>
|
|
40
|
+
(f.action || []).some(
|
|
41
|
+
(ac) => ac.$ && ac.$["android:name"] === "android.intent.action.MAIN",
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
) ||
|
|
45
|
+
list[0] ||
|
|
46
|
+
null
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add an autoVerify App Links `<intent-filter>` (https + host) to the activity
|
|
52
|
+
* for each host, skipping hosts already present. Mutates + returns the activity.
|
|
53
|
+
*/
|
|
54
|
+
export function addAppLinkIntentFilters(mainActivity, hosts) {
|
|
55
|
+
if (!mainActivity) return mainActivity;
|
|
56
|
+
mainActivity["intent-filter"] = mainActivity["intent-filter"] || [];
|
|
57
|
+
for (const host of hosts) {
|
|
58
|
+
const already = mainActivity["intent-filter"].some((f) =>
|
|
59
|
+
(f.data || []).some((d) => d.$ && d.$["android:host"] === host),
|
|
60
|
+
);
|
|
61
|
+
if (already) continue;
|
|
62
|
+
mainActivity["intent-filter"].push({
|
|
63
|
+
$: { "android:autoVerify": "true" },
|
|
64
|
+
action: [{ $: { "android:name": "android.intent.action.VIEW" } }],
|
|
65
|
+
category: [
|
|
66
|
+
{ $: { "android:name": "android.intent.category.DEFAULT" } },
|
|
67
|
+
{ $: { "android:name": "android.intent.category.BROWSABLE" } },
|
|
68
|
+
],
|
|
69
|
+
data: [{ $: { "android:scheme": "https", "android:host": host } }],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return mainActivity;
|
|
73
|
+
}
|