@koolbase/react-native 10.3.0 → 11.0.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/CHANGELOG.md CHANGED
@@ -7,6 +7,86 @@ adheres to [Semantic Versioning][semver].
7
7
  [kac]: https://keepachangelog.com/en/1.1.0/
8
8
  [semver]: https://semver.org/
9
9
 
10
+ ## 11.0.0
11
+
12
+ ### Read before upgrading
13
+
14
+ **`register()` returns a result, not a user.** One line changes in your app,
15
+ and the reason is a bug this fixes.
16
+
17
+ Registration succeeding and authentication succeeding are different outcomes.
18
+ A project with `require_verified_contact` enabled creates the account and
19
+ issues **no session** — the user verifies their email before their first
20
+ sign-in. The server has always answered that case correctly: 201 with
21
+ `verification_required`, deliberately not an error, because reporting failure
22
+ for a signup that worked is worse than either alternative.
23
+
24
+ The SDK ignored that field. It built a session from a response body with no
25
+ tokens in it, persisted it, and answered `currentUser` with a user whose every
26
+ authenticated request went out as `Bearer undefined` and came back 401 —
27
+ signed in as far as the app could tell, and unable to do anything. Silent, and
28
+ only in the configuration that requires verification.
29
+
30
+ ```ts
31
+ // Before
32
+ const user = await Koolbase.auth.register({ email, password });
33
+
34
+ // After
35
+ const result = await Koolbase.auth.register({ email, password });
36
+ switch (result.status) {
37
+ case 'authenticated':
38
+ // result.session is live, the user is signed in
39
+ break;
40
+ case 'verification_required':
41
+ // the account exists, result.session is null, they verify first
42
+ break;
43
+ }
44
+ ```
45
+
46
+ A discriminated union rather than a nullable session, so there is no path
47
+ where an app reads `result.user` and assumes it is signed in. That is how the
48
+ bug worked, and a nullable field would have allowed it at one remove.
49
+
50
+ ### Fixed
51
+
52
+ - **A session is never fabricated from a response without tokens.** Every
53
+ path that builds one — register, login, refresh, Google, Apple — now refuses
54
+ a body claiming authentication while omitting a token, throwing
55
+ `MalformedSessionResponseError`. Distinct from `verification_required`,
56
+ which is a legitimate session-less success: this is a protocol violation and
57
+ says so.
58
+
59
+ - **A pending signup no longer touches existing state.** It persists nothing,
60
+ fires no auth-state change, and leaves a session already on the device
61
+ alone — registering a second account does not sign out the first.
62
+
63
+ ### Migration
64
+
65
+ Assign the result, switch on `status`. If your project does not require
66
+ verified contact, the `authenticated` branch is the only one you will see —
67
+ but write both, because turning that setting on later should not break your
68
+ signup flow.
69
+
70
+ ## 10.4.0
71
+
72
+ ### Added
73
+
74
+ - **`auth.verifyEmail(token)`** — complete email verification with a token
75
+ from a verification link. The endpoint and the Flutter SDK have had this;
76
+ the TypeScript SDKs did not, so an app's verify-email page had nothing to
77
+ call.
78
+
79
+ - **`auth.resendVerificationEmail()`** — re-send the verification email to a
80
+ signed-in but unverified user. Returns `{ alreadyVerified, expiresAt,
81
+ cooldownUntil }`; an already-verified account is a no-op that says so
82
+ rather than an error, which is what a resend button needs when the user
83
+ verified in another tab.
84
+
85
+ The server throttles this, and the refusal now arrives typed:
86
+ `VerificationResendCooldownError` carries `cooldownUntil` so you can show a
87
+ countdown, and `VerificationResendDailyCapError` is separate because the
88
+ remedy differs — wait seconds versus wait until tomorrow.
89
+
10
90
  ## 10.3.0
11
91
 
12
92
  ### Fixed
package/dist/cjs/index.js CHANGED
@@ -35,6 +35,15 @@ let _flags = null;
35
35
  let _analytics = null;
36
36
  let _messaging = null;
37
37
  let _initialized = false;
38
+ // The in-flight initialize, so overlapping callers await the same one.
39
+ //
40
+ // A boolean guard is not enough: the check happens before the first await
41
+ // and the flag is set after the last, so two calls that overlap in that
42
+ // window both pass and both build a whole SDK — two of every client, two
43
+ // analytics flush timers, two sync engines over one queue. React strict
44
+ // mode does exactly this in development, and so does any app that
45
+ // initializes from two components.
46
+ let _initializing = null;
38
47
  function ensureInitialized() {
39
48
  if (!_initialized) {
40
49
  throw new Error('Koolbase not initialized. Call Koolbase.initialize() first.');
@@ -44,33 +53,45 @@ exports.Koolbase = {
44
53
  async initialize(config) {
45
54
  if (_initialized)
46
55
  return;
47
- // The host platform, installed before any subsystem touches storage or
48
- // the network. Tests leave this unset and run on the in-memory default.
49
- (0, core_1.setPlatform)(config.platform ?? (0, platform_js_2.reactNativePlatform)());
50
- _auth = new core_1.KoolbaseAuth(config);
51
- _db = new core_1.KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null),
52
- // A session the server refuses is not a session. Clearing it here means an
53
- // app catching KoolbaseUnauthenticatedError is already signed out and can
54
- // route to login, rather than looping on a dead token.
55
- async () => { await _auth?.clearStoredSession(); });
56
- _storage = new core_1.KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
57
- _realtime = new core_1.KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
58
- _functions = new core_1.KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
59
- // One anonymous device id for the whole SDK bucketing (flags), targeting
60
- // (code push), and registration keying (messaging) must all agree on it.
61
- const deviceId = await (0, core_1.getOrCreateDeviceId)();
62
- _flags = new core_1.KoolbaseFlags(config, deviceId);
63
- // Initialize analytics
64
- if (config.analyticsEnabled !== false) {
65
- _analytics = new core_1.KoolbaseAnalytics(config, () => _auth?.currentUser?.id ?? null);
66
- await _analytics.init(config.appVersion);
56
+ if (_initializing)
57
+ return _initializing;
58
+ _initializing = (async () => {
59
+ // The host platform, installed before any subsystem touches storage or
60
+ // the network. Tests leave this unset and run on the in-memory default.
61
+ (0, core_1.setPlatform)(config.platform ?? (0, platform_js_2.reactNativePlatform)());
62
+ _auth = new core_1.KoolbaseAuth(config);
63
+ _db = new core_1.KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null),
64
+ // A session the server refuses is not a session. Clearing it here means an
65
+ // app catching KoolbaseUnauthenticatedError is already signed out and can
66
+ // route to login, rather than looping on a dead token.
67
+ async () => { await _auth?.clearStoredSession(); });
68
+ _storage = new core_1.KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
69
+ _realtime = new core_1.KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
70
+ _functions = new core_1.KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
71
+ // One anonymous device id for the whole SDK — bucketing (flags), targeting
72
+ // (code push), and registration keying (messaging) must all agree on it.
73
+ const deviceId = await (0, core_1.getOrCreateDeviceId)();
74
+ _flags = new core_1.KoolbaseFlags(config, deviceId);
75
+ // Initialize analytics
76
+ if (config.analyticsEnabled !== false) {
77
+ _analytics = new core_1.KoolbaseAnalytics(config, () => _auth?.currentUser?.id ?? null);
78
+ await _analytics.init(config.appVersion);
79
+ }
80
+ // Initialize messaging
81
+ if (config.messagingEnabled !== false) {
82
+ _messaging = new core_1.KoolbaseMessaging(config);
83
+ _messaging.setDeviceId(deviceId);
84
+ }
85
+ _initialized = true;
86
+ })();
87
+ try {
88
+ await _initializing;
67
89
  }
68
- // Initialize messaging
69
- if (config.messagingEnabled !== false) {
70
- _messaging = new core_1.KoolbaseMessaging(config);
71
- _messaging.setDeviceId(deviceId);
90
+ finally {
91
+ // Cleared either way: a failed initialize must be retryable rather
92
+ // than leaving every later caller awaiting a rejected promise.
93
+ _initializing = null;
72
94
  }
73
- _initialized = true;
74
95
  },
75
96
  get auth() {
76
97
  ensureInitialized();
package/dist/esm/index.js CHANGED
@@ -17,6 +17,15 @@ let _flags = null;
17
17
  let _analytics = null;
18
18
  let _messaging = null;
19
19
  let _initialized = false;
20
+ // The in-flight initialize, so overlapping callers await the same one.
21
+ //
22
+ // A boolean guard is not enough: the check happens before the first await
23
+ // and the flag is set after the last, so two calls that overlap in that
24
+ // window both pass and both build a whole SDK — two of every client, two
25
+ // analytics flush timers, two sync engines over one queue. React strict
26
+ // mode does exactly this in development, and so does any app that
27
+ // initializes from two components.
28
+ let _initializing = null;
20
29
  function ensureInitialized() {
21
30
  if (!_initialized) {
22
31
  throw new Error('Koolbase not initialized. Call Koolbase.initialize() first.');
@@ -26,33 +35,45 @@ export const Koolbase = {
26
35
  async initialize(config) {
27
36
  if (_initialized)
28
37
  return;
29
- // The host platform, installed before any subsystem touches storage or
30
- // the network. Tests leave this unset and run on the in-memory default.
31
- setPlatform(config.platform ?? reactNativePlatform());
32
- _auth = new KoolbaseAuth(config);
33
- _db = new KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null),
34
- // A session the server refuses is not a session. Clearing it here means an
35
- // app catching KoolbaseUnauthenticatedError is already signed out and can
36
- // route to login, rather than looping on a dead token.
37
- async () => { await _auth?.clearStoredSession(); });
38
- _storage = new KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
39
- _realtime = new KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
40
- _functions = new KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
41
- // One anonymous device id for the whole SDK bucketing (flags), targeting
42
- // (code push), and registration keying (messaging) must all agree on it.
43
- const deviceId = await getOrCreateDeviceId();
44
- _flags = new KoolbaseFlags(config, deviceId);
45
- // Initialize analytics
46
- if (config.analyticsEnabled !== false) {
47
- _analytics = new KoolbaseAnalytics(config, () => _auth?.currentUser?.id ?? null);
48
- await _analytics.init(config.appVersion);
38
+ if (_initializing)
39
+ return _initializing;
40
+ _initializing = (async () => {
41
+ // The host platform, installed before any subsystem touches storage or
42
+ // the network. Tests leave this unset and run on the in-memory default.
43
+ setPlatform(config.platform ?? reactNativePlatform());
44
+ _auth = new KoolbaseAuth(config);
45
+ _db = new KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null),
46
+ // A session the server refuses is not a session. Clearing it here means an
47
+ // app catching KoolbaseUnauthenticatedError is already signed out and can
48
+ // route to login, rather than looping on a dead token.
49
+ async () => { await _auth?.clearStoredSession(); });
50
+ _storage = new KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
51
+ _realtime = new KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
52
+ _functions = new KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
53
+ // One anonymous device id for the whole SDK — bucketing (flags), targeting
54
+ // (code push), and registration keying (messaging) must all agree on it.
55
+ const deviceId = await getOrCreateDeviceId();
56
+ _flags = new KoolbaseFlags(config, deviceId);
57
+ // Initialize analytics
58
+ if (config.analyticsEnabled !== false) {
59
+ _analytics = new KoolbaseAnalytics(config, () => _auth?.currentUser?.id ?? null);
60
+ await _analytics.init(config.appVersion);
61
+ }
62
+ // Initialize messaging
63
+ if (config.messagingEnabled !== false) {
64
+ _messaging = new KoolbaseMessaging(config);
65
+ _messaging.setDeviceId(deviceId);
66
+ }
67
+ _initialized = true;
68
+ })();
69
+ try {
70
+ await _initializing;
49
71
  }
50
- // Initialize messaging
51
- if (config.messagingEnabled !== false) {
52
- _messaging = new KoolbaseMessaging(config);
53
- _messaging.setDeviceId(deviceId);
72
+ finally {
73
+ // Cleared either way: a failed initialize must be retryable rather
74
+ // than leaving every later caller awaiting a rejected promise.
75
+ _initializing = null;
54
76
  }
55
- _initialized = true;
56
77
  },
57
78
  get auth() {
58
79
  ensureInitialized();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koolbase/react-native",
3
- "version": "10.3.0",
3
+ "version": "11.0.0",
4
4
  "description": "React Native SDK for Koolbase \u2014 auth, database, storage, realtime, feature flags, and functions in one package.",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "types": "./dist/esm/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "url": "https://github.com/koolbase/koolbase-react-native"
25
25
  },
26
26
  "dependencies": {
27
- "@koolbase/core": "10.3.0"
27
+ "@koolbase/core": "11.0.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@react-native-async-storage/async-storage": "^3.0.2",