@frak-labs/core-sdk 0.1.1 → 0.2.0-beta.7898df5b

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 (130) hide show
  1. package/README.md +58 -0
  2. package/cdn/bundle.js +14 -0
  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 -6
  9. package/dist/bundle.d.ts +4 -6
  10. package/dist/bundle.js +1 -1
  11. package/dist/computeLegacyProductId-CCAZvLa5.d.cts +537 -0
  12. package/dist/computeLegacyProductId-b5cUWdAm.d.ts +537 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -4
  15. package/dist/index.d.ts +3 -4
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-D--Airj6.d.cts → openSso-B0g7-807.d.cts} +173 -136
  18. package/dist/{openSso-DsKJ4y0j.d.ts → openSso-CMzwvaCa.d.ts} +173 -136
  19. package/dist/setupClient-BICl5fdX.js +13 -0
  20. package/dist/setupClient-nl8Dhh4V.cjs +13 -0
  21. package/dist/siweAuthenticate-BWmI2_TN.cjs +1 -0
  22. package/dist/{index-d8xS4ryI.d.ts → siweAuthenticate-CVigMOxz.d.cts} +113 -92
  23. package/dist/{index-C6FxkWPC.d.cts → siweAuthenticate-CnCZ7mok.d.ts} +113 -92
  24. package/dist/siweAuthenticate-zczqxm0a.js +1 -0
  25. package/dist/trackEvent-CeLFVzZn.js +1 -0
  26. package/dist/trackEvent-Ew5r5zfI.cjs +1 -0
  27. package/package.json +11 -22
  28. package/src/actions/displayEmbeddedWallet.ts +1 -0
  29. package/src/actions/displayModal.test.ts +12 -11
  30. package/src/actions/displayModal.ts +7 -18
  31. package/src/actions/ensureIdentity.ts +68 -0
  32. package/src/actions/{getProductInformation.test.ts → getMerchantInformation.test.ts} +33 -50
  33. package/src/actions/getMerchantInformation.ts +16 -0
  34. package/src/actions/index.ts +3 -2
  35. package/src/actions/openSso.ts +4 -2
  36. package/src/actions/referral/processReferral.test.ts +117 -242
  37. package/src/actions/referral/processReferral.ts +134 -204
  38. package/src/actions/referral/referralInteraction.test.ts +4 -12
  39. package/src/actions/referral/referralInteraction.ts +3 -13
  40. package/src/actions/sendInteraction.ts +46 -22
  41. package/src/actions/trackPurchaseStatus.test.ts +354 -141
  42. package/src/actions/trackPurchaseStatus.ts +48 -11
  43. package/src/actions/watchWalletStatus.ts +2 -3
  44. package/src/actions/wrapper/modalBuilder.test.ts +0 -14
  45. package/src/actions/wrapper/modalBuilder.ts +3 -12
  46. package/src/bundle.ts +0 -1
  47. package/src/clients/createIFrameFrakClient.ts +10 -5
  48. package/src/clients/transports/iframeLifecycleManager.test.ts +163 -4
  49. package/src/clients/transports/iframeLifecycleManager.ts +172 -33
  50. package/src/constants/interactionTypes.ts +12 -41
  51. package/src/index.ts +27 -16
  52. package/src/types/config.ts +6 -0
  53. package/src/types/context.ts +48 -6
  54. package/src/types/index.ts +15 -11
  55. package/src/types/lifecycle/client.ts +24 -1
  56. package/src/types/lifecycle/iframe.ts +6 -0
  57. package/src/types/rpc/displayModal.ts +2 -4
  58. package/src/types/rpc/embedded/index.ts +2 -2
  59. package/src/types/rpc/interaction.ts +31 -39
  60. package/src/types/rpc/merchantInformation.ts +77 -0
  61. package/src/types/rpc/modal/index.ts +0 -4
  62. package/src/types/rpc/modal/login.ts +5 -1
  63. package/src/types/rpc/walletStatus.ts +1 -7
  64. package/src/types/rpc.ts +22 -30
  65. package/src/types/tracking.ts +31 -0
  66. package/src/utils/FrakContext.test.ts +270 -186
  67. package/src/utils/FrakContext.ts +78 -56
  68. package/src/utils/backendUrl.test.ts +83 -0
  69. package/src/utils/backendUrl.ts +62 -0
  70. package/src/utils/clientId.test.ts +41 -0
  71. package/src/utils/clientId.ts +43 -0
  72. package/src/utils/compression/compress.test.ts +1 -1
  73. package/src/utils/compression/compress.ts +2 -2
  74. package/src/utils/compression/decompress.test.ts +8 -4
  75. package/src/utils/compression/decompress.ts +2 -2
  76. package/src/utils/{computeProductId.ts → computeLegacyProductId.ts} +2 -2
  77. package/src/utils/constants.ts +5 -0
  78. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  79. package/src/utils/deepLinkWithFallback.ts +103 -0
  80. package/src/utils/formatAmount.ts +6 -0
  81. package/src/utils/iframeHelper.test.ts +18 -5
  82. package/src/utils/iframeHelper.ts +10 -3
  83. package/src/utils/index.ts +16 -1
  84. package/src/utils/merchantId.test.ts +653 -0
  85. package/src/utils/merchantId.ts +143 -0
  86. package/src/utils/sso.ts +18 -11
  87. package/src/utils/trackEvent.test.ts +23 -5
  88. package/src/utils/trackEvent.ts +13 -0
  89. package/cdn/bundle.iife.js +0 -14
  90. package/dist/actions-B5j-i1p0.cjs +0 -1
  91. package/dist/actions-q090Z0oR.js +0 -1
  92. package/dist/index-7OZ39x1U.d.ts +0 -195
  93. package/dist/index-CRsQWnTs.d.cts +0 -351
  94. package/dist/index-Ck1hudEi.d.ts +0 -351
  95. package/dist/index-zDq-VlKx.d.cts +0 -195
  96. package/dist/interaction-DMJ3ZfaF.d.cts +0 -45
  97. package/dist/interaction-KX1h9a7V.d.ts +0 -45
  98. package/dist/interactions-DnfM3oe0.js +0 -1
  99. package/dist/interactions-EIXhNLf6.cjs +0 -1
  100. package/dist/interactions.cjs +0 -1
  101. package/dist/interactions.d.cts +0 -2
  102. package/dist/interactions.d.ts +0 -2
  103. package/dist/interactions.js +0 -1
  104. package/dist/productTypes-BUkXJKZ7.cjs +0 -1
  105. package/dist/productTypes-CGb1MmBF.js +0 -1
  106. package/dist/src-1LQ4eLq5.js +0 -13
  107. package/dist/src-hW71KjPN.cjs +0 -13
  108. package/dist/trackEvent-CHnYa85W.js +0 -1
  109. package/dist/trackEvent-GuQm_1Nm.cjs +0 -1
  110. package/src/actions/getProductInformation.ts +0 -14
  111. package/src/actions/openSso.test.ts +0 -407
  112. package/src/actions/sendInteraction.test.ts +0 -219
  113. package/src/constants/interactionTypes.test.ts +0 -128
  114. package/src/constants/productTypes.test.ts +0 -130
  115. package/src/constants/productTypes.ts +0 -33
  116. package/src/interactions/index.ts +0 -5
  117. package/src/interactions/pressEncoder.test.ts +0 -215
  118. package/src/interactions/pressEncoder.ts +0 -53
  119. package/src/interactions/purchaseEncoder.test.ts +0 -291
  120. package/src/interactions/purchaseEncoder.ts +0 -99
  121. package/src/interactions/referralEncoder.test.ts +0 -170
  122. package/src/interactions/referralEncoder.ts +0 -47
  123. package/src/interactions/retailEncoder.test.ts +0 -107
  124. package/src/interactions/retailEncoder.ts +0 -37
  125. package/src/interactions/webshopEncoder.test.ts +0 -56
  126. package/src/interactions/webshopEncoder.ts +0 -30
  127. package/src/types/rpc/modal/openSession.ts +0 -25
  128. package/src/types/rpc/productInformation.ts +0 -59
  129. package/src/utils/computeProductId.test.ts +0 -80
  130. package/src/utils/sso.test.ts +0 -361
@@ -1,3 +1,7 @@
1
+ import { getBackendUrl } from "../utils/backendUrl";
2
+ import { getClientId } from "../utils/clientId";
3
+ import { fetchMerchantId } from "../utils/merchantId";
4
+
1
5
  /**
2
6
  * Function used to track the status of a purchase
3
7
  * when a purchase is tracked, the `purchaseCompleted` interactions will be automatically send for the user when we receive the purchase confirmation via webhook.
@@ -5,6 +9,7 @@
5
9
  * @param args.customerId - The customer id that made the purchase (on your side)
6
10
  * @param args.orderId - The order id of the purchase (on your side)
7
11
  * @param args.token - The token of the purchase
12
+ * @param args.merchantId - Optional explicit merchant id to use for the tracking request
8
13
  *
9
14
  * @description This function will send a request to the backend to listen for the purchase status.
10
15
  *
@@ -14,40 +19,72 @@
14
19
  * customerId: checkout.order.customer.id,
15
20
  * orderId: checkout.order.id,
16
21
  * token: checkout.token,
22
+ * merchantId: "your-merchant-id",
17
23
  * };
18
24
  *
19
25
  * await trackPurchaseStatus(payload);
20
26
  * }
21
27
  *
22
28
  * @remarks
23
- * - The `trackPurchaseStatus` function requires the `frak-wallet-interaction-token` stored in the session storage to authenticate the request.
24
- * - This function will print a warning if used in a non-browser environment or if the wallet interaction token is not available.
29
+ * - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
30
+ * - This function supports anonymous users and will use the `x-frak-client-id` header when available.
31
+ * - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
32
+ * - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
25
33
  */
26
34
  export async function trackPurchaseStatus(args: {
27
35
  customerId: string | number;
28
36
  orderId: string | number;
29
37
  token: string;
38
+ merchantId?: string;
30
39
  }) {
31
40
  if (typeof window === "undefined") {
32
41
  console.warn("[Frak] No window found, can't track purchase");
33
42
  return;
34
43
  }
44
+
35
45
  const interactionToken = window.sessionStorage.getItem(
36
46
  "frak-wallet-interaction-token"
37
47
  );
38
- if (!interactionToken) {
39
- console.warn("[Frak] No frak session found, skipping purchase check");
48
+
49
+ const clientId = getClientId();
50
+ if (!interactionToken && !clientId) {
51
+ console.warn("[Frak] No identity found, skipping purchase check");
52
+ return;
53
+ }
54
+
55
+ const merchantIdFromStorage =
56
+ window.sessionStorage.getItem("frak-merchant-id");
57
+ const merchantId =
58
+ args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
59
+
60
+ if (!merchantId) {
61
+ console.warn("[Frak] No merchant id found, skipping purchase check");
40
62
  return;
41
63
  }
42
64
 
65
+ const headers: Record<string, string> = {
66
+ Accept: "application/json",
67
+ "Content-Type": "application/json",
68
+ };
69
+
70
+ if (interactionToken) {
71
+ headers["x-wallet-sdk-auth"] = interactionToken;
72
+ }
73
+
74
+ if (clientId) {
75
+ headers["x-frak-client-id"] = clientId;
76
+ }
77
+
43
78
  // Submit the listening request
44
- await fetch("https://backend.frak.id/interactions/listenForPurchase", {
79
+ const backendUrl = getBackendUrl();
80
+ await fetch(`${backendUrl}/user/track/purchase`, {
45
81
  method: "POST",
46
- headers: {
47
- Accept: "application/json",
48
- "Content-Type": "application/json",
49
- "x-wallet-sdk-auth": interactionToken,
50
- },
51
- body: JSON.stringify(args),
82
+ headers,
83
+ body: JSON.stringify({
84
+ customerId: args.customerId,
85
+ orderId: args.orderId,
86
+ token: args.token,
87
+ merchantId,
88
+ }),
52
89
  });
53
90
  }
@@ -1,6 +1,7 @@
1
1
  import { Deferred } from "@frak-labs/frame-connector";
2
2
  import type { FrakClient } from "../types/client";
3
3
  import type { WalletStatusReturnType } from "../types/rpc/walletStatus";
4
+ import { ensureIdentity } from "./ensureIdentity";
4
5
 
5
6
  /**
6
7
  * Function used to watch the current frak wallet status
@@ -14,7 +15,6 @@ import type { WalletStatusReturnType } from "../types/rpc/walletStatus";
14
15
  * await watchWalletStatus(frakConfig, (status: WalletStatusReturnType) => {
15
16
  * if (status.key === "connected") {
16
17
  * console.log("Wallet connected:", status.wallet);
17
- * console.log("Current interaction session:", status.interactionSession);
18
18
  * } else {
19
19
  * console.log("Wallet not connected");
20
20
  * }
@@ -82,13 +82,12 @@ function walletStatusSideEffect(
82
82
  });
83
83
 
84
84
  if (status.interactionToken) {
85
- // If we got an interaction token, save it
86
85
  window.sessionStorage.setItem(
87
86
  "frak-wallet-interaction-token",
88
87
  status.interactionToken
89
88
  );
89
+ ensureIdentity(status.interactionToken);
90
90
  } else {
91
- // Otherwise, remove it
92
91
  window.sessionStorage.removeItem("frak-wallet-interaction-token");
93
92
  }
94
93
  }
@@ -25,7 +25,6 @@ describe("modalBuilder", () => {
25
25
 
26
26
  expect(builder.params).toBeDefined();
27
27
  expect(builder.params.steps.login).toEqual({});
28
- expect(builder.params.steps.openSession).toEqual({});
29
28
  });
30
29
 
31
30
  it("should create builder with custom login params", () => {
@@ -36,14 +35,6 @@ describe("modalBuilder", () => {
36
35
  expect(builder.params.steps.login).toEqual({ allowSso: true });
37
36
  });
38
37
 
39
- it("should create builder with custom openSession params", () => {
40
- const builder = modalBuilder(mockClient, {
41
- openSession: {},
42
- });
43
-
44
- expect(builder.params.steps.openSession).toEqual({});
45
- });
46
-
47
38
  it("should create builder with metadata", () => {
48
39
  const builder = modalBuilder(mockClient, {
49
40
  metadata: {
@@ -197,10 +188,6 @@ describe("modalBuilder", () => {
197
188
  login: {
198
189
  wallet: "0x1234567890123456789012345678901234567890" as Address,
199
190
  },
200
- openSession: {
201
- startTimestamp: 1234567890,
202
- endTimestamp: 1234567900,
203
- },
204
191
  };
205
192
  vi.mocked(displayModal).mockResolvedValue(mockResponse as any);
206
193
 
@@ -240,7 +227,6 @@ describe("modalBuilder", () => {
240
227
  login: {
241
228
  wallet: "0x1234567890123456789012345678901234567890" as Address,
242
229
  },
243
- openSession: {},
244
230
  };
245
231
  vi.mocked(displayModal).mockResolvedValue(mockResponse as any);
246
232
 
@@ -7,7 +7,6 @@ import type {
7
7
  ModalRpcMetadata,
8
8
  ModalRpcStepsResultType,
9
9
  ModalStepTypes,
10
- OpenInteractionSessionModalStepType,
11
10
  SendTransactionModalStepType,
12
11
  } from "../../types";
13
12
  import { displayModal } from "../displayModal";
@@ -58,9 +57,7 @@ export type ModalStepBuilder<
58
57
  /**
59
58
  * Represent the output type of the modal builder
60
59
  */
61
- export type ModalBuilder = ModalStepBuilder<
62
- [LoginModalStepType, OpenInteractionSessionModalStepType]
63
- >;
60
+ export type ModalBuilder = ModalStepBuilder<[LoginModalStepType]>;
64
61
 
65
62
  /**
66
63
  * Helper to craft Frak modal, and share a base initial config
@@ -68,9 +65,8 @@ export type ModalBuilder = ModalStepBuilder<
68
65
  * @param args
69
66
  * @param args.metadata - Common modal metadata (customisation, language etc)
70
67
  * @param args.login - Login step parameters
71
- * @param args.openSession - Open session step parameters
72
68
  *
73
- * @description This function will create a modal builder with the provided metadata, login and open session parameters.
69
+ * @description This function will create a modal builder with the provided metadata and login parameters.
74
70
  *
75
71
  * @example
76
72
  * Here is an example of how to use the `modalBuilder` to create and display a sharing modal:
@@ -102,20 +98,15 @@ export function modalBuilder(
102
98
  {
103
99
  metadata,
104
100
  login,
105
- openSession,
106
101
  }: {
107
102
  metadata?: ModalRpcMetadata;
108
103
  login?: LoginModalStepType["params"];
109
- openSession?: OpenInteractionSessionModalStepType["params"];
110
104
  }
111
105
  ): ModalBuilder {
112
106
  // Build the initial modal params
113
- const baseParams: DisplayModalParamsType<
114
- [LoginModalStepType, OpenInteractionSessionModalStepType]
115
- > = {
107
+ const baseParams: DisplayModalParamsType<[LoginModalStepType]> = {
116
108
  steps: {
117
109
  login: login ?? {},
118
- openSession: openSession ?? {},
119
110
  },
120
111
  metadata,
121
112
  };
package/src/bundle.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from "./actions";
2
2
  export * from "./index";
3
- export * from "./interactions";
@@ -4,12 +4,12 @@ import {
4
4
  type RpcClient,
5
5
  RpcErrorCodes,
6
6
  } from "@frak-labs/frame-connector";
7
- import { createClientCompressionMiddleware } from "@frak-labs/frame-connector/middleware";
8
7
  import { OpenPanel } from "@openpanel/web";
9
8
  import type { FrakLifecycleEvent } from "../types";
10
9
  import type { FrakClient } from "../types/client";
11
10
  import type { FrakWalletSdkConfig } from "../types/config";
12
11
  import type { IFrameRpcSchema } from "../types/rpc";
12
+ import { getClientId } from "../utils";
13
13
  import { BACKUP_KEY } from "../utils/constants";
14
14
  import { setupSsoUrlListener } from "../utils/ssoUrlListener";
15
15
  import { DebugInfoGatherer } from "./DebugInfo";
@@ -23,7 +23,8 @@ type SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
23
23
  /**
24
24
  * Create a new iframe Frak client
25
25
  * @param args
26
- * @param args.config - The configuration to use for the Frak Wallet SDK
26
+ * @param args.config - The configuration to use for the Frak Wallet SDK.
27
+ * When `config.domain` is set, it is forwarded to the iframe handshake so the listener resolves the correct merchant in tunneled/proxied environments (e.g. Shopify dev with Cloudflare tunnel).
27
28
  * @param args.iframe - The iframe to use for the communication
28
29
  * @returns The created Frak Client
29
30
  *
@@ -46,7 +47,11 @@ export function createIFrameFrakClient({
46
47
  const frakWalletUrl = config?.walletUrl ?? "https://wallet.frak.id";
47
48
 
48
49
  // Create lifecycle manager
49
- const lifecycleManager = createIFrameLifecycleManager({ iframe });
50
+ const lifecycleManager = createIFrameLifecycleManager({
51
+ iframe,
52
+ targetOrigin: frakWalletUrl,
53
+ configDomain: config.domain,
54
+ });
50
55
 
51
56
  // Create our debug info gatherer
52
57
  const debugInfo = new DebugInfoGatherer(config, iframe);
@@ -64,7 +69,6 @@ export function createIFrameFrakClient({
64
69
  emittingTransport: iframe.contentWindow,
65
70
  listeningTransport: window,
66
71
  targetOrigin: frakWalletUrl,
67
- // Add compression middleware to handle request/response compression
68
72
  middleware: [
69
73
  // Ensure we are connected before sending request
70
74
  {
@@ -80,7 +84,6 @@ export function createIFrameFrakClient({
80
84
  return ctx;
81
85
  },
82
86
  },
83
- createClientCompressionMiddleware(),
84
87
  // Save debug info
85
88
  {
86
89
  onRequest(message, ctx) {
@@ -139,6 +142,7 @@ export function createIFrameFrakClient({
139
142
  payload.properties = {
140
143
  ...payload.properties,
141
144
  sdkVersion: process.env.SDK_VERSION,
145
+ userAnonymousClientId: getClientId(),
142
146
  };
143
147
  }
144
148
 
@@ -147,6 +151,7 @@ export function createIFrameFrakClient({
147
151
  });
148
152
  openPanel.setGlobalProperties({
149
153
  sdkVersion: process.env.SDK_VERSION,
154
+ userAnonymousClientId: getClientId(),
150
155
  });
151
156
  openPanel.init();
152
157
  }
@@ -24,6 +24,17 @@ vi.mock("../../utils/iframeHelper", () => ({
24
24
  changeIframeVisibility: vi.fn(),
25
25
  }));
26
26
 
27
+ vi.mock("../../utils/clientId", () => ({
28
+ getClientId: vi.fn(() => "mock-client-id"),
29
+ }));
30
+
31
+ vi.mock("../../utils/deepLinkWithFallback", () => ({
32
+ isFrakDeepLink: vi.fn((url: string) => url.startsWith("frakwallet://")),
33
+ triggerDeepLinkWithFallback: vi.fn(),
34
+ }));
35
+
36
+ const WALLET_ORIGIN = "https://wallet.frak.id";
37
+
27
38
  describe("createIFrameLifecycleManager", () => {
28
39
  beforeEach(() => {
29
40
  vi.clearAllMocks();
@@ -45,6 +56,7 @@ describe("createIFrameLifecycleManager", () => {
45
56
  const mockIframe = document.createElement("iframe");
46
57
  const manager = createIFrameLifecycleManager({
47
58
  iframe: mockIframe,
59
+ targetOrigin: WALLET_ORIGIN,
48
60
  });
49
61
 
50
62
  expect(manager).toBeDefined();
@@ -60,6 +72,7 @@ describe("createIFrameLifecycleManager", () => {
60
72
  const mockIframe = document.createElement("iframe");
61
73
  const manager = createIFrameLifecycleManager({
62
74
  iframe: mockIframe,
75
+ targetOrigin: WALLET_ORIGIN,
63
76
  });
64
77
 
65
78
  let resolved = false;
@@ -82,6 +95,7 @@ describe("createIFrameLifecycleManager", () => {
82
95
  const mockIframe = document.createElement("iframe");
83
96
  const manager = createIFrameLifecycleManager({
84
97
  iframe: mockIframe,
98
+ targetOrigin: WALLET_ORIGIN,
85
99
  });
86
100
 
87
101
  const event = {
@@ -103,6 +117,7 @@ describe("createIFrameLifecycleManager", () => {
103
117
  const mockIframe = document.createElement("iframe");
104
118
  const manager = createIFrameLifecycleManager({
105
119
  iframe: mockIframe,
120
+ targetOrigin: WALLET_ORIGIN,
106
121
  });
107
122
 
108
123
  const backup = "encrypted-backup-data";
@@ -124,6 +139,7 @@ describe("createIFrameLifecycleManager", () => {
124
139
  const mockIframe = document.createElement("iframe");
125
140
  const manager = createIFrameLifecycleManager({
126
141
  iframe: mockIframe,
142
+ targetOrigin: WALLET_ORIGIN,
127
143
  });
128
144
 
129
145
  // First set a backup
@@ -147,6 +163,7 @@ describe("createIFrameLifecycleManager", () => {
147
163
  const mockIframe = document.createElement("iframe");
148
164
  const manager = createIFrameLifecycleManager({
149
165
  iframe: mockIframe,
166
+ targetOrigin: WALLET_ORIGIN,
150
167
  });
151
168
 
152
169
  // First set a backup
@@ -174,6 +191,7 @@ describe("createIFrameLifecycleManager", () => {
174
191
  const mockIframe = document.createElement("iframe");
175
192
  const manager = createIFrameLifecycleManager({
176
193
  iframe: mockIframe,
194
+ targetOrigin: WALLET_ORIGIN,
177
195
  });
178
196
 
179
197
  const event = {
@@ -199,6 +217,7 @@ describe("createIFrameLifecycleManager", () => {
199
217
  const mockIframe = document.createElement("iframe");
200
218
  const manager = createIFrameLifecycleManager({
201
219
  iframe: mockIframe,
220
+ targetOrigin: WALLET_ORIGIN,
202
221
  });
203
222
 
204
223
  const event = {
@@ -215,13 +234,14 @@ describe("createIFrameLifecycleManager", () => {
215
234
  });
216
235
 
217
236
  describe("handshake event", () => {
218
- test("should post handshake-response with token", async () => {
237
+ test("should post handshake-response with token to iframe origin", async () => {
219
238
  const { createIFrameLifecycleManager } = await import(
220
239
  "./iframeLifecycleManager"
221
240
  );
222
241
 
223
242
  const mockPostMessage = vi.fn();
224
243
  const mockIframe = {
244
+ src: "https://wallet.frak.id/listener",
225
245
  contentWindow: {
226
246
  postMessage: mockPostMessage,
227
247
  },
@@ -229,6 +249,7 @@ describe("createIFrameLifecycleManager", () => {
229
249
 
230
250
  const manager = createIFrameLifecycleManager({
231
251
  iframe: mockIframe,
252
+ targetOrigin: WALLET_ORIGIN,
232
253
  });
233
254
 
234
255
  const event = {
@@ -244,9 +265,10 @@ describe("createIFrameLifecycleManager", () => {
244
265
  data: {
245
266
  token: "handshake-token-123",
246
267
  currentUrl: "https://test.com",
268
+ clientId: "mock-client-id",
247
269
  },
248
270
  },
249
- "*"
271
+ "https://wallet.frak.id"
250
272
  );
251
273
  });
252
274
 
@@ -262,6 +284,7 @@ describe("createIFrameLifecycleManager", () => {
262
284
 
263
285
  const mockPostMessage = vi.fn();
264
286
  const mockIframe = {
287
+ src: "https://wallet.frak.id/listener",
265
288
  contentWindow: {
266
289
  postMessage: mockPostMessage,
267
290
  },
@@ -269,6 +292,7 @@ describe("createIFrameLifecycleManager", () => {
269
292
 
270
293
  const manager = createIFrameLifecycleManager({
271
294
  iframe: mockIframe,
295
+ targetOrigin: WALLET_ORIGIN,
272
296
  });
273
297
 
274
298
  const event = {
@@ -284,13 +308,13 @@ describe("createIFrameLifecycleManager", () => {
284
308
  currentUrl: "https://example.com/page?param=value",
285
309
  }),
286
310
  }),
287
- "*"
311
+ "https://wallet.frak.id"
288
312
  );
289
313
  });
290
314
  });
291
315
 
292
316
  describe("redirect event", () => {
293
- test("should redirect with appended current URL", async () => {
317
+ test("should redirect with appended current URL for HTTP URLs", async () => {
294
318
  const { createIFrameLifecycleManager } = await import(
295
319
  "./iframeLifecycleManager"
296
320
  );
@@ -305,6 +329,7 @@ describe("createIFrameLifecycleManager", () => {
305
329
  const mockIframe = document.createElement("iframe");
306
330
  const manager = createIFrameLifecycleManager({
307
331
  iframe: mockIframe,
332
+ targetOrigin: WALLET_ORIGIN,
308
333
  });
309
334
 
310
335
  const event = {
@@ -336,6 +361,7 @@ describe("createIFrameLifecycleManager", () => {
336
361
  const mockIframe = document.createElement("iframe");
337
362
  const manager = createIFrameLifecycleManager({
338
363
  iframe: mockIframe,
364
+ targetOrigin: WALLET_ORIGIN,
339
365
  });
340
366
 
341
367
  const event = {
@@ -349,6 +375,137 @@ describe("createIFrameLifecycleManager", () => {
349
375
 
350
376
  expect(window.location.href).toBe("https://redirect.com/path");
351
377
  });
378
+
379
+ test("should use fallback detection for frakwallet:// deep links", async () => {
380
+ const { createIFrameLifecycleManager } = await import(
381
+ "./iframeLifecycleManager"
382
+ );
383
+ const { triggerDeepLinkWithFallback } = await import(
384
+ "../../utils/deepLinkWithFallback"
385
+ );
386
+
387
+ Object.defineProperty(window, "location", {
388
+ value: {
389
+ href: "https://original.com",
390
+ },
391
+ writable: true,
392
+ });
393
+
394
+ const mockIframe = document.createElement("iframe");
395
+ mockIframe.src = "https://wallet.frak.id";
396
+ const manager = createIFrameLifecycleManager({
397
+ iframe: mockIframe,
398
+ targetOrigin: WALLET_ORIGIN,
399
+ });
400
+
401
+ const event = {
402
+ iframeLifecycle: "redirect" as const,
403
+ data: {
404
+ baseRedirectUrl: "frakwallet://wallet",
405
+ },
406
+ };
407
+
408
+ await manager.handleEvent(event);
409
+
410
+ expect(triggerDeepLinkWithFallback).toHaveBeenCalledWith(
411
+ "frakwallet://wallet",
412
+ expect.objectContaining({
413
+ onFallback: expect.any(Function),
414
+ })
415
+ );
416
+ });
417
+
418
+ test("should post deep-link-failed message when fallback is triggered", async () => {
419
+ const { createIFrameLifecycleManager } = await import(
420
+ "./iframeLifecycleManager"
421
+ );
422
+ const { triggerDeepLinkWithFallback } = await import(
423
+ "../../utils/deepLinkWithFallback"
424
+ );
425
+
426
+ Object.defineProperty(window, "location", {
427
+ value: {
428
+ href: "https://original.com",
429
+ },
430
+ writable: true,
431
+ });
432
+
433
+ const mockPostMessage = vi.fn();
434
+ const mockIframe = {
435
+ src: "https://wallet.frak.id",
436
+ contentWindow: {
437
+ postMessage: mockPostMessage,
438
+ },
439
+ } as any;
440
+
441
+ const manager = createIFrameLifecycleManager({
442
+ iframe: mockIframe,
443
+ targetOrigin: WALLET_ORIGIN,
444
+ });
445
+
446
+ const event = {
447
+ iframeLifecycle: "redirect" as const,
448
+ data: {
449
+ baseRedirectUrl: "frakwallet://wallet",
450
+ },
451
+ };
452
+
453
+ await manager.handleEvent(event);
454
+
455
+ // Extract the onFallback callback from the mock call
456
+ const callArgs = (triggerDeepLinkWithFallback as any).mock.calls[0];
457
+ const options = callArgs[1];
458
+ expect(options).toBeDefined();
459
+ expect(options.onFallback).toBeInstanceOf(Function);
460
+
461
+ // Trigger the fallback callback
462
+ options.onFallback();
463
+
464
+ // Verify postMessage was called with deep-link-failed event
465
+ expect(mockPostMessage).toHaveBeenCalledWith(
466
+ {
467
+ clientLifecycle: "deep-link-failed",
468
+ data: { originalUrl: "frakwallet://wallet" },
469
+ },
470
+ "https://wallet.frak.id"
471
+ );
472
+ });
473
+
474
+ test("should NOT use fallback detection for HTTP URLs", async () => {
475
+ const { createIFrameLifecycleManager } = await import(
476
+ "./iframeLifecycleManager"
477
+ );
478
+ const { triggerDeepLinkWithFallback } = await import(
479
+ "../../utils/deepLinkWithFallback"
480
+ );
481
+
482
+ Object.defineProperty(window, "location", {
483
+ value: {
484
+ href: "https://original.com",
485
+ },
486
+ writable: true,
487
+ });
488
+
489
+ const mockIframe = document.createElement("iframe");
490
+ const manager = createIFrameLifecycleManager({
491
+ iframe: mockIframe,
492
+ targetOrigin: WALLET_ORIGIN,
493
+ });
494
+
495
+ const event = {
496
+ iframeLifecycle: "redirect" as const,
497
+ data: {
498
+ baseRedirectUrl: "https://wallet.frak.id/login",
499
+ },
500
+ };
501
+
502
+ await manager.handleEvent(event);
503
+
504
+ // Should NOT call fallback detection
505
+ expect(triggerDeepLinkWithFallback).not.toHaveBeenCalled();
506
+ // Should directly redirect
507
+ expect(window.location.href).toBe("https://wallet.frak.id/login");
508
+ });
352
509
  });
353
510
 
354
511
  describe("event filtering", () => {
@@ -360,6 +517,7 @@ describe("createIFrameLifecycleManager", () => {
360
517
  const mockIframe = document.createElement("iframe");
361
518
  const manager = createIFrameLifecycleManager({
362
519
  iframe: mockIframe,
520
+ targetOrigin: WALLET_ORIGIN,
363
521
  });
364
522
 
365
523
  const event = {
@@ -381,6 +539,7 @@ describe("createIFrameLifecycleManager", () => {
381
539
  const mockIframe = document.createElement("iframe");
382
540
  const manager = createIFrameLifecycleManager({
383
541
  iframe: mockIframe,
542
+ targetOrigin: WALLET_ORIGIN,
384
543
  });
385
544
 
386
545
  // Event without iframeLifecycle