@sendoracloud/sdk-react-native 0.10.1 → 0.10.3

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
@@ -231,9 +231,14 @@ var Auth = class {
231
231
  this.inflight = Promise.resolve();
232
232
  this.refreshInflight = null;
233
233
  this.hooks = hooks;
234
+ this.readyPromise = new Promise((res) => {
235
+ this.resolveReady = res;
236
+ });
234
237
  }
235
238
  /** Re-hydrate session from storage. Called by the parent SDK
236
- * during init() after Storage.hydrate() has run. */
239
+ * during init() after Storage.hydrate() has run. Marks the
240
+ * internal ready promise as resolved so queued auth calls
241
+ * unblock. */
237
242
  hydrate() {
238
243
  const cachedUser = this.hooks.storage.get(USER_KEY);
239
244
  const cachedToken = this.hooks.storage.get(TOKEN_KEY);
@@ -253,6 +258,7 @@ var Auth = class {
253
258
  this.hooks.storage.remove(REFRESH_KEY);
254
259
  }
255
260
  }
261
+ this.resolveReady();
256
262
  }
257
263
  getCurrentUser() {
258
264
  return this.user;
@@ -283,11 +289,29 @@ var Auth = class {
283
289
  return data.user;
284
290
  });
285
291
  }
292
+ /**
293
+ * Create an email/password account.
294
+ *
295
+ * Industry-standard upgrade-first flow (matches Firebase
296
+ * `linkWithCredential`, Auth0 anonymous-merge, Supabase
297
+ * `linkIdentity`): when a refresh token is present in storage we
298
+ * try `/auth-service/upgrade` first — backend validates the
299
+ * refresh, checks the bound user is anonymous, and promotes in
300
+ * place preserving `user.id`. If the bound user is NOT anonymous
301
+ * (already identified, expired refresh, etc.), backend rejects
302
+ * with FORBIDDEN_NON_ANONYMOUS / INVALID_REFRESH and we fall back
303
+ * to a fresh /signup.
304
+ *
305
+ * Why we don't gate on the cached `this.user.isAnonymous`
306
+ * boolean: the in-memory user can be stale during cold start or
307
+ * fast-refresh races. The refresh token in storage is the
308
+ * authoritative signal — if it's there, the upgrade path is
309
+ * worth attempting; backend has the source of truth.
310
+ */
286
311
  signUp(email, password, opts) {
287
312
  return this.serialize(async () => {
288
313
  const refresh = this.hooks.storage.get(REFRESH_KEY);
289
- const isAnonymous = this.user?.isAnonymous === true;
290
- if (isAnonymous && refresh) {
314
+ if (refresh) {
291
315
  try {
292
316
  const data = await this.callAuth("/api/v1/auth-service/upgrade", {
293
317
  refreshToken: refresh,
@@ -301,11 +325,13 @@ var Auth = class {
301
325
  if (err instanceof AuthError && (err.code === "CONFLICT" || err.code === "EMAIL_ALREADY_TAKEN")) {
302
326
  throw new EmailAlreadyTakenError(err.message);
303
327
  }
304
- throw err;
328
+ if (err instanceof AuthError && (err.code === "FORBIDDEN_NON_ANONYMOUS" || err.code === "INVALID_REFRESH" || err.code === "UNAUTHORIZED" || err.code === "REFRESH_EXPIRED")) {
329
+ await this.wipeLocalIdentity();
330
+ } else {
331
+ throw err;
332
+ }
305
333
  }
306
334
  }
307
- const wasIdentified = this.user !== null && this.user.isAnonymous === false;
308
- if (wasIdentified) await this.wipeLocalIdentity();
309
335
  try {
310
336
  const data = await this.callAuth("/api/v1/auth-service/signup", {
311
337
  email,
@@ -685,7 +711,11 @@ var Auth = class {
685
711
  }
686
712
  // --- internals ---
687
713
  serialize(op) {
688
- const next = this.inflight.then(op, op);
714
+ const guarded = async () => {
715
+ await this.readyPromise;
716
+ return op();
717
+ };
718
+ const next = this.inflight.then(guarded, guarded);
689
719
  this.inflight = next.catch(() => void 0);
690
720
  return next;
691
721
  }
@@ -770,7 +800,7 @@ function decodeJwtPayload(token) {
770
800
  }
771
801
 
772
802
  // src/index.ts
773
- var SDK_VERSION = "0.10.1";
803
+ var SDK_VERSION = "0.10.3";
774
804
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
775
805
  var ANON_KEY = "anon_id";
776
806
  var USER_ID_KEY = "user_id";
package/dist/index.d.cts CHANGED
@@ -106,9 +106,26 @@ declare class Auth {
106
106
  private accessExpiresAt;
107
107
  private inflight;
108
108
  private refreshInflight;
109
+ /**
110
+ * Resolves when the parent SDK has finished its async init() —
111
+ * AsyncStorage hydrated + auth.hydrate() restored the cached
112
+ * session. All public auth methods await this internally so a
113
+ * customer who forgets `await sendora.init()` before showing the
114
+ * signup UI doesn't hit the cold-start race where the upgrade
115
+ * path's `storage.get(REFRESH_KEY)` returns null even though a
116
+ * valid anonymous session is sitting on disk.
117
+ *
118
+ * Industry-standard "SDK gates on internal readiness" pattern
119
+ * (Firebase Auth, Auth0, Clerk all do equivalent internal
120
+ * gating).
121
+ */
122
+ private readyPromise;
123
+ private resolveReady;
109
124
  constructor(hooks: AuthHooks);
110
125
  /** Re-hydrate session from storage. Called by the parent SDK
111
- * during init() after Storage.hydrate() has run. */
126
+ * during init() after Storage.hydrate() has run. Marks the
127
+ * internal ready promise as resolved so queued auth calls
128
+ * unblock. */
112
129
  hydrate(): void;
113
130
  getCurrentUser(): AuthUser | null;
114
131
  /**
@@ -123,6 +140,25 @@ declare class Auth {
123
140
  name?: string;
124
141
  metadata?: Record<string, unknown>;
125
142
  }): Promise<AuthUser>;
143
+ /**
144
+ * Create an email/password account.
145
+ *
146
+ * Industry-standard upgrade-first flow (matches Firebase
147
+ * `linkWithCredential`, Auth0 anonymous-merge, Supabase
148
+ * `linkIdentity`): when a refresh token is present in storage we
149
+ * try `/auth-service/upgrade` first — backend validates the
150
+ * refresh, checks the bound user is anonymous, and promotes in
151
+ * place preserving `user.id`. If the bound user is NOT anonymous
152
+ * (already identified, expired refresh, etc.), backend rejects
153
+ * with FORBIDDEN_NON_ANONYMOUS / INVALID_REFRESH and we fall back
154
+ * to a fresh /signup.
155
+ *
156
+ * Why we don't gate on the cached `this.user.isAnonymous`
157
+ * boolean: the in-memory user can be stale during cold start or
158
+ * fast-refresh races. The refresh token in storage is the
159
+ * authoritative signal — if it's there, the upgrade path is
160
+ * worth attempting; backend has the source of truth.
161
+ */
126
162
  signUp(email: string, password: string, opts?: {
127
163
  name?: string;
128
164
  metadata?: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -106,9 +106,26 @@ declare class Auth {
106
106
  private accessExpiresAt;
107
107
  private inflight;
108
108
  private refreshInflight;
109
+ /**
110
+ * Resolves when the parent SDK has finished its async init() —
111
+ * AsyncStorage hydrated + auth.hydrate() restored the cached
112
+ * session. All public auth methods await this internally so a
113
+ * customer who forgets `await sendora.init()` before showing the
114
+ * signup UI doesn't hit the cold-start race where the upgrade
115
+ * path's `storage.get(REFRESH_KEY)` returns null even though a
116
+ * valid anonymous session is sitting on disk.
117
+ *
118
+ * Industry-standard "SDK gates on internal readiness" pattern
119
+ * (Firebase Auth, Auth0, Clerk all do equivalent internal
120
+ * gating).
121
+ */
122
+ private readyPromise;
123
+ private resolveReady;
109
124
  constructor(hooks: AuthHooks);
110
125
  /** Re-hydrate session from storage. Called by the parent SDK
111
- * during init() after Storage.hydrate() has run. */
126
+ * during init() after Storage.hydrate() has run. Marks the
127
+ * internal ready promise as resolved so queued auth calls
128
+ * unblock. */
112
129
  hydrate(): void;
113
130
  getCurrentUser(): AuthUser | null;
114
131
  /**
@@ -123,6 +140,25 @@ declare class Auth {
123
140
  name?: string;
124
141
  metadata?: Record<string, unknown>;
125
142
  }): Promise<AuthUser>;
143
+ /**
144
+ * Create an email/password account.
145
+ *
146
+ * Industry-standard upgrade-first flow (matches Firebase
147
+ * `linkWithCredential`, Auth0 anonymous-merge, Supabase
148
+ * `linkIdentity`): when a refresh token is present in storage we
149
+ * try `/auth-service/upgrade` first — backend validates the
150
+ * refresh, checks the bound user is anonymous, and promotes in
151
+ * place preserving `user.id`. If the bound user is NOT anonymous
152
+ * (already identified, expired refresh, etc.), backend rejects
153
+ * with FORBIDDEN_NON_ANONYMOUS / INVALID_REFRESH and we fall back
154
+ * to a fresh /signup.
155
+ *
156
+ * Why we don't gate on the cached `this.user.isAnonymous`
157
+ * boolean: the in-memory user can be stale during cold start or
158
+ * fast-refresh races. The refresh token in storage is the
159
+ * authoritative signal — if it's there, the upgrade path is
160
+ * worth attempting; backend has the source of truth.
161
+ */
126
162
  signUp(email: string, password: string, opts?: {
127
163
  name?: string;
128
164
  metadata?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -191,9 +191,14 @@ var Auth = class {
191
191
  this.inflight = Promise.resolve();
192
192
  this.refreshInflight = null;
193
193
  this.hooks = hooks;
194
+ this.readyPromise = new Promise((res) => {
195
+ this.resolveReady = res;
196
+ });
194
197
  }
195
198
  /** Re-hydrate session from storage. Called by the parent SDK
196
- * during init() after Storage.hydrate() has run. */
199
+ * during init() after Storage.hydrate() has run. Marks the
200
+ * internal ready promise as resolved so queued auth calls
201
+ * unblock. */
197
202
  hydrate() {
198
203
  const cachedUser = this.hooks.storage.get(USER_KEY);
199
204
  const cachedToken = this.hooks.storage.get(TOKEN_KEY);
@@ -213,6 +218,7 @@ var Auth = class {
213
218
  this.hooks.storage.remove(REFRESH_KEY);
214
219
  }
215
220
  }
221
+ this.resolveReady();
216
222
  }
217
223
  getCurrentUser() {
218
224
  return this.user;
@@ -243,11 +249,29 @@ var Auth = class {
243
249
  return data.user;
244
250
  });
245
251
  }
252
+ /**
253
+ * Create an email/password account.
254
+ *
255
+ * Industry-standard upgrade-first flow (matches Firebase
256
+ * `linkWithCredential`, Auth0 anonymous-merge, Supabase
257
+ * `linkIdentity`): when a refresh token is present in storage we
258
+ * try `/auth-service/upgrade` first — backend validates the
259
+ * refresh, checks the bound user is anonymous, and promotes in
260
+ * place preserving `user.id`. If the bound user is NOT anonymous
261
+ * (already identified, expired refresh, etc.), backend rejects
262
+ * with FORBIDDEN_NON_ANONYMOUS / INVALID_REFRESH and we fall back
263
+ * to a fresh /signup.
264
+ *
265
+ * Why we don't gate on the cached `this.user.isAnonymous`
266
+ * boolean: the in-memory user can be stale during cold start or
267
+ * fast-refresh races. The refresh token in storage is the
268
+ * authoritative signal — if it's there, the upgrade path is
269
+ * worth attempting; backend has the source of truth.
270
+ */
246
271
  signUp(email, password, opts) {
247
272
  return this.serialize(async () => {
248
273
  const refresh = this.hooks.storage.get(REFRESH_KEY);
249
- const isAnonymous = this.user?.isAnonymous === true;
250
- if (isAnonymous && refresh) {
274
+ if (refresh) {
251
275
  try {
252
276
  const data = await this.callAuth("/api/v1/auth-service/upgrade", {
253
277
  refreshToken: refresh,
@@ -261,11 +285,13 @@ var Auth = class {
261
285
  if (err instanceof AuthError && (err.code === "CONFLICT" || err.code === "EMAIL_ALREADY_TAKEN")) {
262
286
  throw new EmailAlreadyTakenError(err.message);
263
287
  }
264
- throw err;
288
+ if (err instanceof AuthError && (err.code === "FORBIDDEN_NON_ANONYMOUS" || err.code === "INVALID_REFRESH" || err.code === "UNAUTHORIZED" || err.code === "REFRESH_EXPIRED")) {
289
+ await this.wipeLocalIdentity();
290
+ } else {
291
+ throw err;
292
+ }
265
293
  }
266
294
  }
267
- const wasIdentified = this.user !== null && this.user.isAnonymous === false;
268
- if (wasIdentified) await this.wipeLocalIdentity();
269
295
  try {
270
296
  const data = await this.callAuth("/api/v1/auth-service/signup", {
271
297
  email,
@@ -645,7 +671,11 @@ var Auth = class {
645
671
  }
646
672
  // --- internals ---
647
673
  serialize(op) {
648
- const next = this.inflight.then(op, op);
674
+ const guarded = async () => {
675
+ await this.readyPromise;
676
+ return op();
677
+ };
678
+ const next = this.inflight.then(guarded, guarded);
649
679
  this.inflight = next.catch(() => void 0);
650
680
  return next;
651
681
  }
@@ -730,7 +760,7 @@ function decodeJwtPayload(token) {
730
760
  }
731
761
 
732
762
  // src/index.ts
733
- var SDK_VERSION = "0.10.1";
763
+ var SDK_VERSION = "0.10.3";
734
764
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
735
765
  var ANON_KEY = "anon_id";
736
766
  var USER_ID_KEY = "user_id";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",