@layers/amba-react-native 3.0.2 → 4.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/dist/index.js CHANGED
@@ -1,12 +1,38 @@
1
1
  //#region src/async-storage-adapter.ts
2
- let cachedStorage = null;
2
+ let cachedStorage = void 0;
3
+ /**
4
+ * Late-imports `@react-native-async-storage/async-storage` and returns
5
+ * it ONLY if the resolved module exposes the three methods we use
6
+ * (`getItem` / `setItem` / `removeItem`). Otherwise returns null and
7
+ * the persistence layer falls back to memory-only.
8
+ *
9
+ * Why the shape check: pnpm with auto-install-peers (default since
10
+ * pnpm 7) installs declared peer deps even in non-RN scratch
11
+ * directories (e.g., this SDK's docs-smoke harness). The installed
12
+ * package's Node entry point doesn't expose AsyncStorage's methods
13
+ * — it's a React Native native module — so the `await import(...)`
14
+ * succeeds (no catch fires) but the returned object has no
15
+ * `getItem`. Calling it throws `TypeError: storage.getItem is not a
16
+ * function` from inside `read()` (verified at PR #246 docs-react-native
17
+ * / docs-expo run on 547a1ff4). Checking method presence here turns
18
+ * that into the same fall-through as "peer dep not installed at all."
19
+ */
20
+ function isAsyncStorageLike(mod) {
21
+ return typeof mod === "object" && mod !== null && typeof mod.getItem === "function" && typeof mod.setItem === "function" && typeof mod.removeItem === "function";
22
+ }
3
23
  async function getStorage() {
4
- if (cachedStorage !== null) return cachedStorage;
24
+ if (cachedStorage !== void 0) return cachedStorage;
5
25
  try {
6
26
  const mod = await import("@react-native-async-storage/async-storage");
7
- cachedStorage = mod.default ?? mod;
27
+ const candidate = mod.default ?? mod;
28
+ if (!isAsyncStorageLike(candidate)) {
29
+ cachedStorage = null;
30
+ return null;
31
+ }
32
+ cachedStorage = candidate;
8
33
  return cachedStorage;
9
34
  } catch {
35
+ cachedStorage = null;
10
36
  return null;
11
37
  }
12
38
  }
@@ -324,7 +350,7 @@ function freshSeed() {
324
350
  //#endregion
325
351
  //#region src/index.ts
326
352
  /** SDK semver — burned in at build time. Bump alongside package.json. */
327
- const SDK_VERSION = "3.0.0";
353
+ const SDK_VERSION = "4.0.0";
328
354
  let state = null;
329
355
  function getState() {
330
356
  if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start.");
@@ -376,15 +402,34 @@ function requireEmail(email) {
376
402
  function requirePassword(password) {
377
403
  if (password.length < 8) throw new AmbaApiError("VALIDATION_ERROR", "password must be at least 8 characters");
378
404
  }
405
+ function requireOtpCode(code) {
406
+ if (code.length !== 6 || !/^[0-9]{6}$/.test(code)) throw new AmbaApiError("VALIDATION_ERROR", "code must be 6 digits");
407
+ }
408
+ const E164_RE = /^\+[1-9][0-9]{6,14}$/;
409
+ function requireE164Phone(phone) {
410
+ if (!E164_RE.test(phone)) throw new AmbaApiError("VALIDATION_ERROR", "phone must be E.164 (starts with +, 8–15 digits)");
411
+ }
379
412
  const P = {
380
413
  authAnonymous: "/v1/client/auth/anonymous",
381
414
  authEmailLogin: "/v1/client/auth/email/login",
382
415
  authEmailSignup: "/v1/client/auth/email/signup",
416
+ authEmailOtpRequest: "/v1/client/auth/email-otp/request",
417
+ authSmsOtpRequest: "/v1/client/auth/sms-otp/request",
418
+ authSmsOtpVerify: "/v1/client/auth/sms-otp/verify",
419
+ authEmailOtpVerify: "/v1/client/auth/email-otp/verify",
383
420
  authSocial: "/v1/client/auth/social",
421
+ authMagicLinkRequest: "/v1/client/auth/magic-link/request",
422
+ authMagicLinkVerify: "/v1/client/auth/magic-link/verify",
423
+ authLink: "/v1/client/auth/link",
384
424
  authRefresh: "/v1/client/auth/refresh",
385
425
  authLogout: "/v1/client/auth/logout",
386
426
  authMe: "/v1/client/auth/me",
387
427
  events: "/v1/client/events",
428
+ users: "/v1/client/users",
429
+ usersMe: "/v1/client/users/me",
430
+ sessions: "/v1/client/sessions",
431
+ sync: "/v1/client/sync",
432
+ leagues: "/v1/client/leagues",
388
433
  collections: "/v1/client/collections",
389
434
  storage: "/v1/client/media",
390
435
  storagePresign: "/v1/client/media/presign",
@@ -399,6 +444,7 @@ const P = {
399
444
  aiAnthropic: "/v1/client/ai/anthropic/messages",
400
445
  config: "/v1/client/config",
401
446
  flags: "/v1/client/flags",
447
+ diagnosticsPing: "/v1/client/diagnostics/ping",
402
448
  achievements: "/v1/client/achievements",
403
449
  challenges: "/v1/client/challenges",
404
450
  catalog: "/v1/client/catalog",
@@ -417,7 +463,7 @@ const P = {
417
463
  roles: "/v1/client/roles",
418
464
  referrals: "/v1/client/referrals",
419
465
  content: "/v1/client/content",
420
- deepLinks: "/v1/client/deeplinks",
466
+ deepLinks: "/v1/client/deep-links",
421
467
  onboarding: "/v1/client/onboarding"
422
468
  };
423
469
  function encodePathSegment(s) {
@@ -467,14 +513,16 @@ const Amba = {
467
513
  setDebug: (enabled) => {
468
514
  getState().debug = enabled;
469
515
  },
470
- events: { track: async (event, properties) => {
516
+ events: { track: async (event, properties, options) => {
471
517
  const s = getState();
472
- await s.http.post(P.events, {
473
- name: event,
518
+ const body = {
519
+ event,
474
520
  properties: properties ?? void 0,
475
521
  anonymous_id: s.identity.getAnonymousId(),
476
522
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
477
- });
523
+ };
524
+ if (options?.telemetry) body["telemetry"] = true;
525
+ await s.http.post(P.events, body);
478
526
  } },
479
527
  auth: {
480
528
  signInAnonymously: async () => {
@@ -497,13 +545,65 @@ const Amba = {
497
545
  password
498
546
  });
499
547
  },
500
- signInWithSocial: async (provider, idToken) => {
548
+ signInWithApple: async (identityToken) => {
549
+ if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
550
+ return postAuth(getState(), P.authSocial, {
551
+ provider: "apple",
552
+ id_token: identityToken
553
+ });
554
+ },
555
+ signInWithGoogle: async (idToken) => {
501
556
  if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
502
557
  return postAuth(getState(), P.authSocial, {
503
- provider,
558
+ provider: "google",
504
559
  id_token: idToken
505
560
  });
506
561
  },
562
+ requestEmailOtp: async (email) => {
563
+ requireEmail(email);
564
+ await getState().http.post(P.authEmailOtpRequest, { email });
565
+ },
566
+ verifyEmailOtp: async (email, code) => {
567
+ requireEmail(email);
568
+ requireOtpCode(code);
569
+ return postAuth(getState(), P.authEmailOtpVerify, {
570
+ email,
571
+ code
572
+ });
573
+ },
574
+ requestSmsOtp: async (phone) => {
575
+ requireE164Phone(phone);
576
+ await getState().http.post(P.authSmsOtpRequest, { phone });
577
+ },
578
+ verifySmsOtp: async (phone, code) => {
579
+ requireE164Phone(phone);
580
+ requireOtpCode(code);
581
+ return postAuth(getState(), P.authSmsOtpVerify, {
582
+ phone,
583
+ code
584
+ });
585
+ },
586
+ requestMagicLink: async (email) => {
587
+ requireEmail(email);
588
+ await getState().http.post(P.authMagicLinkRequest, { email });
589
+ },
590
+ verifyMagicLink: async (token) => {
591
+ if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
592
+ return postAuth(getState(), P.authMagicLinkVerify, { token });
593
+ },
594
+ linkAccount: async (provider, credential) => {
595
+ const providerTrimmed = provider?.trim() ?? "";
596
+ if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
597
+ if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
598
+ const s = getState();
599
+ const sessionToken = s.identity.getSessionToken();
600
+ if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
601
+ return postAuth(s, P.authLink, {
602
+ provider: providerTrimmed,
603
+ token: credential,
604
+ session_token: sessionToken
605
+ });
606
+ },
507
607
  signOut: async (rotateAnonymousId = false) => {
508
608
  const s = getState();
509
609
  if (s.identity.getSessionToken()) try {
@@ -540,6 +640,41 @@ const Amba = {
540
640
  };
541
641
  }
542
642
  },
643
+ users: {
644
+ get: async (userId) => {
645
+ const s = getState();
646
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
647
+ const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
648
+ return (await s.http.get(path)).data;
649
+ },
650
+ update: ((arg1, arg2) => {
651
+ const s = getState();
652
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
653
+ const isById = typeof arg1 === "string";
654
+ const patch = isById ? arg2 ?? {} : arg1;
655
+ const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
656
+ return s.http.patch(path, patch).then((env) => env.data);
657
+ })
658
+ },
659
+ sessions: {
660
+ list: async () => {
661
+ return (await getState().http.get(P.sessions)).data;
662
+ },
663
+ revoke: async (sessionId) => {
664
+ await getState().http.post(`${P.sessions}/${encodePathSegment(sessionId)}/end`, {});
665
+ }
666
+ },
667
+ sync: {
668
+ pushChanges: async (changes) => {
669
+ return (await getState().http.post(P.sync, { changes })).data;
670
+ },
671
+ pullChanges: async (since) => {
672
+ if (!since.entity_type || since.entity_type.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "pullChanges requires a non-empty entity_type");
673
+ const query = { entity_type: since.entity_type };
674
+ if (since.checkpoint_token !== void 0) query["checkpoint_token"] = since.checkpoint_token;
675
+ return (await getState().http.get(P.sync, query)).data;
676
+ }
677
+ },
543
678
  collections: {
544
679
  find: async (name, options = {}) => {
545
680
  return getState().http.post(`${P.collections}/${encodePathSegment(name)}/find`, options);
@@ -547,6 +682,20 @@ const Amba = {
547
682
  findOne: async (name, id) => {
548
683
  return (await getState().http.get(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`)).data;
549
684
  },
685
+ findNearest: async (name, vector, k, where) => {
686
+ const s = getState();
687
+ const body = {
688
+ to_vector: vector,
689
+ k
690
+ };
691
+ if (where !== void 0) body.where = where;
692
+ return (await s.http.post(`${P.collections}/${encodePathSegment(name)}/find-nearest`, body)).data;
693
+ },
694
+ count: async (name, where) => {
695
+ const s = getState();
696
+ const query = where !== void 0 ? { query: JSON.stringify({ where }) } : void 0;
697
+ return (await s.http.get(`${P.collections}/${encodePathSegment(name)}/count`, query)).data.count;
698
+ },
550
699
  insert: async (name, row) => {
551
700
  return (await getState().http.post(`${P.collections}/${encodePathSegment(name)}`, row)).data;
552
701
  },
@@ -635,6 +784,33 @@ const Amba = {
635
784
  upload_id: uploadId,
636
785
  asset_id: assetId
637
786
  })).data;
787
+ },
788
+ list: async (prefix) => {
789
+ const env = await getState().http.get(P.storage, prefix !== void 0 ? { prefix } : void 0);
790
+ return {
791
+ data: env.data ?? [],
792
+ next_cursor: env.next_cursor ?? null
793
+ };
794
+ },
795
+ delete: async (assetId) => {
796
+ await getState().http.delete(`${P.storage}/${encodePathSegment(assetId)}`);
797
+ },
798
+ download: async (assetId) => {
799
+ const assetEnv = await getState().http.get(`${P.storage}/${encodePathSegment(assetId)}`);
800
+ const cdnUrl = assetEnv.data.url ?? assetEnv.data.storage_url ?? "";
801
+ if (!cdnUrl) throw new AmbaApiError("NOT_FOUND", "asset has no resolvable CDN url");
802
+ let response;
803
+ try {
804
+ response = await (globalThis.fetch ?? fetch)(cdnUrl, { method: "GET" });
805
+ } catch (err) {
806
+ throw new AmbaApiError("NETWORK_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`, err);
807
+ }
808
+ if (!response.ok) {
809
+ const text = await response.text();
810
+ const ct = response.headers.get("content-type") ?? "";
811
+ throw mapHttpError(response, text, ct.includes("application/json"));
812
+ }
813
+ return response.blob();
638
814
  }
639
815
  },
640
816
  push: {
@@ -705,8 +881,21 @@ const Amba = {
705
881
  values: parsed.data ?? {}
706
882
  };
707
883
  } },
708
- flags: { fetch: async () => {
709
- return (await getState().http.get(P.flags)).data;
884
+ flags: {
885
+ fetch: async () => {
886
+ return (await getState().http.get(P.flags)).data;
887
+ },
888
+ get: async (key) => {
889
+ try {
890
+ return (await getState().http.get(`${P.flags}/${encodePathSegment(key)}`)).data;
891
+ } catch (err) {
892
+ if (err instanceof AmbaApiError && err.code === "NOT_FOUND") return null;
893
+ throw err;
894
+ }
895
+ }
896
+ },
897
+ diagnostics: { ping: async () => {
898
+ return await getState().http.post(P.diagnosticsPing, {});
710
899
  } },
711
900
  achievements: {
712
901
  getAll: async () => {
@@ -735,7 +924,7 @@ const Amba = {
735
924
  return (await getState().http.get(P.currencies)).data;
736
925
  },
737
926
  getTransactions: async (currencyKey) => {
738
- return (await getState().http.get(`${P.currencies}/${encodePathSegment(currencyKey)}/transactions`)).data;
927
+ return (await getState().http.get(`${P.currencies}/transactions`, { currency_code: currencyKey })).data;
739
928
  }
740
929
  },
741
930
  inventory: {
@@ -749,7 +938,8 @@ const Amba = {
749
938
  return (await getState().http.post(`${P.inventory}/purchase`, request)).data;
750
939
  },
751
940
  consume: async (request) => {
752
- return (await getState().http.post(`${P.inventory}/consume`, request)).data;
941
+ const { item_id,...body } = request;
942
+ return (await getState().http.post(`${P.inventory}/${encodePathSegment(item_id)}/consume`, body)).data;
753
943
  }
754
944
  },
755
945
  leaderboards: {
@@ -763,6 +953,14 @@ const Amba = {
763
953
  return (await getState().http.get(`${P.leaderboards}/${encodePathSegment(key)}/me`)).data;
764
954
  }
765
955
  },
956
+ leagues: {
957
+ me: async () => {
958
+ return (await getState().http.get(`${P.leagues}/me`)).data;
959
+ },
960
+ cohort: async () => {
961
+ return (await getState().http.get(`${P.leagues}/me/cohort`)).data;
962
+ }
963
+ },
766
964
  stores: {
767
965
  list: async () => {
768
966
  return (await getState().http.get(P.stores)).data;
@@ -773,7 +971,7 @@ const Amba = {
773
971
  purchase: async (storeKey, purchaseOptionId, receipt) => {
774
972
  return (await getState().http.post(`${P.stores}/${encodePathSegment(storeKey)}/purchase`, {
775
973
  purchase_option_id: purchaseOptionId,
776
- receipt
974
+ receipt: receipt ?? {}
777
975
  })).data;
778
976
  }
779
977
  },
@@ -807,6 +1005,15 @@ const Amba = {
807
1005
  getFriends: async () => {
808
1006
  return (await getState().http.get(P.friends, { state: "accepted" })).data;
809
1007
  },
1008
+ sendRequest: async (userId) => {
1009
+ return (await getState().http.post(`${P.friends}/request`, { user_id: userId })).data;
1010
+ },
1011
+ acceptRequest: async (friendshipId) => {
1012
+ return (await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/accept`, {})).data;
1013
+ },
1014
+ declineRequest: async (friendshipId) => {
1015
+ await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/reject`, {});
1016
+ },
810
1017
  blockUser: async (userId) => {
811
1018
  return (await getState().http.post(`${P.friends}/block`, { user_id: userId })).data;
812
1019
  },
@@ -815,6 +1022,9 @@ const Amba = {
815
1022
  },
816
1023
  removeBlock: async (friendshipId) => {
817
1024
  await getState().http.delete(`${P.friends}/${encodePathSegment(friendshipId)}`);
1025
+ },
1026
+ removeFriend: async (userId) => {
1027
+ await getState().http.delete(`${P.friends}/by-user/${encodePathSegment(userId)}`);
818
1028
  }
819
1029
  },
820
1030
  groups: {
@@ -847,10 +1057,7 @@ const Amba = {
847
1057
  createConversation: async (request) => {
848
1058
  return (await getState().http.post(`${P.messaging}/conversations`, request)).data;
849
1059
  },
850
- getConversations: async () => {
851
- return (await getState().http.get(`${P.messaging}/conversations`)).data;
852
- },
853
- listConversations: async () => {
1060
+ conversations: async () => {
854
1061
  return (await getState().http.get(`${P.messaging}/conversations`)).data;
855
1062
  },
856
1063
  sendMessage: async (conversationId, request) => {
@@ -864,6 +1071,24 @@ const Amba = {
864
1071
  },
865
1072
  markRead: async (conversationId) => {
866
1073
  return (await getState().http.post(`${P.messaging}/conversations/${encodePathSegment(conversationId)}/read`, {})).data;
1074
+ },
1075
+ getMessage: async (conversationId, messageId) => {
1076
+ const PAGE_SIZE = 100;
1077
+ const MAX_PAGES = 50;
1078
+ const s = getState();
1079
+ const path = `${P.messaging}/conversations/${encodePathSegment(conversationId)}/messages`;
1080
+ let offset = 0;
1081
+ for (let i = 0; i < MAX_PAGES; i++) {
1082
+ const page = (await s.http.get(path, {
1083
+ limit: PAGE_SIZE,
1084
+ offset
1085
+ })).data;
1086
+ const hit = page.find((m) => m.id === messageId);
1087
+ if (hit) return hit;
1088
+ if (page.length < PAGE_SIZE) return null;
1089
+ offset += PAGE_SIZE;
1090
+ }
1091
+ return null;
867
1092
  }
868
1093
  },
869
1094
  moderation: {
@@ -904,7 +1129,7 @@ const Amba = {
904
1129
  },
905
1130
  referrals: {
906
1131
  getReferralCode: async () => {
907
- return (await getState().http.get(`${P.referrals}/me`)).data;
1132
+ return (await getState().http.get(`${P.referrals}/my-code`)).data;
908
1133
  },
909
1134
  claimReferral: async (code) => {
910
1135
  return (await getState().http.post(`${P.referrals}/claim`, { code })).data;
@@ -916,20 +1141,25 @@ const Amba = {
916
1141
  return (await getState().http.post(P.referrals, body)).data;
917
1142
  }
918
1143
  },
919
- catalog: { list: async () => {
920
- return (await getState().http.get(P.catalog)).data;
921
- } },
1144
+ catalog: {
1145
+ list: async () => {
1146
+ return (await getState().http.get(P.catalog)).data;
1147
+ },
1148
+ get: async (id) => {
1149
+ return (await getState().http.get(`${P.catalog}/${encodePathSegment(id)}`)).data;
1150
+ }
1151
+ },
922
1152
  content: {
923
- getToday: async (channel = "default") => {
1153
+ today: async (channel = "default") => {
924
1154
  return (await getState().http.get(`${P.content}/today/${encodePathSegment(channel)}`)).data ?? null;
925
1155
  },
926
- getLibrary: async (channel = "default", options) => {
1156
+ library: async (channel = "default", options) => {
927
1157
  return (await getState().http.get(`${P.content}/library/${encodePathSegment(channel)}`, {
928
1158
  limit: options?.limit,
929
1159
  cursor: options?.cursor
930
1160
  })).data;
931
1161
  },
932
- getItem: async (id) => {
1162
+ item: async (id) => {
933
1163
  return (await getState().http.get(`${P.content}/${encodePathSegment(id)}`)).data;
934
1164
  },
935
1165
  updateItem: async (id, state_) => {
@@ -941,7 +1171,7 @@ const Amba = {
941
1171
  },
942
1172
  deepLinks: {
943
1173
  get: async (shortCode) => {
944
- return (await getState().http.get(`${P.deepLinks}/${encodePathSegment(shortCode)}`)).data;
1174
+ return (await getState().http.get(`${P.deepLinks}/resolve/${encodePathSegment(shortCode)}`)).data;
945
1175
  },
946
1176
  create: async (params) => {
947
1177
  return (await getState().http.post(P.deepLinks, params)).data;