@frak-labs/core-sdk 0.2.1-beta.b38eef2e → 0.2.1-beta.d04602ec

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.
Files changed (74) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +55 -3
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -3
  5. package/dist/actions.d.ts +3 -3
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -1
  8. package/dist/bundle.d.cts +4 -4
  9. package/dist/bundle.d.ts +4 -4
  10. package/dist/bundle.js +1 -1
  11. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → computeLegacyProductId-fKvxbC4k.d.ts} +91 -37
  12. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → computeLegacyProductId-rYIvY4c3.d.cts} +91 -37
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-B0g7-807.d.cts → openSso-CMZM06uR.d.ts} +258 -46
  18. package/dist/{openSso-CMzwvaCa.d.ts → openSso-CebB8mFv.d.cts} +258 -46
  19. package/dist/setupClient-B_XMB52l.cjs +13 -0
  20. package/dist/setupClient-jYx-fbxB.js +13 -0
  21. package/dist/siweAuthenticate-CWcVvP-G.cjs +1 -0
  22. package/dist/siweAuthenticate-DQfdb5UQ.js +1 -0
  23. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-Dc_Yg9Bg.d.cts} +102 -8
  24. package/dist/{siweAuthenticate-CVigMOxz.d.cts → siweAuthenticate-Ddhl-o4N.d.ts} +102 -8
  25. package/dist/trackEvent-Ce1XlsIE.js +1 -0
  26. package/dist/trackEvent-CvbJTTqA.cjs +1 -0
  27. package/package.json +8 -8
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/displaySharingPage.ts +49 -0
  31. package/src/actions/ensureIdentity.ts +2 -2
  32. package/src/actions/getMerchantInformation.test.ts +13 -1
  33. package/src/actions/getMerchantInformation.ts +20 -5
  34. package/src/actions/getUserReferralStatus.ts +42 -0
  35. package/src/actions/index.ts +7 -1
  36. package/src/actions/referral/setupReferral.test.ts +79 -0
  37. package/src/actions/referral/setupReferral.ts +32 -0
  38. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  39. package/src/actions/trackPurchaseStatus.ts +3 -5
  40. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  41. package/src/actions/wrapper/modalBuilder.ts +6 -8
  42. package/src/clients/createIFrameFrakClient.ts +148 -25
  43. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  44. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  45. package/src/index.ts +17 -4
  46. package/src/types/config.ts +10 -3
  47. package/src/types/index.ts +13 -1
  48. package/src/types/lifecycle/client.ts +22 -27
  49. package/src/types/lifecycle/iframe.ts +0 -8
  50. package/src/types/resolvedConfig.ts +122 -0
  51. package/src/types/rpc/displaySharingPage.ts +77 -0
  52. package/src/types/rpc/interaction.ts +4 -0
  53. package/src/types/rpc/userReferralStatus.ts +20 -0
  54. package/src/types/rpc.ts +42 -5
  55. package/src/utils/backendUrl.test.ts +2 -2
  56. package/src/utils/backendUrl.ts +1 -1
  57. package/src/utils/cache/index.ts +7 -0
  58. package/src/utils/cache/lruMap.test.ts +55 -0
  59. package/src/utils/cache/lruMap.ts +38 -0
  60. package/src/utils/cache/withCache.test.ts +162 -0
  61. package/src/utils/cache/withCache.ts +105 -0
  62. package/src/utils/inAppBrowser.ts +60 -0
  63. package/src/utils/index.ts +6 -4
  64. package/src/utils/sdkConfigStore.test.ts +405 -0
  65. package/src/utils/sdkConfigStore.ts +263 -0
  66. package/src/utils/sso.ts +3 -7
  67. package/dist/setupClient-CqTHGvVa.cjs +0 -13
  68. package/dist/setupClient-DTyvAPgh.js +0 -13
  69. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  70. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  71. package/dist/trackEvent-CeLFVzZn.js +0 -1
  72. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  73. package/src/utils/merchantId.test.ts +0 -653
  74. package/src/utils/merchantId.ts +0 -143
package/src/types/rpc.ts CHANGED
@@ -4,6 +4,10 @@ import type {
4
4
  ModalRpcStepsInput,
5
5
  ModalRpcStepsResultType,
6
6
  } from "./rpc/displayModal";
7
+ import type {
8
+ DisplaySharingPageParamsType,
9
+ DisplaySharingPageResultType,
10
+ } from "./rpc/displaySharingPage";
7
11
  import type {
8
12
  DisplayEmbeddedWalletParamsType,
9
13
  DisplayEmbeddedWalletResultType,
@@ -16,6 +20,7 @@ import type {
16
20
  PrepareSsoParamsType,
17
21
  PrepareSsoReturnType,
18
22
  } from "./rpc/sso";
23
+ import type { UserReferralStatusType } from "./rpc/userReferralStatus";
19
24
  import type { WalletStatusReturnType } from "./rpc/walletStatus";
20
25
 
21
26
  /**
@@ -38,7 +43,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
38
43
  * - Response Type: stream (emits updates when wallet status changes)
39
44
  *
40
45
  * #### frak_displayModal
41
- * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"]]
46
+ * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
42
47
  * - Returns: {@link ModalRpcStepsResultType}
43
48
  * - Response Type: promise (one-shot)
44
49
  *
@@ -53,9 +58,14 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
53
58
  * - Response Type: promise (one-shot)
54
59
  *
55
60
  * #### frak_displayEmbeddedWallet
56
- * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"]]
61
+ * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
57
62
  * - Returns: {@link DisplayEmbeddedWalletResultType}
58
63
  * - Response Type: promise (one-shot)
64
+ *
65
+ * #### frak_displaySharingPage
66
+ * - Params: [request: {@link DisplaySharingPageParamsType}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
67
+ * - Returns: {@link DisplaySharingPageResultType}
68
+ * - Response Type: promise (one-shot)
59
69
  */
60
70
  export type IFrameRpcSchema = [
61
71
  /**
@@ -77,6 +87,7 @@ export type IFrameRpcSchema = [
77
87
  requests: ModalRpcStepsInput,
78
88
  metadata: ModalRpcMetadata | undefined,
79
89
  configMetadata: FrakWalletSdkConfig["metadata"],
90
+ placement?: string,
80
91
  ];
81
92
  ReturnType: ModalRpcStepsResultType;
82
93
  },
@@ -89,7 +100,7 @@ export type IFrameRpcSchema = [
89
100
  Method: "frak_prepareSso";
90
101
  Parameters: [
91
102
  params: PrepareSsoParamsType,
92
- name: string,
103
+ name?: string,
93
104
  customCss?: string,
94
105
  ];
95
106
  ReturnType: PrepareSsoReturnType;
@@ -104,7 +115,7 @@ export type IFrameRpcSchema = [
104
115
  Method: "frak_openSso";
105
116
  Parameters: [
106
117
  params: OpenSsoParamsType,
107
- name: string,
118
+ name?: string,
108
119
  customCss?: string,
109
120
  ];
110
121
  ReturnType: OpenSsoReturnType;
@@ -130,6 +141,7 @@ export type IFrameRpcSchema = [
130
141
  Parameters: [
131
142
  request: DisplayEmbeddedWalletParamsType,
132
143
  metadata: FrakWalletSdkConfig["metadata"],
144
+ placement?: string,
133
145
  ];
134
146
  ReturnType: DisplayEmbeddedWalletResultType;
135
147
  },
@@ -137,7 +149,7 @@ export type IFrameRpcSchema = [
137
149
  * Method to send interactions (arrival, sharing, custom events)
138
150
  * Fire-and-forget method - no return value expected
139
151
  * merchantId is resolved from context
140
- * clientId is passed via metadata as safeguard against handshake race condition
152
+ * clientId is passed via metadata as safeguard against race conditions
141
153
  */
142
154
  {
143
155
  Method: "frak_sendInteraction";
@@ -147,4 +159,29 @@ export type IFrameRpcSchema = [
147
159
  ];
148
160
  ReturnType: undefined;
149
161
  },
162
+ /**
163
+ * Method to get the current user's referral status on this merchant.
164
+ * Returns whether the user was referred (has a referral link as referee).
165
+ * Returns null when the user's identity cannot be resolved.
166
+ * This is a one-shot request.
167
+ */
168
+ {
169
+ Method: "frak_getUserReferralStatus";
170
+ Parameters?: undefined;
171
+ ReturnType: UserReferralStatusType | null;
172
+ },
173
+ /**
174
+ * Method to display a sharing page with product info and sharing buttons
175
+ * Resolves on first user action (share/copy) but the page stays visible
176
+ * This is a one-shot request
177
+ */
178
+ {
179
+ Method: "frak_displaySharingPage";
180
+ Parameters: [
181
+ request: DisplaySharingPageParamsType,
182
+ configMetadata: FrakWalletSdkConfig["metadata"],
183
+ placement?: string,
184
+ ];
185
+ ReturnType: DisplaySharingPageResultType;
186
+ },
150
187
  ];
@@ -15,13 +15,13 @@ describe("getBackendUrl", () => {
15
15
  describe("with explicit walletUrl", () => {
16
16
  test("should return localhost backend for localhost:3000", () => {
17
17
  expect(getBackendUrl("https://localhost:3000")).toBe(
18
- "http://localhost:3030"
18
+ "https://localhost:3030"
19
19
  );
20
20
  });
21
21
 
22
22
  test("should return localhost backend for localhost:3010", () => {
23
23
  expect(getBackendUrl("https://localhost:3010")).toBe(
24
- "http://localhost:3030"
24
+ "https://localhost:3030"
25
25
  );
26
26
  });
27
27
 
@@ -19,7 +19,7 @@ function isLocalDevelopment(walletUrl: string): boolean {
19
19
  */
20
20
  function deriveBackendUrl(walletUrl: string): string {
21
21
  if (isLocalDevelopment(walletUrl)) {
22
- return "http://localhost:3030";
22
+ return "https://localhost:3030";
23
23
  }
24
24
  // Dev environment
25
25
  if (
@@ -0,0 +1,7 @@
1
+ export { LruMap } from "./lruMap";
2
+ export {
3
+ clearAllCache,
4
+ DEFAULT_CACHE_TIME,
5
+ getCache,
6
+ withCache,
7
+ } from "./withCache";
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "../../../tests/vitest-fixtures";
2
+ import { LruMap } from "./lruMap";
3
+
4
+ describe("LruMap", () => {
5
+ it("should store and retrieve values", () => {
6
+ const map = new LruMap<number>(3);
7
+ map.set("a", 1);
8
+ map.set("b", 2);
9
+
10
+ expect(map.get("a")).toBe(1);
11
+ expect(map.get("b")).toBe(2);
12
+ });
13
+
14
+ it("should evict least recently used when exceeding max size", () => {
15
+ const map = new LruMap<number>(2);
16
+ map.set("a", 1);
17
+ map.set("b", 2);
18
+ map.set("c", 3); // Should evict "a"
19
+
20
+ expect(map.get("a")).toBeUndefined();
21
+ expect(map.get("b")).toBe(2);
22
+ expect(map.get("c")).toBe(3);
23
+ });
24
+
25
+ it("should promote accessed keys to most recently used", () => {
26
+ const map = new LruMap<number>(2);
27
+ map.set("a", 1);
28
+ map.set("b", 2);
29
+
30
+ // Access "a" to promote it
31
+ map.get("a");
32
+
33
+ // "b" is now least recently used, should be evicted
34
+ map.set("c", 3);
35
+
36
+ expect(map.get("a")).toBe(1);
37
+ expect(map.get("b")).toBeUndefined();
38
+ expect(map.get("c")).toBe(3);
39
+ });
40
+
41
+ it("should overwrite existing keys without increasing size", () => {
42
+ const map = new LruMap<number>(2);
43
+ map.set("a", 1);
44
+ map.set("b", 2);
45
+ map.set("a", 10); // Overwrite, not a new entry
46
+
47
+ expect(map.size).toBe(2);
48
+ expect(map.get("a")).toBe(10);
49
+ });
50
+
51
+ it("should return undefined for missing keys", () => {
52
+ const map = new LruMap<number>(2);
53
+ expect(map.get("missing")).toBeUndefined();
54
+ });
55
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Map with a LRU (Least Recently Used) eviction policy.
3
+ *
4
+ * When the map exceeds `maxSize`, the least recently accessed entry is removed.
5
+ * Accessing a key via `get()` promotes it to "most recently used".
6
+ *
7
+ * Adapted from viem's LruMap utility.
8
+ * @link https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
9
+ */
10
+ export class LruMap<TValue = unknown> extends Map<string, TValue> {
11
+ maxSize: number;
12
+
13
+ constructor(size: number) {
14
+ super();
15
+ this.maxSize = size;
16
+ }
17
+
18
+ override get(key: string) {
19
+ const value = super.get(key);
20
+ if (super.has(key)) {
21
+ // Move to end (most recently used)
22
+ super.delete(key);
23
+ super.set(key, value as TValue);
24
+ }
25
+ return value;
26
+ }
27
+
28
+ override set(key: string, value: TValue) {
29
+ if (super.has(key)) super.delete(key);
30
+ super.set(key, value);
31
+ // Evict least recently used if over capacity
32
+ if (this.maxSize && this.size > this.maxSize) {
33
+ const firstKey = super.keys().next().value;
34
+ if (firstKey !== undefined) super.delete(firstKey);
35
+ }
36
+ return this;
37
+ }
38
+ }
@@ -0,0 +1,162 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ vi,
8
+ } from "../../../tests/vitest-fixtures";
9
+ import { clearAllCache, withCache } from "./withCache";
10
+
11
+ describe("withCache", () => {
12
+ beforeEach(() => {
13
+ clearAllCache();
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ describe("caching behavior", () => {
21
+ it("should call fn on first invocation", async () => {
22
+ const fn = vi.fn().mockResolvedValue("result");
23
+
24
+ const result = await withCache(fn, {
25
+ cacheKey: "test-key",
26
+ });
27
+
28
+ expect(fn).toHaveBeenCalledOnce();
29
+ expect(result).toBe("result");
30
+ });
31
+
32
+ it("should return cached result on subsequent calls within TTL", async () => {
33
+ const fn = vi.fn().mockResolvedValue("result");
34
+
35
+ await withCache(fn, {
36
+ cacheKey: "test-key",
37
+ cacheTime: 10_000,
38
+ });
39
+ const result = await withCache(fn, {
40
+ cacheKey: "test-key",
41
+ cacheTime: 10_000,
42
+ });
43
+
44
+ expect(fn).toHaveBeenCalledOnce();
45
+ expect(result).toBe("result");
46
+ });
47
+
48
+ it("should re-fetch after cache expires", async () => {
49
+ vi.useFakeTimers();
50
+
51
+ const fn = vi
52
+ .fn()
53
+ .mockResolvedValueOnce("first")
54
+ .mockResolvedValueOnce("second");
55
+
56
+ const first = await withCache(fn, {
57
+ cacheKey: "test-key",
58
+ cacheTime: 100,
59
+ });
60
+ expect(first).toBe("first");
61
+
62
+ // Advance past TTL
63
+ vi.advanceTimersByTime(200);
64
+
65
+ const second = await withCache(fn, {
66
+ cacheKey: "test-key",
67
+ cacheTime: 100,
68
+ });
69
+ expect(second).toBe("second");
70
+ expect(fn).toHaveBeenCalledTimes(2);
71
+
72
+ vi.useRealTimers();
73
+ });
74
+
75
+ it("should not cache when cacheTime is 0", async () => {
76
+ const fn = vi.fn().mockResolvedValue("result");
77
+
78
+ await withCache(fn, { cacheKey: "test-key", cacheTime: 0 });
79
+ await withCache(fn, { cacheKey: "test-key", cacheTime: 0 });
80
+
81
+ expect(fn).toHaveBeenCalledTimes(2);
82
+ });
83
+
84
+ it("should use different caches for different keys", async () => {
85
+ const fnA = vi.fn().mockResolvedValue("a");
86
+ const fnB = vi.fn().mockResolvedValue("b");
87
+
88
+ const a = await withCache(fnA, { cacheKey: "key-a" });
89
+ const b = await withCache(fnB, { cacheKey: "key-b" });
90
+
91
+ expect(a).toBe("a");
92
+ expect(b).toBe("b");
93
+ expect(fnA).toHaveBeenCalledOnce();
94
+ expect(fnB).toHaveBeenCalledOnce();
95
+ });
96
+ });
97
+
98
+ describe("deduplication", () => {
99
+ it("should deduplicate concurrent calls with the same key", async () => {
100
+ let resolvePromise: (value: string) => void;
101
+ const fn = vi.fn().mockImplementation(
102
+ () =>
103
+ new Promise<string>((resolve) => {
104
+ resolvePromise = resolve;
105
+ })
106
+ );
107
+
108
+ const promise1 = withCache(fn, { cacheKey: "dedup-key" });
109
+ const promise2 = withCache(fn, { cacheKey: "dedup-key" });
110
+
111
+ // fn should only be called once
112
+ expect(fn).toHaveBeenCalledOnce();
113
+
114
+ // Both should resolve to the same value
115
+ resolvePromise!("shared");
116
+ const [result1, result2] = await Promise.all([promise1, promise2]);
117
+ expect(result1).toBe("shared");
118
+ expect(result2).toBe("shared");
119
+ });
120
+ });
121
+
122
+ describe("error handling", () => {
123
+ it("should propagate errors from fn", async () => {
124
+ const fn = vi.fn().mockRejectedValue(new Error("fetch failed"));
125
+
126
+ await expect(
127
+ withCache(fn, { cacheKey: "error-key" })
128
+ ).rejects.toThrow("fetch failed");
129
+ });
130
+
131
+ it("should not cache errors — subsequent call retries", async () => {
132
+ const fn = vi
133
+ .fn()
134
+ .mockRejectedValueOnce(new Error("fail"))
135
+ .mockResolvedValueOnce("recovered");
136
+
137
+ await expect(
138
+ withCache(fn, { cacheKey: "retry-key" })
139
+ ).rejects.toThrow("fail");
140
+
141
+ const result = await withCache(fn, { cacheKey: "retry-key" });
142
+ expect(result).toBe("recovered");
143
+ expect(fn).toHaveBeenCalledTimes(2);
144
+ });
145
+ });
146
+
147
+ describe("clearAllCache", () => {
148
+ it("should clear all cached data", async () => {
149
+ const fn = vi
150
+ .fn()
151
+ .mockResolvedValueOnce("first")
152
+ .mockResolvedValueOnce("second");
153
+
154
+ await withCache(fn, { cacheKey: "clear-key" });
155
+ clearAllCache();
156
+ const result = await withCache(fn, { cacheKey: "clear-key" });
157
+
158
+ expect(result).toBe("second");
159
+ expect(fn).toHaveBeenCalledTimes(2);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,105 @@
1
+ import { LruMap } from "./lruMap";
2
+
3
+ type CacheEntry<TData> = {
4
+ data: TData;
5
+ created: number;
6
+ };
7
+
8
+ /** Global cache for in-flight promises (dedup concurrent calls) */
9
+ const promiseCache = new LruMap<Promise<unknown>>(1024);
10
+
11
+ /** Global cache for resolved responses (TTL-based) */
12
+ const responseCache = new LruMap<CacheEntry<unknown>>(1024);
13
+
14
+ /** Default cache time: 30 seconds */
15
+ export const DEFAULT_CACHE_TIME = 30_000;
16
+
17
+ type WithCacheOptions = {
18
+ /** The key to cache the data against */
19
+ cacheKey: string;
20
+ /** Time in ms that cached data will remain valid. Default: 30_000 (30s). Set to 0 to disable caching. */
21
+ cacheTime?: number;
22
+ };
23
+
24
+ /**
25
+ * Returns the result of a given promise, and caches the result for
26
+ * subsequent invocations against a provided cache key.
27
+ *
28
+ * Also deduplicates concurrent calls — if multiple callers request the same
29
+ * cache key while the promise is pending, they share the same promise.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // First call fetches, subsequent calls return cached data for 30s
34
+ * const data = await withCache(
35
+ * () => client.request({ method: "frak_getMerchantInformation" }),
36
+ * { cacheKey: "merchantInfo", cacheTime: 30_000 }
37
+ * );
38
+ * ```
39
+ */
40
+ export async function withCache<TData>(
41
+ fn: () => Promise<TData>,
42
+ { cacheKey, cacheTime = DEFAULT_CACHE_TIME }: WithCacheOptions
43
+ ): Promise<TData> {
44
+ // Check response cache — return immediately if fresh
45
+ if (cacheTime > 0) {
46
+ const cached = responseCache.get(cacheKey) as
47
+ | CacheEntry<TData>
48
+ | undefined;
49
+ if (cached) {
50
+ const age = Date.now() - cached.created;
51
+ if (age < cacheTime) return cached.data;
52
+ }
53
+ }
54
+
55
+ // Check if there's already a pending promise (dedup concurrent calls)
56
+ let promise = promiseCache.get(cacheKey) as Promise<TData> | undefined;
57
+ if (!promise) {
58
+ promise = fn();
59
+ promiseCache.set(cacheKey, promise);
60
+ }
61
+
62
+ try {
63
+ const data = await promise;
64
+ // Store the response with a timestamp
65
+ responseCache.set(cacheKey, { data, created: Date.now() });
66
+ return data;
67
+ } finally {
68
+ // Clear the promise cache so subsequent calls can re-fetch after TTL
69
+ promiseCache.delete(cacheKey);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get a cache handle for a specific key, useful for manual invalidation.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // Invalidate merchant info cache after a mutation
79
+ * getCache("frak_getMerchantInformation").clear();
80
+ * ```
81
+ */
82
+ export function getCache(cacheKey: string) {
83
+ return {
84
+ /** Clear both the pending promise and the cached response */
85
+ clear: () => {
86
+ promiseCache.delete(cacheKey);
87
+ responseCache.delete(cacheKey);
88
+ },
89
+ /** Check if a non-expired response exists */
90
+ has: (cacheTime: number = DEFAULT_CACHE_TIME) => {
91
+ const cached = responseCache.get(cacheKey);
92
+ if (!cached) return false;
93
+ return Date.now() - cached.created < cacheTime;
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Clear all cached data (both pending promises and resolved responses).
100
+ * Called automatically when the client is destroyed.
101
+ */
102
+ export function clearAllCache() {
103
+ promiseCache.clear();
104
+ responseCache.clear();
105
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Check if the current device runs iOS (including iPadOS 13+).
3
+ */
4
+ function checkIsIOS(): boolean {
5
+ if (typeof navigator === "undefined") return false;
6
+ const ua = navigator.userAgent;
7
+ // Standard iOS devices
8
+ if (/iPhone|iPad|iPod/i.test(ua)) return true;
9
+ // iPadOS 13+ reports as Macintosh — detect via touch support
10
+ if (/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1) return true;
11
+ return false;
12
+ }
13
+
14
+ /**
15
+ * Whether the current device runs iOS (including iPadOS 13+).
16
+ */
17
+ export const isIOS: boolean = checkIsIOS();
18
+
19
+ /**
20
+ * Check if the current browser is a social media in-app browser
21
+ * (Instagram, Facebook WebView).
22
+ */
23
+ function checkInAppBrowser(): boolean {
24
+ if (typeof navigator === "undefined") return false;
25
+ const ua = navigator.userAgent.toLowerCase();
26
+ return (
27
+ ua.includes("instagram") ||
28
+ ua.includes("fban") ||
29
+ ua.includes("fbav") ||
30
+ ua.includes("facebook")
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Whether the current browser is a social media in-app browser
36
+ * (Instagram, Facebook).
37
+ */
38
+ export const isInAppBrowser: boolean = checkInAppBrowser();
39
+
40
+ /**
41
+ * Redirect to external browser from in-app WebView.
42
+ *
43
+ * - **iOS**: Uses `x-safari-https://` scheme — server-side 302 redirects
44
+ * to custom URL schemes are silently swallowed by WKWebView.
45
+ * Direct `window.location.href` assignment works (confirmed iOS 17+).
46
+ *
47
+ * - **Android**: Uses backend `/common/social` endpoint which returns a PDF
48
+ * Content-Type response, forcing the WebView to hand off to the default browser.
49
+ *
50
+ * @param targetUrl - The URL to open in the external browser
51
+ */
52
+ export function redirectToExternalBrowser(targetUrl: string): void {
53
+ if (isIOS && targetUrl.startsWith("https://")) {
54
+ window.location.href = `x-safari-https://${targetUrl.slice(8)}`;
55
+ } else if (isIOS && targetUrl.startsWith("http://")) {
56
+ window.location.href = `x-safari-http://${targetUrl.slice(7)}`;
57
+ } else {
58
+ window.location.href = `${process.env.BACKEND_URL}/common/social?u=${encodeURIComponent(targetUrl)}`;
59
+ }
60
+ }
@@ -1,5 +1,6 @@
1
1
  export { Deferred } from "@frak-labs/frame-connector";
2
2
  export { getBackendUrl } from "./backendUrl";
3
+ export { clearAllCache, getCache, withCache } from "./cache";
3
4
  export { getClientId } from "./clientId";
4
5
  export { base64urlDecode, base64urlEncode } from "./compression/b64";
5
6
  export { compressJsonToB64 } from "./compression/compress";
@@ -23,10 +24,11 @@ export {
23
24
  findIframeInOpener,
24
25
  } from "./iframeHelper";
25
26
  export {
26
- clearMerchantIdCache,
27
- fetchMerchantId,
28
- resolveMerchantId,
29
- } from "./merchantId";
27
+ isInAppBrowser,
28
+ isIOS,
29
+ redirectToExternalBrowser,
30
+ } from "./inAppBrowser";
31
+ export { sdkConfigStore } from "./sdkConfigStore";
30
32
  export {
31
33
  type AppSpecificSsoMetadata,
32
34
  type CompressedSsoData,