@sendoracloud/sdk-react-native 1.0.3 → 1.0.5
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 +58 -3
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +58 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -237,6 +237,8 @@ var Auth = class {
|
|
|
237
237
|
this.accessExpiresAt = 0;
|
|
238
238
|
this.inflight = Promise.resolve();
|
|
239
239
|
this.refreshInflight = null;
|
|
240
|
+
this.takeoverListeners = /* @__PURE__ */ new Set();
|
|
241
|
+
this.lastTakeover = null;
|
|
240
242
|
/**
|
|
241
243
|
* Proactive refresh (s58.72). Fires when access-token age crosses
|
|
242
244
|
* ~80% of TTL. Pairs with the AppState foreground listener so a
|
|
@@ -299,6 +301,47 @@ var Auth = class {
|
|
|
299
301
|
getCurrentUser() {
|
|
300
302
|
return this.user;
|
|
301
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Subscribe to device-takeover events (s58.111+). Backend retires
|
|
306
|
+
* the anonymous user_id whenever a previously-anon device signs in
|
|
307
|
+
* to an identified account. Use this to clean up any local mirror
|
|
308
|
+
* keyed by the anonymous id — your own users table, an analytics
|
|
309
|
+
* cache, an audience filter — without needing a webhook receiver.
|
|
310
|
+
*
|
|
311
|
+
* Listener is invoked from `signIn`, `loginSocial`, `verifyMagicLink`,
|
|
312
|
+
* `verifyEmailOtp`, `passkey authentication`, and SSO completion
|
|
313
|
+
* (via `consumeSsoFragment`). Returns an unsubscribe function.
|
|
314
|
+
*
|
|
315
|
+
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
316
|
+
* + logged in dev mode. Auth state never depends on listener success.
|
|
317
|
+
*/
|
|
318
|
+
onDeviceTakeover(listener) {
|
|
319
|
+
this.takeoverListeners.add(listener);
|
|
320
|
+
return () => {
|
|
321
|
+
this.takeoverListeners.delete(listener);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Inspect the most recent device-takeover event observed by the
|
|
326
|
+
* SDK. Cleared on signOut. Returns null when no takeover has
|
|
327
|
+
* happened during this session — useful for late subscribers or
|
|
328
|
+
* post-sign-in side-effects (e.g. app-launch hydrate).
|
|
329
|
+
*/
|
|
330
|
+
getLastDeviceTakeover() {
|
|
331
|
+
return this.lastTakeover;
|
|
332
|
+
}
|
|
333
|
+
/** Internal — fan out to subscribers + cache the latest. */
|
|
334
|
+
fireDeviceTakeover(retiredAnonUserId, identifiedUserId) {
|
|
335
|
+
const evt = { retiredAnonUserId, identifiedUserId, at: /* @__PURE__ */ new Date() };
|
|
336
|
+
this.lastTakeover = evt;
|
|
337
|
+
for (const fn of this.takeoverListeners) {
|
|
338
|
+
try {
|
|
339
|
+
fn(evt);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (this.hooks.debug) console.warn("[sendoracloud] onDeviceTakeover listener threw", err);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
302
345
|
/**
|
|
303
346
|
* Returns a non-expired access token or null. If the cached token
|
|
304
347
|
* is expired but a refresh token exists, transparently refreshes
|
|
@@ -684,10 +727,13 @@ var Auth = class {
|
|
|
684
727
|
* call `consumeSsoFragment(url)` on the captured URL.
|
|
685
728
|
*/
|
|
686
729
|
async startSso(returnTo) {
|
|
730
|
+
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
731
|
+
const body = { returnTo };
|
|
732
|
+
if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
687
733
|
const res = await post(
|
|
688
734
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
689
735
|
"/api/v1/auth-service/sso/oidc/start",
|
|
690
|
-
|
|
736
|
+
body
|
|
691
737
|
);
|
|
692
738
|
if (!res || !res.ok) throw new AuthError("SSO_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
693
739
|
const parsed = await res.json();
|
|
@@ -707,10 +753,13 @@ var Auth = class {
|
|
|
707
753
|
* and SAML fragments transparently.
|
|
708
754
|
*/
|
|
709
755
|
async startSaml(returnTo) {
|
|
756
|
+
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
757
|
+
const body = { returnTo };
|
|
758
|
+
if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
710
759
|
const res = await post(
|
|
711
760
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
712
761
|
"/api/v1/auth-service/sso/saml/start",
|
|
713
|
-
|
|
762
|
+
body
|
|
714
763
|
);
|
|
715
764
|
if (!res || !res.ok) throw new AuthError("SAML_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
716
765
|
const parsed = await res.json();
|
|
@@ -735,6 +784,7 @@ var Auth = class {
|
|
|
735
784
|
if (err) throw new AuthError("SSO_FAILED", decodeURIComponent(err));
|
|
736
785
|
const refresh = params.get("sendora_oidc_token") ?? params.get("sendora_saml_token");
|
|
737
786
|
if (!refresh) throw new AuthError("SSO_NO_TOKEN", "Callback URL has no SSO token");
|
|
787
|
+
const retiredAnonUserId = params.get("sendora_retired_anon");
|
|
738
788
|
const res = await post(
|
|
739
789
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
740
790
|
"/api/v1/auth-service/token/refresh",
|
|
@@ -763,7 +813,8 @@ var Auth = class {
|
|
|
763
813
|
refreshToken: newRefresh,
|
|
764
814
|
expiresIn,
|
|
765
815
|
tokenType: "Bearer"
|
|
766
|
-
}
|
|
816
|
+
},
|
|
817
|
+
...retiredAnonUserId ? { retiredAnonUserId } : {}
|
|
767
818
|
});
|
|
768
819
|
return user;
|
|
769
820
|
});
|
|
@@ -1004,11 +1055,15 @@ var Auth = class {
|
|
|
1004
1055
|
this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
|
|
1005
1056
|
this.hooks.onIdentityChange(data.user.id);
|
|
1006
1057
|
this.startProactiveRefreshCron();
|
|
1058
|
+
if (data.retiredAnonUserId) {
|
|
1059
|
+
this.fireDeviceTakeover(data.retiredAnonUserId, data.user.id);
|
|
1060
|
+
}
|
|
1007
1061
|
}
|
|
1008
1062
|
async wipeLocalIdentity() {
|
|
1009
1063
|
this.user = null;
|
|
1010
1064
|
this.accessToken = null;
|
|
1011
1065
|
this.accessExpiresAt = 0;
|
|
1066
|
+
this.lastTakeover = null;
|
|
1012
1067
|
this.hooks.storage.remove(USER_KEY);
|
|
1013
1068
|
this.hooks.storage.remove(TOKEN_KEY);
|
|
1014
1069
|
this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
|
package/dist/index.d.cts
CHANGED
|
@@ -99,6 +99,19 @@ interface AuthHooks {
|
|
|
99
99
|
publicKey: string;
|
|
100
100
|
debug: boolean;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Detail handed to onDeviceTakeover subscribers. Fires once per
|
|
104
|
+
* signIn / loginSocial / verifyMagicLink / verifyEmailOtp /
|
|
105
|
+
* passkey-auth / SSO completion that retires an anonymous row.
|
|
106
|
+
*/
|
|
107
|
+
interface DeviceTakeoverEvent {
|
|
108
|
+
/** The anonymous user_id Sendora just hard-deleted. */
|
|
109
|
+
retiredAnonUserId: string;
|
|
110
|
+
/** The identified user_id that took the device over. */
|
|
111
|
+
identifiedUserId: string;
|
|
112
|
+
/** Local clock at the time the SDK observed the event. */
|
|
113
|
+
at: Date;
|
|
114
|
+
}
|
|
102
115
|
declare class Auth {
|
|
103
116
|
private hooks;
|
|
104
117
|
private user;
|
|
@@ -106,6 +119,8 @@ declare class Auth {
|
|
|
106
119
|
private accessExpiresAt;
|
|
107
120
|
private inflight;
|
|
108
121
|
private refreshInflight;
|
|
122
|
+
private takeoverListeners;
|
|
123
|
+
private lastTakeover;
|
|
109
124
|
/**
|
|
110
125
|
* Resolves when the parent SDK has finished its async init() —
|
|
111
126
|
* AsyncStorage hydrated + auth.hydrate() restored the cached
|
|
@@ -142,6 +157,30 @@ declare class Auth {
|
|
|
142
157
|
*/
|
|
143
158
|
hydrate(): void;
|
|
144
159
|
getCurrentUser(): AuthUser | null;
|
|
160
|
+
/**
|
|
161
|
+
* Subscribe to device-takeover events (s58.111+). Backend retires
|
|
162
|
+
* the anonymous user_id whenever a previously-anon device signs in
|
|
163
|
+
* to an identified account. Use this to clean up any local mirror
|
|
164
|
+
* keyed by the anonymous id — your own users table, an analytics
|
|
165
|
+
* cache, an audience filter — without needing a webhook receiver.
|
|
166
|
+
*
|
|
167
|
+
* Listener is invoked from `signIn`, `loginSocial`, `verifyMagicLink`,
|
|
168
|
+
* `verifyEmailOtp`, `passkey authentication`, and SSO completion
|
|
169
|
+
* (via `consumeSsoFragment`). Returns an unsubscribe function.
|
|
170
|
+
*
|
|
171
|
+
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
172
|
+
* + logged in dev mode. Auth state never depends on listener success.
|
|
173
|
+
*/
|
|
174
|
+
onDeviceTakeover(listener: (evt: DeviceTakeoverEvent) => void): () => void;
|
|
175
|
+
/**
|
|
176
|
+
* Inspect the most recent device-takeover event observed by the
|
|
177
|
+
* SDK. Cleared on signOut. Returns null when no takeover has
|
|
178
|
+
* happened during this session — useful for late subscribers or
|
|
179
|
+
* post-sign-in side-effects (e.g. app-launch hydrate).
|
|
180
|
+
*/
|
|
181
|
+
getLastDeviceTakeover(): DeviceTakeoverEvent | null;
|
|
182
|
+
/** Internal — fan out to subscribers + cache the latest. */
|
|
183
|
+
private fireDeviceTakeover;
|
|
145
184
|
/**
|
|
146
185
|
* Returns a non-expired access token or null. If the cached token
|
|
147
186
|
* is expired but a refresh token exists, transparently refreshes
|
package/dist/index.d.ts
CHANGED
|
@@ -99,6 +99,19 @@ interface AuthHooks {
|
|
|
99
99
|
publicKey: string;
|
|
100
100
|
debug: boolean;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Detail handed to onDeviceTakeover subscribers. Fires once per
|
|
104
|
+
* signIn / loginSocial / verifyMagicLink / verifyEmailOtp /
|
|
105
|
+
* passkey-auth / SSO completion that retires an anonymous row.
|
|
106
|
+
*/
|
|
107
|
+
interface DeviceTakeoverEvent {
|
|
108
|
+
/** The anonymous user_id Sendora just hard-deleted. */
|
|
109
|
+
retiredAnonUserId: string;
|
|
110
|
+
/** The identified user_id that took the device over. */
|
|
111
|
+
identifiedUserId: string;
|
|
112
|
+
/** Local clock at the time the SDK observed the event. */
|
|
113
|
+
at: Date;
|
|
114
|
+
}
|
|
102
115
|
declare class Auth {
|
|
103
116
|
private hooks;
|
|
104
117
|
private user;
|
|
@@ -106,6 +119,8 @@ declare class Auth {
|
|
|
106
119
|
private accessExpiresAt;
|
|
107
120
|
private inflight;
|
|
108
121
|
private refreshInflight;
|
|
122
|
+
private takeoverListeners;
|
|
123
|
+
private lastTakeover;
|
|
109
124
|
/**
|
|
110
125
|
* Resolves when the parent SDK has finished its async init() —
|
|
111
126
|
* AsyncStorage hydrated + auth.hydrate() restored the cached
|
|
@@ -142,6 +157,30 @@ declare class Auth {
|
|
|
142
157
|
*/
|
|
143
158
|
hydrate(): void;
|
|
144
159
|
getCurrentUser(): AuthUser | null;
|
|
160
|
+
/**
|
|
161
|
+
* Subscribe to device-takeover events (s58.111+). Backend retires
|
|
162
|
+
* the anonymous user_id whenever a previously-anon device signs in
|
|
163
|
+
* to an identified account. Use this to clean up any local mirror
|
|
164
|
+
* keyed by the anonymous id — your own users table, an analytics
|
|
165
|
+
* cache, an audience filter — without needing a webhook receiver.
|
|
166
|
+
*
|
|
167
|
+
* Listener is invoked from `signIn`, `loginSocial`, `verifyMagicLink`,
|
|
168
|
+
* `verifyEmailOtp`, `passkey authentication`, and SSO completion
|
|
169
|
+
* (via `consumeSsoFragment`). Returns an unsubscribe function.
|
|
170
|
+
*
|
|
171
|
+
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
172
|
+
* + logged in dev mode. Auth state never depends on listener success.
|
|
173
|
+
*/
|
|
174
|
+
onDeviceTakeover(listener: (evt: DeviceTakeoverEvent) => void): () => void;
|
|
175
|
+
/**
|
|
176
|
+
* Inspect the most recent device-takeover event observed by the
|
|
177
|
+
* SDK. Cleared on signOut. Returns null when no takeover has
|
|
178
|
+
* happened during this session — useful for late subscribers or
|
|
179
|
+
* post-sign-in side-effects (e.g. app-launch hydrate).
|
|
180
|
+
*/
|
|
181
|
+
getLastDeviceTakeover(): DeviceTakeoverEvent | null;
|
|
182
|
+
/** Internal — fan out to subscribers + cache the latest. */
|
|
183
|
+
private fireDeviceTakeover;
|
|
145
184
|
/**
|
|
146
185
|
* Returns a non-expired access token or null. If the cached token
|
|
147
186
|
* is expired but a refresh token exists, transparently refreshes
|
package/dist/index.js
CHANGED
|
@@ -194,6 +194,8 @@ var Auth = class {
|
|
|
194
194
|
this.accessExpiresAt = 0;
|
|
195
195
|
this.inflight = Promise.resolve();
|
|
196
196
|
this.refreshInflight = null;
|
|
197
|
+
this.takeoverListeners = /* @__PURE__ */ new Set();
|
|
198
|
+
this.lastTakeover = null;
|
|
197
199
|
/**
|
|
198
200
|
* Proactive refresh (s58.72). Fires when access-token age crosses
|
|
199
201
|
* ~80% of TTL. Pairs with the AppState foreground listener so a
|
|
@@ -256,6 +258,47 @@ var Auth = class {
|
|
|
256
258
|
getCurrentUser() {
|
|
257
259
|
return this.user;
|
|
258
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Subscribe to device-takeover events (s58.111+). Backend retires
|
|
263
|
+
* the anonymous user_id whenever a previously-anon device signs in
|
|
264
|
+
* to an identified account. Use this to clean up any local mirror
|
|
265
|
+
* keyed by the anonymous id — your own users table, an analytics
|
|
266
|
+
* cache, an audience filter — without needing a webhook receiver.
|
|
267
|
+
*
|
|
268
|
+
* Listener is invoked from `signIn`, `loginSocial`, `verifyMagicLink`,
|
|
269
|
+
* `verifyEmailOtp`, `passkey authentication`, and SSO completion
|
|
270
|
+
* (via `consumeSsoFragment`). Returns an unsubscribe function.
|
|
271
|
+
*
|
|
272
|
+
* Listeners are best-effort: errors thrown inside them are swallowed
|
|
273
|
+
* + logged in dev mode. Auth state never depends on listener success.
|
|
274
|
+
*/
|
|
275
|
+
onDeviceTakeover(listener) {
|
|
276
|
+
this.takeoverListeners.add(listener);
|
|
277
|
+
return () => {
|
|
278
|
+
this.takeoverListeners.delete(listener);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Inspect the most recent device-takeover event observed by the
|
|
283
|
+
* SDK. Cleared on signOut. Returns null when no takeover has
|
|
284
|
+
* happened during this session — useful for late subscribers or
|
|
285
|
+
* post-sign-in side-effects (e.g. app-launch hydrate).
|
|
286
|
+
*/
|
|
287
|
+
getLastDeviceTakeover() {
|
|
288
|
+
return this.lastTakeover;
|
|
289
|
+
}
|
|
290
|
+
/** Internal — fan out to subscribers + cache the latest. */
|
|
291
|
+
fireDeviceTakeover(retiredAnonUserId, identifiedUserId) {
|
|
292
|
+
const evt = { retiredAnonUserId, identifiedUserId, at: /* @__PURE__ */ new Date() };
|
|
293
|
+
this.lastTakeover = evt;
|
|
294
|
+
for (const fn of this.takeoverListeners) {
|
|
295
|
+
try {
|
|
296
|
+
fn(evt);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (this.hooks.debug) console.warn("[sendoracloud] onDeviceTakeover listener threw", err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
259
302
|
/**
|
|
260
303
|
* Returns a non-expired access token or null. If the cached token
|
|
261
304
|
* is expired but a refresh token exists, transparently refreshes
|
|
@@ -641,10 +684,13 @@ var Auth = class {
|
|
|
641
684
|
* call `consumeSsoFragment(url)` on the captured URL.
|
|
642
685
|
*/
|
|
643
686
|
async startSso(returnTo) {
|
|
687
|
+
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
688
|
+
const body = { returnTo };
|
|
689
|
+
if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
644
690
|
const res = await post(
|
|
645
691
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
646
692
|
"/api/v1/auth-service/sso/oidc/start",
|
|
647
|
-
|
|
693
|
+
body
|
|
648
694
|
);
|
|
649
695
|
if (!res || !res.ok) throw new AuthError("SSO_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
650
696
|
const parsed = await res.json();
|
|
@@ -664,10 +710,13 @@ var Auth = class {
|
|
|
664
710
|
* and SAML fragments transparently.
|
|
665
711
|
*/
|
|
666
712
|
async startSaml(returnTo) {
|
|
713
|
+
const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
|
|
714
|
+
const body = { returnTo };
|
|
715
|
+
if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
|
|
667
716
|
const res = await post(
|
|
668
717
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
669
718
|
"/api/v1/auth-service/sso/saml/start",
|
|
670
|
-
|
|
719
|
+
body
|
|
671
720
|
);
|
|
672
721
|
if (!res || !res.ok) throw new AuthError("SAML_START_FAILED", `HTTP ${res?.status ?? "network"}`);
|
|
673
722
|
const parsed = await res.json();
|
|
@@ -692,6 +741,7 @@ var Auth = class {
|
|
|
692
741
|
if (err) throw new AuthError("SSO_FAILED", decodeURIComponent(err));
|
|
693
742
|
const refresh = params.get("sendora_oidc_token") ?? params.get("sendora_saml_token");
|
|
694
743
|
if (!refresh) throw new AuthError("SSO_NO_TOKEN", "Callback URL has no SSO token");
|
|
744
|
+
const retiredAnonUserId = params.get("sendora_retired_anon");
|
|
695
745
|
const res = await post(
|
|
696
746
|
{ apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
|
|
697
747
|
"/api/v1/auth-service/token/refresh",
|
|
@@ -720,7 +770,8 @@ var Auth = class {
|
|
|
720
770
|
refreshToken: newRefresh,
|
|
721
771
|
expiresIn,
|
|
722
772
|
tokenType: "Bearer"
|
|
723
|
-
}
|
|
773
|
+
},
|
|
774
|
+
...retiredAnonUserId ? { retiredAnonUserId } : {}
|
|
724
775
|
});
|
|
725
776
|
return user;
|
|
726
777
|
});
|
|
@@ -961,11 +1012,15 @@ var Auth = class {
|
|
|
961
1012
|
this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
|
|
962
1013
|
this.hooks.onIdentityChange(data.user.id);
|
|
963
1014
|
this.startProactiveRefreshCron();
|
|
1015
|
+
if (data.retiredAnonUserId) {
|
|
1016
|
+
this.fireDeviceTakeover(data.retiredAnonUserId, data.user.id);
|
|
1017
|
+
}
|
|
964
1018
|
}
|
|
965
1019
|
async wipeLocalIdentity() {
|
|
966
1020
|
this.user = null;
|
|
967
1021
|
this.accessToken = null;
|
|
968
1022
|
this.accessExpiresAt = 0;
|
|
1023
|
+
this.lastTakeover = null;
|
|
969
1024
|
this.hooks.storage.remove(USER_KEY);
|
|
970
1025
|
this.hooks.storage.remove(TOKEN_KEY);
|
|
971
1026
|
this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
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 token registration requires a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|