@layers/amba-react-native 4.0.2 → 4.0.4

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.d.ts CHANGED
@@ -9,9 +9,25 @@ interface AmbaConfig {
9
9
  * Defaults to `"https://api.amba.dev"`.
10
10
  */
11
11
  apiUrl?: string;
12
+ /**
13
+ * Base URL for `Amba.functions.invoke()`. Set this to call your deployed
14
+ * functions through a custom domain or a non-prod environment, e.g.
15
+ * `"https://api.example.com"`. `invoke(name, body)` POSTs to
16
+ * `"<functionsBaseUrl>/<name>"`. When omitted, the SDK resolves the default
17
+ * host automatically on first use (one cached round-trip).
18
+ */
19
+ functionsBaseUrl?: string;
12
20
  debug?: boolean;
13
21
  /** Override the AsyncStorage key prefix. Defaults to `"amba"`. */
14
22
  storagePrefix?: string;
23
+ /**
24
+ * Lazily establish an anonymous session on the first authenticated call so
25
+ * reads (xp/collections/etc.) work without an explicit
26
+ * `await Amba.auth.signInAnonymously()`. Defaults to `true`. Set to `false`
27
+ * to require an explicit sign-in (e.g. to control exactly when a billable
28
+ * anonymous user is created).
29
+ */
30
+ autoAnonymousSession?: boolean;
15
31
  }
16
32
  type SocialProvider = "apple" | "google";
17
33
  type PushPlatform = "apns" | "fcm" | "web";
@@ -122,15 +138,26 @@ interface PushToken {
122
138
  bundle_id?: string | null;
123
139
  created_at: string;
124
140
  }
125
- type EntitlementSource = "revenue_cat" | "app_store" | "play_store" | "stripe" | "promo_code" | "manual";
141
+ /** Store an entitlement's purchase resolved from. */
142
+ type EntitlementStore = "app_store" | "play_store" | "stripe";
143
+ /** Lifecycle phase of the subscription period backing the entitlement. */
144
+ type EntitlementPeriodType = "trial" | "intro" | "normal";
145
+ /**
146
+ * A server-evaluated entitlement grant, mirroring the
147
+ * `GET /v1/client/entitlements` response field-for-field. `store` and
148
+ * `period_type` are nullable, CHECK-constrained text columns, typed
149
+ * loosely (`… | (string & {}) | null`) so an unexpected value from a newer
150
+ * server never breaks decoding while still autocompleting the known set.
151
+ */
126
152
  interface UserEntitlement {
127
- id: string;
128
- name: string;
153
+ /** Project-defined entitlement key (e.g. `"pro"`) — what `has()` checks. */
154
+ entitlement_id: string;
129
155
  is_active: boolean;
130
- source: EntitlementSource;
131
- expires_at?: string | null;
132
- granted_at?: string | null;
133
- metadata: Record<string, unknown>;
156
+ product_id?: string | null;
157
+ store?: EntitlementStore | (string & {}) | null;
158
+ purchase_date?: string | null;
159
+ expiration_date?: string | null;
160
+ period_type?: EntitlementPeriodType | (string & {}) | null;
134
161
  }
135
162
  interface AiMessage {
136
163
  role: string;
@@ -218,17 +245,23 @@ interface ChallengeProgress {
218
245
  claimed_at?: string | null;
219
246
  }
220
247
  interface CurrencyBalance {
221
- currency_id: string;
222
- key: string;
248
+ currency_code: string;
249
+ name: string;
250
+ is_premium: boolean;
223
251
  balance: number;
224
- updated_at: string;
252
+ lifetime_earned: number;
253
+ lifetime_spent: number;
225
254
  }
226
255
  interface CurrencyTransaction {
227
256
  id: string;
228
- currency_id: string;
229
- delta: number;
257
+ app_user_id?: string;
258
+ currency_code: string;
259
+ amount: number;
230
260
  balance_after: number;
231
- reason?: string | null;
261
+ transaction_type?: string;
262
+ source?: string;
263
+ reference_id?: string | null;
264
+ idempotency_key?: string | null;
232
265
  created_at: string;
233
266
  }
234
267
  interface InventoryItem {
@@ -669,7 +702,8 @@ interface LeagueMembership {
669
702
  /**
670
703
  * Reply from `GET /v1/client/leagues/me/cohort`. The cohort with every
671
704
  * member's anonymised standing — `members[]` carries `display_name` /
672
- * `score` / `rank` only (no `user_id` leak).
705
+ * `score` / `rank` / `is_me` only (no `user_id` leak). `is_me` is a
706
+ * server-derived boolean so the caller can highlight their own row.
673
707
  */
674
708
  interface LeagueCohort {
675
709
  cohort: LeagueCohortRef | null;
@@ -678,8 +712,318 @@ interface LeagueCohort {
678
712
  display_name?: string | null;
679
713
  score: number;
680
714
  rank: number;
715
+ is_me?: boolean;
681
716
  }>;
682
717
  }
718
+ /**
719
+ * When a local notification should fire. Exactly one shape:
720
+ *
721
+ * - `{ at: Date }` — fire once at an absolute time.
722
+ * - `{ inSeconds, repeats? }` — fire after a relative delay; set
723
+ * `repeats: true` to re-fire on that interval (the OS enforces a
724
+ * minimum interval — typically ~60s on iOS for repeating triggers).
725
+ *
726
+ * Local notifications are scheduled and fired by the device itself, with
727
+ * no server involvement — distinct from remote push (`Amba.push.*`).
728
+ */
729
+ type LocalNotificationTrigger = {
730
+ at: Date | string;
731
+ } | {
732
+ inSeconds: number;
733
+ repeats?: boolean;
734
+ };
735
+ /** Input to `Amba.notifications.scheduleLocal`. */
736
+ interface ScheduleLocalNotificationInput {
737
+ /**
738
+ * Optional stable id. Supply your own to make scheduling idempotent
739
+ * and to control the value `cancelLocal` expects; omit to let the
740
+ * platform mint one (returned in the handle).
741
+ */
742
+ id?: string;
743
+ /** Notification title (required, non-empty). */
744
+ title: string;
745
+ /** Notification body text. */
746
+ body: string;
747
+ /** Arbitrary payload delivered to the app when the notification is tapped. */
748
+ data?: Record<string, unknown>;
749
+ /** When to fire it. */
750
+ trigger: LocalNotificationTrigger;
751
+ }
752
+ /** Handle returned by `scheduleLocal`; `id` is accepted by `cancelLocal`. */
753
+ interface LocalNotificationHandle {
754
+ id: string;
755
+ }
756
+ /** A pending local notification, as reported by `listScheduledLocal`. */
757
+ interface ScheduledLocalNotification {
758
+ id: string;
759
+ title: string;
760
+ body: string;
761
+ data?: Record<string, unknown>;
762
+ }
763
+ //#endregion
764
+ //#region src/resilience.d.ts
765
+ /**
766
+ * Offline mutation queue + optimistic-update helpers — framework-agnostic
767
+ * orchestration that sits ABOVE the collection / event write methods.
768
+ *
769
+ * Nothing here touches the SDK runtime directly: the queue is handed a
770
+ * small `KvStore` (durable key/value persistence) and a `WriteExecutor`
771
+ * (the function that actually performs a queued mutation against the API).
772
+ * That keeps the same code identical across every JS SDK target — only the
773
+ * adapters that supply storage + the executor differ per platform.
774
+ *
775
+ * Design:
776
+ * - A write that fails because the device is offline (a transport-level
777
+ * network error, or `navigator.onLine === false`) is appended to a
778
+ * durable outbox instead of throwing all the way up.
779
+ * - Each queued op carries a stable idempotency key. On replay the key is
780
+ * forwarded to the API's collection-write idempotency support so a
781
+ * mutation that actually reached the server before the connection
782
+ * dropped is never applied twice.
783
+ * - On reconnect (`online` event) the outbox is flushed in FIFO order;
784
+ * each op is removed only after it succeeds, so a mid-flush failure
785
+ * leaves the remainder intact for the next attempt.
786
+ */
787
+ /**
788
+ * Minimal durable key/value contract the outbox persists through. Both the
789
+ * browser `localStorage` adapter and the React Native AsyncStorage adapter
790
+ * satisfy this — the queue never assumes anything else.
791
+ */
792
+ interface KvStore {
793
+ get(key: string): Promise<string | null> | string | null;
794
+ set(key: string, value: string): Promise<void> | void;
795
+ delete(key: string): Promise<void> | void;
796
+ }
797
+ /** The kinds of mutation the outbox can durably replay. */
798
+ type OutboxOpKind = "collection.insert" | "collection.update" | "collection.delete" | "track";
799
+ /**
800
+ * A single durably-queued mutation. `idempotencyKey` is reused verbatim on
801
+ * every replay attempt so the server dedupes a COLLECTION write that landed
802
+ * before the connection dropped (the events `track` API has no idempotency
803
+ * channel, so `track` replays are at-least-once — a rare reconnect duplicate
804
+ * is acceptable for fire-and-forget analytics). `enqueuedAt` is an ISO
805
+ * timestamp for inspection.
806
+ */
807
+ interface OutboxOp {
808
+ /** Stable per-op id (the idempotency key for collection writes). */
809
+ idempotencyKey: string;
810
+ kind: OutboxOpKind;
811
+ /** Target collection name (absent for `track`). */
812
+ collection?: string;
813
+ /** Row id for update / delete. */
814
+ id?: string;
815
+ /** Insert row / update set payload. */
816
+ payload?: Record<string, unknown>;
817
+ /** Event name for `track`. */
818
+ event?: string;
819
+ /** Event properties for `track`. */
820
+ properties?: Record<string, unknown>;
821
+ /** Whether the tracked event was telemetry-tier. */
822
+ telemetry?: boolean;
823
+ /**
824
+ * ISO timestamp the `track` originally OCCURRED, captured at enqueue. Replayed
825
+ * as the events body `timestamp` (→ `occurred_at`) so a buffered event keeps
826
+ * its real time instead of being re-stamped at flush. (Attribution is by the
827
+ * session user at replay time — the events route has no body-id channel.)
828
+ */
829
+ occurredAt?: string;
830
+ enqueuedAt: string;
831
+ }
832
+ /** Result of a {@link OfflineQueue.flush}. */
833
+ interface FlushResult {
834
+ /** Ops successfully replayed and removed from the outbox. */
835
+ flushed: number;
836
+ /**
837
+ * Ops dropped because their replay failed deterministically (a coded server
838
+ * rejection like `VALIDATION_ERROR` / `NOT_FOUND` that would fail identically
839
+ * forever). Dropping them prevents one poisoned op from wedging the queue;
840
+ * the drain continues past it.
841
+ */
842
+ dropped: number;
843
+ /** Ops still pending after the flush (a replay failed and stopped it). */
844
+ remaining: number;
845
+ }
846
+ /**
847
+ * Apply a local change immediately, fire the real mutation, and roll the
848
+ * local change back if the mutation fails. Framework-agnostic: `apply` and
849
+ * `rollback` operate on whatever local view you hand them (a React state
850
+ * setter, a store, an in-memory cache).
851
+ *
852
+ * Resolves with the mutation's result on success. On failure it invokes
853
+ * `rollback` (with the value `apply` returned, so you can restore a captured
854
+ * snapshot) and rethrows the original error.
855
+ *
856
+ * @example
857
+ * await withOptimistic({
858
+ * apply: () => { const prev = todos; setTodos([...todos, draft]); return prev; },
859
+ * mutate: () => Amba.collections.insert("todos", draft),
860
+ * rollback: (prev) => setTodos(prev),
861
+ * });
862
+ */
863
+ declare function withOptimistic<TApply, TResult>(spec: {
864
+ /** Mutate the local view now. Return a snapshot for `rollback` to restore. */
865
+ apply: () => TApply;
866
+ /** The real async mutation against the API. */
867
+ mutate: () => Promise<TResult>;
868
+ /** Undo the local change. Receives whatever `apply` returned. */
869
+ rollback: (applied: TApply) => void;
870
+ }): Promise<TResult>;
871
+ //#endregion
872
+ //#region src/push-rotation.d.ts
873
+ /**
874
+ * A platform token-refresh subscription primitive. Given a handler that
875
+ * receives the new token string, it must invoke that handler on every rotation
876
+ * and return an unsubscribe function. `Notifications.addPushTokenListener`
877
+ * (Expo) adapts to this directly.
878
+ */
879
+ type TokenRefreshSubscribe = (handler: (token: string) => void) => () => void;
880
+ //#endregion
881
+ //#region src/realtime.d.ts
882
+ /**
883
+ * Live collection / messaging / gamification subscriptions for
884
+ * `@layers/amba-react-native`.
885
+ *
886
+ * Public surface is byte-for-byte identical to `@layers/amba-web` and
887
+ * `@layers/amba-node` so the docs are uniform across every JS SDK:
888
+ *
889
+ * const unsub = Amba.collection("messages")
890
+ * .where("circle_id", circleId)
891
+ * .subscribe((change) => {
892
+ * // change.op: "INSERT" | "UPDATE" | "DELETE"; change.row: the row
893
+ * });
894
+ * unsub(); // stop listening
895
+ *
896
+ * const unsub2 = Amba.messaging
897
+ * .conversation(conversationId)
898
+ * .subscribe((change) => { ... });
899
+ *
900
+ * const unsub3 = Amba.gamification.subscribe((change) => { ... });
901
+ *
902
+ * ── Why a different transport from web/node ─────────────────────────────
903
+ * The web/node SDKs read the streamed HTTP response body via
904
+ * `response.body.getReader()`. React Native's `fetch` (Hermes / RN's
905
+ * networking layer) resolves the Response only AFTER the body is fully
906
+ * received and does NOT expose a readable `body` stream — so the web
907
+ * transport silently never delivers a frame on a long-lived connection.
908
+ *
909
+ * Instead we drive the SAME server endpoint (an event stream) through
910
+ * `XMLHttpRequest`, which RN implements with INCREMENTAL `responseText`:
911
+ * `onreadystatechange` fires at `readyState === 3` (LOADING) as bytes
912
+ * arrive, so we parse the newly-appended slice frame-by-frame. This is the
913
+ * mechanism `react-native-sse` uses under the hood; we inline it to avoid a
914
+ * peer dependency (and to set the auth header in `Authorization`, keeping
915
+ * the session token out of the URL/query string where it would leak into
916
+ * access logs — `EventSource` can't set headers).
917
+ *
918
+ * If XHR is somehow unavailable in the host runtime, we fall back to the
919
+ * web-style `fetch` + streamed-body reader (works on RN-for-web / any
920
+ * runtime that does expose `response.body`). Both transports speak the
921
+ * exact same wire frames, so the public behaviour is identical.
922
+ *
923
+ * Reconnect/backoff and frame parsing match web/node exactly.
924
+ */
925
+ type RowChangeOp = "INSERT" | "UPDATE" | "DELETE";
926
+ interface RealtimeChange<T = unknown> {
927
+ /** Which kind of write happened. */
928
+ op: RowChangeOp;
929
+ /**
930
+ * The affected row. For deletes this is a partial row (at minimum `id`).
931
+ * If the row was too large to deliver inline, `truncated` is `true` and
932
+ * `row` carries just `id` — re-fetch with `Amba.collections.find()` /
933
+ * `findOne()` when you need the full body.
934
+ */
935
+ row: T;
936
+ truncated?: boolean;
937
+ }
938
+ interface SubscribeOptions {
939
+ /** Called once when the live connection is (re)established. */
940
+ onOpen?: () => void;
941
+ /** Called on a connection error before the SDK retries. */
942
+ onError?: (err: unknown) => void;
943
+ }
944
+ /** Unsubscribe handle returned by `.subscribe(...)`. Call it to stop. */
945
+ type Unsubscribe = () => void;
946
+ /**
947
+ * Auth + endpoint context for a subscription. `authHeaders` is read lazily
948
+ * at connect time so reconnects pick up a rotated session token. Returns
949
+ * `null` when there's no live session (subscribe throws).
950
+ */
951
+ interface RealtimeContext {
952
+ baseUrl: string;
953
+ authHeaders: () => Record<string, string> | null;
954
+ }
955
+ /**
956
+ * Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
957
+ * narrow, then `.subscribe(cb)` to start streaming.
958
+ */
959
+ declare class CollectionSubscription<T = unknown> {
960
+ private readonly ctx;
961
+ private readonly name;
962
+ private readonly filters;
963
+ constructor(ctx: RealtimeContext, name: string);
964
+ /** Add an equality filter. Chainable. All filters must match (AND). */
965
+ where(column: string, value: unknown): this;
966
+ /**
967
+ * Start listening. `cb` fires once per matching row change. Returns an
968
+ * unsubscribe handle — call it to close the connection.
969
+ */
970
+ subscribe(cb: (change: RealtimeChange<T>) => void, options?: SubscribeOptions): Unsubscribe;
971
+ }
972
+ /**
973
+ * A live message change in a conversation. `op` is "INSERT" for a new
974
+ * message; moderation removals arrive as "DELETE". For a delete (or when a
975
+ * message was too large to deliver inline) `row` may be partial — re-fetch
976
+ * with `Amba.messaging.listMessages(conversationId)` when you need the full
977
+ * body.
978
+ */
979
+ type MessageChange<T = unknown> = RealtimeChange<T>;
980
+ /**
981
+ * Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
982
+ * to receive new messages live instead of polling.
983
+ *
984
+ * Delivery is scoped to conversation participants — you receive messages for
985
+ * a conversation only if your session is a member of it. Reconnects with the
986
+ * same exponential backoff as collection subscriptions.
987
+ */
988
+ declare class ConversationSubscription<T = unknown> {
989
+ private readonly ctx;
990
+ private readonly conversationId;
991
+ constructor(ctx: RealtimeContext, conversationId: string);
992
+ subscribe(cb: (change: MessageChange<T>) => void, options?: SubscribeOptions): Unsubscribe;
993
+ }
994
+ /** Which slice of the signed-in user's gamification state changed. */
995
+ type GamificationKind = "xp" | "level" | "achievement" | "streak";
996
+ /**
997
+ * A live change to the signed-in user's gamification state. `kind` says which
998
+ * slice moved; `data` carries the new state inline so you can update without a
999
+ * re-fetch:
1000
+ * - `xp` — `{ total_xp, level, delta }`
1001
+ * - `level` — `{ level, total_xp }`
1002
+ * - `achievement` — `{ achievement_id, unlocked }`
1003
+ * - `streak` — `{ streak_id, count, status, ... }`
1004
+ *
1005
+ * If a payload was too large to deliver inline, `truncated` is `true` and `data`
1006
+ * is empty — re-fetch with `Amba.xp.getBalance()` / `Amba.streaks.getAll()` etc.
1007
+ */
1008
+ interface GamificationChange<T = Record<string, unknown>> {
1009
+ kind: GamificationKind;
1010
+ data: T;
1011
+ truncated?: boolean;
1012
+ }
1013
+ /**
1014
+ * Builder returned by `Amba.gamification`. Call `.subscribe(cb)` to receive the
1015
+ * signed-in user's xp / level / achievement / streak changes live instead of
1016
+ * polling.
1017
+ *
1018
+ * Delivery is scoped to the signed-in user — you only ever receive your own
1019
+ * gamification changes, never another user's. Reconnects with the same
1020
+ * exponential backoff as collection subscriptions.
1021
+ */
1022
+ declare class GamificationSubscription {
1023
+ private readonly ctx;
1024
+ constructor(ctx: RealtimeContext);
1025
+ subscribe(cb: (change: GamificationChange) => void, options?: SubscribeOptions): Unsubscribe;
1026
+ }
683
1027
  //#endregion
684
1028
  //#region src/error.d.ts
685
1029
  /**
@@ -699,16 +1043,70 @@ interface LeagueCohort {
699
1043
  * `new AmbaApiError("MY_CODE", "...")` without coordinating with this
700
1044
  * package's TypeScript types.
701
1045
  */
702
- type AmbaApiErrorCode = "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "VALIDATION_ERROR" | "NETWORK_ERROR" | "HTTP_ERROR" | "CIRCUIT_OPEN" | "CONSENT_NOT_GRANTED" | "NOT_INITIALIZED" | "INVALID_CONFIG" | "INVALID_ARGUMENT" | "PUSH_PERMISSION_DENIED" | "PUSH_REGISTRATION_FAILED" | "NOT_SANDBOX_TIER" | "UNKNOWN_ERROR" | (string & {});
1046
+ type AmbaApiErrorCode = "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "VALIDATION_ERROR" | "NETWORK_ERROR" | "HTTP_ERROR" | "CIRCUIT_OPEN" | "CONSENT_NOT_GRANTED" | "NOT_INITIALIZED" | "INVALID_CONFIG" | "INVALID_ARGUMENT" | "PUSH_PERMISSION_DENIED" | "PUSH_REGISTRATION_FAILED" | "LOCAL_NOTIFICATIONS_UNAVAILABLE" | "LOCAL_NOTIFICATIONS_PERMISSION_DENIED" | "NOT_SANDBOX_TIER" | "UNKNOWN_ERROR" | (string & {});
703
1047
  declare class AmbaApiError extends Error {
704
1048
  readonly code: AmbaApiErrorCode;
705
1049
  readonly details?: unknown;
706
1050
  constructor(code: AmbaApiErrorCode, message: string, details?: unknown);
707
1051
  }
708
1052
  //#endregion
1053
+ //#region src/local-notifications.d.ts
1054
+ /**
1055
+ * The `notifications` namespace. Shared by `@layers/amba-react-native`
1056
+ * and (via re-export) `@layers/amba-expo`.
1057
+ */
1058
+ declare const notifications: {
1059
+ /**
1060
+ * Schedule a notification the device will fire locally — no server
1061
+ * round-trip. Requests the OS notification permission on first use.
1062
+ *
1063
+ * @returns a handle whose `id` can be passed to `cancelLocal`.
1064
+ * @throws `AmbaApiError`:
1065
+ * - `LOCAL_NOTIFICATIONS_UNAVAILABLE` — `expo-notifications` not installed.
1066
+ * - `LOCAL_NOTIFICATIONS_PERMISSION_DENIED` — user declined the prompt.
1067
+ * - `INVALID_ARGUMENT` — bad title / body / trigger.
1068
+ *
1069
+ * @example
1070
+ * const { id } = await Amba.notifications.scheduleLocal({
1071
+ * title: 'Keep your streak alive',
1072
+ * body: 'You have a lesson waiting.',
1073
+ * trigger: { inSeconds: 60 * 60 * 12 },
1074
+ * });
1075
+ */
1076
+ scheduleLocal(input: ScheduleLocalNotificationInput): Promise<LocalNotificationHandle>;
1077
+ /**
1078
+ * Cancel a previously scheduled local notification by id. No-throw if
1079
+ * the id is unknown (the device framework treats it as already-gone),
1080
+ * but still throws `LOCAL_NOTIFICATIONS_UNAVAILABLE` if the peer dep is
1081
+ * absent so callers learn about the missing integration.
1082
+ */
1083
+ cancelLocal(id: string): Promise<void>;
1084
+ /**
1085
+ * List the notifications Amba has scheduled to fire on this device.
1086
+ * Returns an empty array when none are pending.
1087
+ *
1088
+ * `getAllScheduledNotificationsAsync` returns EVERY pending
1089
+ * `expo-notifications` schedule in the app — including ones the host app
1090
+ * created directly. We filter to only entries carrying the Amba marker
1091
+ * (`data.__ambaLocalId`) so foreign schedules never leak into the Amba
1092
+ * list. (Mirrors the web SDK's marker filter.) These are pending by
1093
+ * definition — `getAllScheduledNotificationsAsync` does not return
1094
+ * already-delivered notifications.
1095
+ */
1096
+ listScheduledLocal(): Promise<ScheduledLocalNotification[]>;
1097
+ };
1098
+ type LocalNotificationsNamespace = typeof notifications;
1099
+ //#endregion
709
1100
  //#region src/index.d.ts
710
1101
  /** SDK semver — burned in at build time. Bump alongside package.json. */
711
- declare const SDK_VERSION = "4.0.2";
1102
+ declare const SDK_VERSION = "4.0.4";
1103
+ /** Options for `Amba.functions.invoke`. */
1104
+ interface InvokeOptions {
1105
+ /** HTTP method. Defaults to `"POST"`. */
1106
+ method?: "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
1107
+ /** Extra headers merged on top of the injected auth headers. */
1108
+ headers?: Record<string, string>;
1109
+ }
712
1110
  /**
713
1111
  * Top-level SDK namespace for React Native.
714
1112
  */
@@ -726,6 +1124,37 @@ declare const Amba: {
726
1124
  readonly isAuthenticated: boolean;
727
1125
  readonly projectId: string;
728
1126
  setDebug: (enabled: boolean) => void;
1127
+ /**
1128
+ * Live collection subscriptions. Returns a builder — chain `.where(...)`
1129
+ * to narrow, then `.subscribe(cb)` to start streaming row changes:
1130
+ *
1131
+ * const unsub = Amba.collection("messages")
1132
+ * .where("circle_id", circleId)
1133
+ * .subscribe((change) => {
1134
+ * // change.op: "INSERT" | "UPDATE" | "DELETE"; change.row: the row
1135
+ * });
1136
+ * unsub(); // stop listening
1137
+ *
1138
+ * Delivery mirrors `Amba.collections.find()` — you receive only rows your
1139
+ * session owns (plus shared/public rows), optionally narrowed by
1140
+ * `.where(...)`. Reconnects automatically with backoff if the connection
1141
+ * drops. Requires an authenticated session.
1142
+ */
1143
+ collection: <T = unknown>(name: string) => CollectionSubscription<T>;
1144
+ /**
1145
+ * Live gamification updates for the signed-in user. Call `.subscribe(cb)`
1146
+ * to receive xp / level / achievement / streak changes live:
1147
+ *
1148
+ * const unsub = Amba.gamification.subscribe((change) => {
1149
+ * // change.kind: "xp" | "level" | "achievement" | "streak"
1150
+ * // change.data: the new state
1151
+ * });
1152
+ * unsub(); // stop listening
1153
+ *
1154
+ * You only ever receive your own gamification changes — never another
1155
+ * user's. A getter so `baseUrl` is read at access time. Requires a session.
1156
+ */
1157
+ readonly gamification: GamificationSubscription;
729
1158
  events: {
730
1159
  /**
731
1160
  * Track an event.
@@ -826,6 +1255,16 @@ declare const Amba: {
826
1255
  linkSmsOtp: (phone: string, code: string) => Promise<User>;
827
1256
  /** Symmetric counterpart to `linkSmsOtp` for email. */
828
1257
  linkEmailOtp: (email: string, code: string) => Promise<User>;
1258
+ /**
1259
+ * Upgrade an anonymous (or any current) session by attaching an
1260
+ * email + password credential — converting the current user in place
1261
+ * to an email-identified account so its data carries over (same
1262
+ * `user_id`, same session token, no rotation). Server returns
1263
+ * `EMAIL_ALREADY_LINKED` (409) with `details.conflicting_user_id` if
1264
+ * the email is owned by another user — rethrown as `AmbaApiError`.
1265
+ * After linking, the user can sign in via `signInWithEmail`.
1266
+ */
1267
+ linkEmailPassword: (email: string, password: string) => Promise<User>;
829
1268
  /**
830
1269
  * Detach the SMS-OTP identifier from the current session's user.
831
1270
  * Refused with `LAST_IDENTIFIER` if it would orphan the account.
@@ -916,6 +1355,44 @@ declare const Amba: {
916
1355
  checkpoint_token?: string;
917
1356
  }) => Promise<SyncPullResult>;
918
1357
  };
1358
+ /**
1359
+ * Automatic offline mutation queue (opt-in).
1360
+ *
1361
+ * Once enabled, a collection write (`insert` / `update` / `delete`) or a
1362
+ * `track` that fails because the device is offline is buffered to a durable
1363
+ * on-device outbox (AsyncStorage) instead of throwing — then replayed in
1364
+ * order on the next {@link flush} (e.g. when your NetInfo listener detects
1365
+ * reconnect). Replays reuse a per-op idempotency key so a write that reached
1366
+ * the server before the connection dropped is never applied twice.
1367
+ *
1368
+ * This is distinct from `Amba.sync` (manual change replay): `offline` is the
1369
+ * automatic, fire-and-forget safety net for ordinary writes.
1370
+ *
1371
+ * @example
1372
+ * import NetInfo from "@react-native-community/netinfo";
1373
+ * Amba.offline.enable();
1374
+ * NetInfo.addEventListener((s) => { if (s.isConnected) Amba.offline.flush(); });
1375
+ * await Amba.collections.insert("todos", { title: "Buy milk" }); // queues if offline
1376
+ */
1377
+ offline: {
1378
+ /**
1379
+ * Turn the outbox on. Idempotent. Returns a disable function (queued ops
1380
+ * are NOT dropped — call `clear()` for that). Drive `flush()` from your own
1381
+ * connectivity listener (e.g. NetInfo) since React Native has no global
1382
+ * `online` event.
1383
+ */
1384
+ enable: () => (() => void);
1385
+ /** Whether the outbox is currently enabled. */
1386
+ isEnabled: () => boolean;
1387
+ /** Count of writes awaiting replay. */
1388
+ pendingCount: () => Promise<number>;
1389
+ /** Snapshot the pending ops (a copy — safe to inspect). */
1390
+ pending: () => Promise<OutboxOp[]>;
1391
+ /** Force a replay attempt now. Resolves with `{ flushed, dropped, remaining }` (dropped = poisoned ops the server rejects on replay, removed so they cannot wedge the queue). */
1392
+ flush: () => Promise<FlushResult>;
1393
+ /** Drop every queued write without replaying. */
1394
+ clear: () => Promise<void>;
1395
+ };
919
1396
  collections: {
920
1397
  find: <T = unknown>(name: string, options?: FindOptions) => Promise<FindResponse<T>>;
921
1398
  findOne: <T = unknown>(name: string, id: string) => Promise<T>;
@@ -952,6 +1429,36 @@ declare const Amba: {
952
1429
  deleted: boolean;
953
1430
  };
954
1431
  }>;
1432
+ /**
1433
+ * Optimistic insert — update a local view immediately, fire the real
1434
+ * insert, and roll the view back if it fails. Thin convenience over the
1435
+ * exported {@link withOptimistic}; for multi-step flows use that directly.
1436
+ *
1437
+ * Optimistic writes always hit the network directly — they are NOT diverted
1438
+ * to the offline outbox even when `Amba.offline` is enabled. Optimistic UI
1439
+ * needs an unambiguous outcome: this resolves only on a confirmed write and
1440
+ * rejects (running your `rollback`) on any failure including being offline.
1441
+ * To buffer-and-replay instead, use the plain `insert`/`update`/`delete`
1442
+ * with the outbox on.
1443
+ */
1444
+ optimisticInsert: <T = unknown, TApply = unknown>(name: string, row: Record<string, unknown>, view: {
1445
+ apply: () => TApply;
1446
+ rollback: (applied: TApply) => void;
1447
+ }) => Promise<T>;
1448
+ /** Optimistic update — see {@link optimisticInsert}. */
1449
+ optimisticUpdate: <T = unknown, TApply = unknown>(name: string, id: string, set: Record<string, unknown>, view: {
1450
+ apply: () => TApply;
1451
+ rollback: (applied: TApply) => void;
1452
+ }) => Promise<T>;
1453
+ /** Optimistic delete — see {@link optimisticInsert}. */
1454
+ optimisticDelete: <TApply = unknown>(name: string, id: string, view: {
1455
+ apply: () => TApply;
1456
+ rollback: (applied: TApply) => void;
1457
+ }) => Promise<{
1458
+ data: {
1459
+ deleted: boolean;
1460
+ };
1461
+ }>;
955
1462
  where: {
956
1463
  eq: (column: string, value: FilterValue) => Filter;
957
1464
  ne: (column: string, value: FilterValue) => Filter;
@@ -1025,10 +1532,50 @@ declare const Amba: {
1025
1532
  getTokens: () => Promise<PushToken[]>;
1026
1533
  subscribe: (topic: string) => Promise<void>;
1027
1534
  unsubscribe: (topic: string) => Promise<void>;
1535
+ /**
1536
+ * Auto re-register the device push token when the OS rotates it.
1537
+ *
1538
+ * The OS can roll a device's push token (reinstall, restore, key roll);
1539
+ * the new token must be re-registered or notifications stop arriving. Pass
1540
+ * your notification library's token-refresh subscription primitive — e.g.
1541
+ * `(h) => Notifications.addPushTokenListener((t) => h(t.data)).remove` on
1542
+ * Expo, or the bare-FCM `onTokenRefresh` callback — and the SDK forwards
1543
+ * each rotated token through the idempotent {@link register} path.
1544
+ *
1545
+ * Pass the token `platform` for your build — `"apns"` on iOS, `"fcm"` on
1546
+ * Android (defaults to `"fcm"` when omitted). The bare RN SDK never reads
1547
+ * `Platform.OS` itself (it's a pure-JS client that also loads under Node),
1548
+ * so the caller supplies it; the Expo wrapper's
1549
+ * `Amba.enablePushAutoRotation()` resolves it for you. Returns an
1550
+ * unsubscribe function.
1551
+ *
1552
+ * @example
1553
+ * import { Platform } from "react-native";
1554
+ * import * as Notifications from "expo-notifications";
1555
+ * const detach = Amba.push.enableAutoRotation(
1556
+ * (h) => Notifications.addPushTokenListener((t) => h(t.data)).remove,
1557
+ * Platform.OS === "ios" ? "apns" : "fcm",
1558
+ * );
1559
+ */
1560
+ enableAutoRotation: (subscribe: TokenRefreshSubscribe, platform?: "apns" | "fcm") => (() => void);
1561
+ };
1562
+ /**
1563
+ * On-device local notifications — `scheduleLocal` / `cancelLocal` /
1564
+ * `listScheduledLocal`. These schedule notifications the device fires
1565
+ * itself (reminders, streak nudges) with no server round-trip, distinct
1566
+ * from remote push (`Amba.push.*`). Backed by `expo-notifications`
1567
+ * (an optional peer dep that works in both Expo and bare RN apps),
1568
+ * loaded lazily; methods throw a clear `LOCAL_NOTIFICATIONS_UNAVAILABLE`
1569
+ * error if it isn't installed. See `./local-notifications.ts`.
1570
+ */
1571
+ notifications: {
1572
+ scheduleLocal(input: ScheduleLocalNotificationInput): Promise<LocalNotificationHandle>;
1573
+ cancelLocal(id: string): Promise<void>;
1574
+ listScheduledLocal(): Promise<ScheduledLocalNotification[]>;
1028
1575
  };
1029
1576
  entitlements: {
1030
1577
  list: () => Promise<UserEntitlement[]>;
1031
- has: (name: string) => Promise<boolean>;
1578
+ has: (entitlementId: string) => Promise<boolean>;
1032
1579
  };
1033
1580
  ai: {
1034
1581
  anthropic: {
@@ -1037,6 +1584,24 @@ declare const Amba: {
1037
1584
  };
1038
1585
  };
1039
1586
  };
1587
+ /**
1588
+ * Invoke a deployed function by name, with the project's client key (and
1589
+ * the live session token, if any) injected automatically. Prefer this over
1590
+ * hand-rolling `fetch` against a function URL, which leaves the call
1591
+ * unauthenticated.
1592
+ *
1593
+ * @example
1594
+ * const result = await Amba.functions.invoke<{ ok: boolean }>(
1595
+ * 'send_welcome_email',
1596
+ * { to: 'user@example.com' },
1597
+ * );
1598
+ *
1599
+ * @returns The parsed JSON response (or the raw text if not JSON).
1600
+ * @throws AmbaApiError on a network failure or a non-2xx response.
1601
+ */
1602
+ functions: {
1603
+ invoke: <T = unknown>(name: string, body?: unknown, options?: InvokeOptions) => Promise<T>;
1604
+ };
1040
1605
  config: {
1041
1606
  fetch: () => Promise<ConfigBundle>;
1042
1607
  };
@@ -1169,6 +1734,22 @@ declare const Amba: {
1169
1734
  * The real route is `/v1/client/messaging/conversations/:id/messages`.
1170
1735
  */
1171
1736
  messaging: {
1737
+ /**
1738
+ * Live conversation subscription. Returns a builder — call
1739
+ * `.subscribe(cb)` to receive new messages live instead of polling:
1740
+ *
1741
+ * const unsub = Amba.messaging
1742
+ * .conversation(conversationId)
1743
+ * .subscribe((change) => {
1744
+ * // change.op === "INSERT"; change.row is the new message
1745
+ * });
1746
+ * unsub(); // stop listening
1747
+ *
1748
+ * Delivery is scoped to conversation participants — you receive messages
1749
+ * only if your session is a member of the conversation. Reconnects
1750
+ * automatically with backoff. Requires an authenticated session.
1751
+ */
1752
+ conversation: <T = Message>(conversationId: string) => ConversationSubscription<T>;
1172
1753
  /**
1173
1754
  * `POST /v1/client/messaging/conversations` — create (or fetch
1174
1755
  * the existing direct) conversation. Idempotent for direct
@@ -1278,5 +1859,5 @@ declare const Amba: {
1278
1859
  };
1279
1860
  };
1280
1861
  //#endregion
1281
- export { Achievement, AchievementProgress, AiMessage, AiMessageRequest, AiMessageResponse, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AndFilter, AuthResult, CatalogItem, Challenge, ChallengeProgress, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DiagnosticsEnvironment, EntitlementSource, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, Friendship, FriendshipState, Group, GroupCreate, GroupMember, GroupUpdate, InventoryItem, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortRef, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, MarkReadResult, MediaAsset, Message, MessageType, NotFilter, OnboardingStatus, OrFilter, OrderBy, PingResult, PresignData, PurchaseOption, PurchaseRequest, PurchaseResult, PushPlatform, PushToken, ReferralClaim, ReferralCode, Report, ReportRequest, Review, ReviewCreate, ReviewUpdate, Role, SDK_VERSION, SendMessageRequest, Session, SessionRecord, SocialProvider, Store, Streak, SyncChange, SyncConflict, SyncPullResult, SyncPullSince, SyncPushResult, User, UserEntitlement, UserPatch, XpBalance, XpTransaction };
1862
+ export { Achievement, AchievementProgress, AiMessage, AiMessageRequest, AiMessageResponse, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AndFilter, AuthResult, CatalogItem, Challenge, ChallengeProgress, CollectionSubscription, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationSubscription, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DiagnosticsEnvironment, EntitlementPeriodType, EntitlementStore, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, type FlushResult, Friendship, FriendshipState, type GamificationChange, type GamificationKind, GamificationSubscription, Group, GroupCreate, GroupMember, GroupUpdate, InventoryItem, InvokeOptions, type KvStore, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortRef, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, LocalNotificationHandle, LocalNotificationTrigger, type LocalNotificationsNamespace, MarkReadResult, MediaAsset, Message, type MessageChange, MessageType, NotFilter, OnboardingStatus, OrFilter, OrderBy, type OutboxOp, type OutboxOpKind, PingResult, PresignData, PurchaseOption, PurchaseRequest, PurchaseResult, PushPlatform, PushToken, type RealtimeChange, ReferralClaim, ReferralCode, Report, ReportRequest, Review, ReviewCreate, ReviewUpdate, Role, type RowChangeOp, SDK_VERSION, ScheduleLocalNotificationInput, ScheduledLocalNotification, SendMessageRequest, Session, SessionRecord, SocialProvider, Store, Streak, type SubscribeOptions, SyncChange, SyncConflict, SyncPullResult, SyncPullSince, SyncPushResult, type TokenRefreshSubscribe, type Unsubscribe, User, UserEntitlement, UserPatch, XpBalance, XpTransaction, withOptimistic };
1282
1863
  //# sourceMappingURL=index.d.ts.map