@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.4dfea079

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 +3 -8
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -1400
  5. package/dist/actions.d.ts +3 -1400
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -13
  8. package/dist/bundle.d.cts +4 -1927
  9. package/dist/bundle.d.ts +4 -1927
  10. package/dist/bundle.js +1 -13
  11. package/dist/computeLegacyProductId-CscYhyUi.d.cts +525 -0
  12. package/dist/computeLegacyProductId-WbD1gXV9.d.ts +525 -0
  13. package/dist/index.cjs +1 -13
  14. package/dist/index.d.cts +3 -1269
  15. package/dist/index.d.ts +3 -1269
  16. package/dist/index.js +1 -13
  17. package/dist/openSso-CC1-loUk.d.cts +1019 -0
  18. package/dist/openSso-tkqaDQLV.d.ts +1019 -0
  19. package/dist/setupClient-BjIbK6XJ.cjs +13 -0
  20. package/dist/setupClient-D_HId3e2.js +13 -0
  21. package/dist/siweAuthenticate-B_Z2OZmj.cjs +1 -0
  22. package/dist/siweAuthenticate-CQ4OfPuA.js +1 -0
  23. package/dist/siweAuthenticate-CR4Dpji6.d.cts +467 -0
  24. package/dist/siweAuthenticate-udoruuy9.d.ts +467 -0
  25. package/dist/trackEvent-CGIryq5h.cjs +1 -0
  26. package/dist/trackEvent-YfUh4jrx.js +1 -0
  27. package/package.json +24 -30
  28. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  29. package/src/actions/displayEmbeddedWallet.ts +20 -0
  30. package/src/actions/displayModal.test.ts +388 -0
  31. package/src/actions/displayModal.ts +120 -0
  32. package/src/actions/getMerchantInformation.test.ts +116 -0
  33. package/src/actions/getMerchantInformation.ts +9 -0
  34. package/src/actions/index.ts +29 -0
  35. package/src/actions/openSso.ts +116 -0
  36. package/src/actions/prepareSso.test.ts +223 -0
  37. package/src/actions/prepareSso.ts +48 -0
  38. package/src/actions/referral/processReferral.test.ts +248 -0
  39. package/src/actions/referral/processReferral.ts +232 -0
  40. package/src/actions/referral/referralInteraction.test.ts +147 -0
  41. package/src/actions/referral/referralInteraction.ts +52 -0
  42. package/src/actions/sendInteraction.ts +24 -0
  43. package/src/actions/trackPurchaseStatus.test.ts +287 -0
  44. package/src/actions/trackPurchaseStatus.ts +56 -0
  45. package/src/actions/watchWalletStatus.test.ts +372 -0
  46. package/src/actions/watchWalletStatus.ts +93 -0
  47. package/src/actions/wrapper/modalBuilder.test.ts +239 -0
  48. package/src/actions/wrapper/modalBuilder.ts +203 -0
  49. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  50. package/src/actions/wrapper/sendTransaction.ts +62 -0
  51. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  52. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  53. package/src/bundle.ts +2 -0
  54. package/src/clients/DebugInfo.test.ts +418 -0
  55. package/src/clients/DebugInfo.ts +182 -0
  56. package/src/clients/createIFrameFrakClient.ts +289 -0
  57. package/src/clients/index.ts +3 -0
  58. package/src/clients/setupClient.test.ts +343 -0
  59. package/src/clients/setupClient.ts +73 -0
  60. package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
  61. package/src/clients/transports/iframeLifecycleManager.ts +174 -0
  62. package/src/constants/interactionTypes.ts +15 -0
  63. package/src/constants/locales.ts +14 -0
  64. package/src/index.ts +110 -0
  65. package/src/types/client.ts +14 -0
  66. package/src/types/compression.ts +22 -0
  67. package/src/types/config.ts +117 -0
  68. package/src/types/context.ts +13 -0
  69. package/src/types/index.ts +75 -0
  70. package/src/types/lifecycle/client.ts +69 -0
  71. package/src/types/lifecycle/iframe.ts +41 -0
  72. package/src/types/lifecycle/index.ts +2 -0
  73. package/src/types/rpc/displayModal.ts +82 -0
  74. package/src/types/rpc/embedded/index.ts +68 -0
  75. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  76. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  77. package/src/types/rpc/interaction.ts +30 -0
  78. package/src/types/rpc/merchantInformation.ts +77 -0
  79. package/src/types/rpc/modal/final.ts +46 -0
  80. package/src/types/rpc/modal/generic.ts +46 -0
  81. package/src/types/rpc/modal/index.ts +16 -0
  82. package/src/types/rpc/modal/login.ts +36 -0
  83. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  84. package/src/types/rpc/modal/transaction.ts +33 -0
  85. package/src/types/rpc/sso.ts +80 -0
  86. package/src/types/rpc/walletStatus.ts +29 -0
  87. package/src/types/rpc.ts +146 -0
  88. package/src/types/tracking.ts +60 -0
  89. package/src/types/transport.ts +34 -0
  90. package/src/utils/FrakContext.test.ts +407 -0
  91. package/src/utils/FrakContext.ts +158 -0
  92. package/src/utils/backendUrl.test.ts +83 -0
  93. package/src/utils/backendUrl.ts +62 -0
  94. package/src/utils/clientId.test.ts +41 -0
  95. package/src/utils/clientId.ts +40 -0
  96. package/src/utils/compression/b64.test.ts +181 -0
  97. package/src/utils/compression/b64.ts +29 -0
  98. package/src/utils/compression/compress.test.ts +123 -0
  99. package/src/utils/compression/compress.ts +11 -0
  100. package/src/utils/compression/decompress.test.ts +149 -0
  101. package/src/utils/compression/decompress.ts +11 -0
  102. package/src/utils/compression/index.ts +3 -0
  103. package/src/utils/computeLegacyProductId.ts +11 -0
  104. package/src/utils/constants.test.ts +23 -0
  105. package/src/utils/constants.ts +14 -0
  106. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  107. package/src/utils/deepLinkWithFallback.ts +97 -0
  108. package/src/utils/formatAmount.test.ts +113 -0
  109. package/src/utils/formatAmount.ts +18 -0
  110. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  111. package/src/utils/getCurrencyAmountKey.ts +15 -0
  112. package/src/utils/getSupportedCurrency.test.ts +51 -0
  113. package/src/utils/getSupportedCurrency.ts +14 -0
  114. package/src/utils/getSupportedLocale.test.ts +64 -0
  115. package/src/utils/getSupportedLocale.ts +16 -0
  116. package/src/utils/iframeHelper.test.ts +450 -0
  117. package/src/utils/iframeHelper.ts +147 -0
  118. package/src/utils/index.ts +36 -0
  119. package/src/utils/merchantId.test.ts +564 -0
  120. package/src/utils/merchantId.ts +122 -0
  121. package/src/utils/sso.ts +126 -0
  122. package/src/utils/ssoUrlListener.test.ts +252 -0
  123. package/src/utils/ssoUrlListener.ts +60 -0
  124. package/src/utils/trackEvent.test.ts +180 -0
  125. package/src/utils/trackEvent.ts +31 -0
  126. package/cdn/bundle.js.LICENSE.txt +0 -10
  127. package/dist/interactions.cjs +0 -1
  128. package/dist/interactions.d.cts +0 -182
  129. package/dist/interactions.d.ts +0 -182
  130. package/dist/interactions.js +0 -1
@@ -0,0 +1,248 @@
1
+ import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
2
+ import type { Address } from "viem";
3
+ import { vi } from "vitest"; // Keep vi from vitest for vi.mock() hoisting
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ it,
10
+ } from "../../../tests/vitest-fixtures";
11
+ import type {
12
+ FrakClient,
13
+ FrakContext,
14
+ WalletStatusReturnType,
15
+ } from "../../types";
16
+ import { processReferral } from "./processReferral";
17
+
18
+ // Mock dependencies
19
+ vi.mock("../displayEmbeddedWallet", () => ({
20
+ displayEmbeddedWallet: vi.fn(),
21
+ }));
22
+
23
+ vi.mock("../sendInteraction", () => ({
24
+ sendInteraction: vi.fn().mockResolvedValue(undefined),
25
+ }));
26
+
27
+ vi.mock("../../utils", () => ({
28
+ FrakContextManager: {
29
+ replaceUrl: vi.fn(),
30
+ },
31
+ trackEvent: vi.fn(),
32
+ resolveMerchantId: vi.fn().mockResolvedValue(undefined),
33
+ }));
34
+
35
+ describe("processReferral", () => {
36
+ let mockClient: FrakClient;
37
+ let mockAddress: Address;
38
+ let mockReferrerAddress: Address;
39
+ let mockWalletStatus: WalletStatusReturnType;
40
+ let mockFrakContext: Partial<FrakContext>;
41
+
42
+ beforeEach(async () => {
43
+ vi.clearAllMocks();
44
+
45
+ mockAddress = "0x1234567890123456789012345678901234567890" as Address;
46
+ mockReferrerAddress =
47
+ "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" as Address;
48
+
49
+ mockClient = {
50
+ openPanel: {
51
+ track: vi.fn(),
52
+ },
53
+ config: {
54
+ metadata: {
55
+ name: "Test App",
56
+ },
57
+ domain: "example.com",
58
+ },
59
+ request: vi.fn(),
60
+ } as unknown as FrakClient;
61
+
62
+ mockWalletStatus = {
63
+ key: "connected" as const,
64
+ wallet: mockAddress,
65
+ };
66
+
67
+ mockFrakContext = {
68
+ r: mockReferrerAddress,
69
+ };
70
+
71
+ // Mock window.location
72
+ Object.defineProperty(window, "location", {
73
+ value: {
74
+ href: "https://example.com/test",
75
+ },
76
+ writable: true,
77
+ });
78
+ });
79
+
80
+ afterEach(() => {
81
+ vi.clearAllMocks();
82
+ });
83
+
84
+ it("should return 'no-referrer' when frakContext has no referrer", async () => {
85
+ const result = await processReferral(mockClient, {
86
+ walletStatus: mockWalletStatus,
87
+ frakContext: {},
88
+ });
89
+
90
+ expect(result).toBe("no-referrer");
91
+ });
92
+
93
+ it("should return 'no-referrer' when frakContext is null", async () => {
94
+ const result = await processReferral(mockClient, {
95
+ walletStatus: mockWalletStatus,
96
+ frakContext: null,
97
+ });
98
+
99
+ expect(result).toBe("no-referrer");
100
+ });
101
+
102
+ it("should return 'self-referral' when referrer equals current wallet", async () => {
103
+ const result = await processReferral(mockClient, {
104
+ walletStatus: mockWalletStatus,
105
+ frakContext: {
106
+ r: mockAddress, // Same as wallet
107
+ },
108
+ });
109
+
110
+ expect(result).toBe("self-referral");
111
+ });
112
+
113
+ it("should successfully process referral when all conditions are met", async () => {
114
+ const utils = await import("../../utils");
115
+
116
+ const result = await processReferral(mockClient, {
117
+ walletStatus: mockWalletStatus,
118
+ frakContext: mockFrakContext,
119
+ });
120
+
121
+ expect(result).toBe("success");
122
+
123
+ expect(utils.trackEvent).toHaveBeenCalledWith(
124
+ mockClient,
125
+ "user_referred_started",
126
+ {
127
+ properties: {
128
+ referrer: mockReferrerAddress,
129
+ walletStatus: "connected",
130
+ },
131
+ }
132
+ );
133
+
134
+ expect(utils.trackEvent).toHaveBeenCalledWith(
135
+ mockClient,
136
+ "user_referred_completed",
137
+ {
138
+ properties: {
139
+ status: "success",
140
+ referrer: mockReferrerAddress,
141
+ wallet: mockAddress,
142
+ },
143
+ }
144
+ );
145
+ });
146
+
147
+ it("should handle wallet not connected scenario", async () => {
148
+ const { displayEmbeddedWallet } = await import(
149
+ "../displayEmbeddedWallet"
150
+ );
151
+
152
+ // Mock displayEmbeddedWallet to return a wallet
153
+ vi.mocked(displayEmbeddedWallet).mockResolvedValue({
154
+ wallet: mockAddress,
155
+ } as any);
156
+
157
+ const result = await processReferral(mockClient, {
158
+ walletStatus: undefined,
159
+ frakContext: mockFrakContext,
160
+ });
161
+
162
+ expect(result).toBe("success");
163
+ expect(displayEmbeddedWallet).toHaveBeenCalled();
164
+ });
165
+
166
+ it("should return 'no-wallet' when wallet connection fails", async () => {
167
+ const { displayEmbeddedWallet } = await import(
168
+ "../displayEmbeddedWallet"
169
+ );
170
+
171
+ const error = new FrakRpcError(
172
+ RpcErrorCodes.walletNotConnected,
173
+ "Wallet not connected"
174
+ );
175
+ vi.mocked(displayEmbeddedWallet).mockRejectedValue(error);
176
+
177
+ const result = await processReferral(mockClient, {
178
+ walletStatus: undefined,
179
+ frakContext: mockFrakContext,
180
+ });
181
+
182
+ expect(result).toBe("no-wallet");
183
+ });
184
+
185
+ it("should return 'error' for unknown errors", async () => {
186
+ const { displayEmbeddedWallet } = await import(
187
+ "../displayEmbeddedWallet"
188
+ );
189
+
190
+ const error = new Error("Unknown error");
191
+ vi.mocked(displayEmbeddedWallet).mockRejectedValue(error);
192
+
193
+ const result = await processReferral(mockClient, {
194
+ walletStatus: undefined,
195
+ frakContext: mockFrakContext,
196
+ });
197
+
198
+ expect(result).toBe("error");
199
+ });
200
+
201
+ it("should update URL context when alwaysAppendUrl is true", async () => {
202
+ const utils = await import("../../utils");
203
+
204
+ await processReferral(mockClient, {
205
+ walletStatus: mockWalletStatus,
206
+ frakContext: mockFrakContext,
207
+ options: {
208
+ alwaysAppendUrl: true,
209
+ },
210
+ });
211
+
212
+ expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
213
+ url: window.location.href,
214
+ context: { r: mockAddress },
215
+ });
216
+ });
217
+
218
+ it("should remove URL context when alwaysAppendUrl is false", async () => {
219
+ const utils = await import("../../utils");
220
+
221
+ await processReferral(mockClient, {
222
+ walletStatus: mockWalletStatus,
223
+ frakContext: mockFrakContext,
224
+ options: {
225
+ alwaysAppendUrl: false,
226
+ },
227
+ });
228
+
229
+ expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
230
+ url: window.location.href,
231
+ context: null,
232
+ });
233
+ });
234
+
235
+ it("should remove URL context by default", async () => {
236
+ const utils = await import("../../utils");
237
+
238
+ await processReferral(mockClient, {
239
+ walletStatus: mockWalletStatus,
240
+ frakContext: mockFrakContext,
241
+ });
242
+
243
+ expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
244
+ url: window.location.href,
245
+ context: null,
246
+ });
247
+ });
248
+ });
@@ -0,0 +1,232 @@
1
+ import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
2
+ import { type Address, isAddressEqual } from "viem";
3
+ import type {
4
+ DisplayEmbeddedWalletParamsType,
5
+ FrakClient,
6
+ FrakContext,
7
+ WalletStatusReturnType,
8
+ } from "../../types";
9
+ import { FrakContextManager, trackEvent } from "../../utils";
10
+ import { displayEmbeddedWallet } from "../displayEmbeddedWallet";
11
+ import { sendInteraction } from "../sendInteraction";
12
+
13
+ /**
14
+ * The different states of the referral process
15
+ * @inline
16
+ */
17
+ type ReferralState =
18
+ | "idle"
19
+ | "processing"
20
+ | "success"
21
+ | "no-wallet"
22
+ | "error"
23
+ | "no-referrer"
24
+ | "self-referral";
25
+
26
+ /**
27
+ * Options for the referral auto-interaction process
28
+ */
29
+ export type ProcessReferralOptions = {
30
+ /**
31
+ * If we want to always append the url with the frak context or not
32
+ * @defaultValue false
33
+ */
34
+ alwaysAppendUrl?: boolean;
35
+ };
36
+
37
+ /**
38
+ * This function handle all the heavy lifting of the referral interaction process
39
+ * 1. Check if the user has been referred or not (if not, early exit)
40
+ * 2. Then check if the user is logged in or not
41
+ * 2.1 If not logged in, try a soft login, if it fail, display a modal for the user to login
42
+ * 3. Check if that's not a self-referral (if yes, early exit)
43
+ * 4. Track the referral event
44
+ * 5. Update the current url with the right data
45
+ * 6. Return the resulting referral state
46
+ *
47
+ * If any error occurs during the process, the function will catch it and return an error state
48
+ *
49
+ * @param client - The current Frak Client
50
+ * @param args
51
+ * @param args.walletStatus - The current user wallet status
52
+ * @param args.frakContext - The current frak context
53
+ * @param args.modalConfig - The modal configuration to display if the user is not logged in
54
+ * @param args.options - Some options for the referral interaction
55
+ * @returns A promise with the resulting referral state
56
+ *
57
+ * @see {@link displayModal} for more details about the displayed modal
58
+ * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
59
+ */
60
+ export async function processReferral(
61
+ client: FrakClient,
62
+ {
63
+ walletStatus,
64
+ frakContext,
65
+ modalConfig,
66
+ options,
67
+ }: {
68
+ walletStatus?: WalletStatusReturnType;
69
+ frakContext?: Partial<FrakContext> | null;
70
+ modalConfig?: DisplayEmbeddedWalletParamsType;
71
+ options?: ProcessReferralOptions;
72
+ }
73
+ ) {
74
+ // Early exit if we don't have any referral informations
75
+ if (!frakContext?.r) {
76
+ return "no-referrer";
77
+ }
78
+
79
+ // If we got a context, log an event
80
+ trackEvent(client, "user_referred_started", {
81
+ properties: {
82
+ referrer: frakContext?.r,
83
+ walletStatus: walletStatus?.key,
84
+ },
85
+ });
86
+
87
+ sendInteraction(client, {
88
+ type: "arrival",
89
+ referrerWallet: frakContext.r,
90
+ landingUrl:
91
+ typeof window !== "undefined" ? window.location.href : undefined,
92
+ });
93
+
94
+ // Helper to fetch a fresh wallet status
95
+ let walletRequest = false;
96
+ async function getFreshWalletStatus() {
97
+ if (walletRequest) {
98
+ return;
99
+ }
100
+ walletRequest = true;
101
+ return ensureWalletConnected(client, {
102
+ modalConfig: {
103
+ ...modalConfig,
104
+ loggedIn: {
105
+ action: {
106
+ key: "referred",
107
+ },
108
+ },
109
+ },
110
+ walletStatus,
111
+ });
112
+ }
113
+
114
+ try {
115
+ // Do the core processing logic
116
+ const { status, currentWallet } = await processReferralLogic({
117
+ initialWalletStatus: walletStatus,
118
+ getFreshWalletStatus,
119
+ // We can enforce this type cause of the condition at the start
120
+ frakContext: frakContext as Pick<FrakContext, "r">,
121
+ });
122
+
123
+ // Update the current url with the right data
124
+ FrakContextManager.replaceUrl({
125
+ url: window.location?.href,
126
+ context: options?.alwaysAppendUrl ? { r: currentWallet } : null,
127
+ });
128
+
129
+ // Track the event
130
+ trackEvent(client, "user_referred_completed", {
131
+ properties: {
132
+ status,
133
+ referrer: frakContext?.r,
134
+ wallet: currentWallet,
135
+ },
136
+ });
137
+
138
+ return status;
139
+ } catch (error) {
140
+ console.log("Error processing referral", { error });
141
+
142
+ // Track the error event
143
+ trackEvent(client, "user_referred_error", {
144
+ properties: {
145
+ referrer: frakContext?.r,
146
+ error:
147
+ error instanceof FrakRpcError
148
+ ? `[${error.code}] ${error.name} - ${error.message}`
149
+ : error instanceof Error
150
+ ? error.message
151
+ : "undefined",
152
+ },
153
+ });
154
+
155
+ // Update the current url with the right data
156
+ FrakContextManager.replaceUrl({
157
+ url: window.location?.href,
158
+ context: options?.alwaysAppendUrl
159
+ ? { r: walletStatus?.wallet }
160
+ : null,
161
+ });
162
+
163
+ // And map the error a state
164
+ return mapErrorToState(error);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Process referral logic - ensure user is logged in and check for self-referral
170
+ */
171
+ async function processReferralLogic({
172
+ initialWalletStatus,
173
+ getFreshWalletStatus,
174
+ frakContext,
175
+ }: {
176
+ initialWalletStatus?: WalletStatusReturnType;
177
+ getFreshWalletStatus: () => Promise<Address | undefined>;
178
+ frakContext: Pick<FrakContext, "r">;
179
+ }) {
180
+ // Get the current wallet, without auto displaying the modal
181
+ let currentWallet = initialWalletStatus?.wallet;
182
+
183
+ // If we don't have a current wallet, display the modal to log in
184
+ if (!currentWallet) {
185
+ currentWallet = await getFreshWalletStatus();
186
+ }
187
+
188
+ // Check for self-referral
189
+ if (currentWallet && isAddressEqual(frakContext.r, currentWallet)) {
190
+ return { status: "self-referral", currentWallet } as const;
191
+ }
192
+
193
+ return { status: "success", currentWallet } as const;
194
+ }
195
+
196
+ /**
197
+ * Helper to ensure a wallet is connected, and display a modal if we got everything needed
198
+ */
199
+ async function ensureWalletConnected(
200
+ client: FrakClient,
201
+ {
202
+ modalConfig,
203
+ walletStatus,
204
+ }: {
205
+ modalConfig?: DisplayEmbeddedWalletParamsType;
206
+ walletStatus?: WalletStatusReturnType;
207
+ }
208
+ ) {
209
+ // If wallet not connected, display modal
210
+ if (walletStatus?.key !== "connected") {
211
+ const result = await displayEmbeddedWallet(client, modalConfig ?? {});
212
+ return result?.wallet ?? undefined;
213
+ }
214
+
215
+ return walletStatus.wallet ?? undefined;
216
+ }
217
+
218
+ /**
219
+ * Helper to map an error to a state
220
+ * @param error
221
+ */
222
+ function mapErrorToState(error: unknown): ReferralState {
223
+ if (error instanceof FrakRpcError) {
224
+ switch (error.code) {
225
+ case RpcErrorCodes.walletNotConnected:
226
+ return "no-wallet";
227
+ default:
228
+ return "error";
229
+ }
230
+ }
231
+ return "error";
232
+ }
@@ -0,0 +1,147 @@
1
+ import type { Hex } from "viem";
2
+ import { beforeEach, describe, expect, test, vi } from "vitest";
3
+ import { referralInteraction } from "./referralInteraction";
4
+
5
+ vi.mock("../../utils", () => ({
6
+ FrakContextManager: {
7
+ parse: vi.fn(),
8
+ },
9
+ }));
10
+
11
+ vi.mock("../index", () => ({
12
+ watchWalletStatus: vi.fn(),
13
+ }));
14
+
15
+ vi.mock("./processReferral", () => ({
16
+ processReferral: vi.fn(),
17
+ }));
18
+
19
+ describe("referralInteraction", () => {
20
+ const mockClient = {
21
+ request: vi.fn(),
22
+ } as any;
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ Object.defineProperty(global, "window", {
27
+ value: { location: { href: "https://example.com?frak=test" } },
28
+ writable: true,
29
+ });
30
+ });
31
+
32
+ test("should parse context from window location", async () => {
33
+ const { FrakContextManager } = await import("../../utils");
34
+ const { watchWalletStatus } = await import("../index");
35
+ const { processReferral } = await import("./processReferral");
36
+
37
+ vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
38
+ vi.mocked(watchWalletStatus).mockResolvedValue(null as any);
39
+ vi.mocked(processReferral).mockResolvedValue("success");
40
+
41
+ await referralInteraction(mockClient);
42
+
43
+ expect(FrakContextManager.parse).toHaveBeenCalledWith({
44
+ url: "https://example.com?frak=test",
45
+ });
46
+ });
47
+
48
+ test("should get current wallet status", async () => {
49
+ const { FrakContextManager } = await import("../../utils");
50
+ const { watchWalletStatus } = await import("../index");
51
+ const { processReferral } = await import("./processReferral");
52
+
53
+ vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
54
+ vi.mocked(watchWalletStatus).mockResolvedValue({
55
+ key: "connected",
56
+ wallet: "0x123" as Hex,
57
+ } as any);
58
+ vi.mocked(processReferral).mockResolvedValue("success");
59
+
60
+ await referralInteraction(mockClient);
61
+
62
+ expect(watchWalletStatus).toHaveBeenCalledWith(mockClient);
63
+ });
64
+
65
+ test("should call processReferral with all parameters", async () => {
66
+ const { FrakContextManager } = await import("../../utils");
67
+ const { watchWalletStatus } = await import("../index");
68
+ const { processReferral } = await import("./processReferral");
69
+
70
+ const mockContext = { r: "0xreferrer" as Hex };
71
+ const mockWalletStatus = { wallet: "0x123" as Hex };
72
+ const mockModalConfig = { type: "login" };
73
+ const mockOptions = { alwaysAppendUrl: true };
74
+
75
+ vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext as any);
76
+ vi.mocked(watchWalletStatus).mockResolvedValue(mockWalletStatus as any);
77
+ vi.mocked(processReferral).mockResolvedValue("success");
78
+
79
+ await referralInteraction(mockClient, {
80
+ modalConfig: mockModalConfig as any,
81
+ options: mockOptions,
82
+ });
83
+
84
+ expect(processReferral).toHaveBeenCalledWith(mockClient, {
85
+ walletStatus: mockWalletStatus,
86
+ frakContext: mockContext,
87
+ modalConfig: mockModalConfig,
88
+ options: mockOptions,
89
+ });
90
+ });
91
+
92
+ test("should return result from processReferral", async () => {
93
+ const { FrakContextManager } = await import("../../utils");
94
+ const { watchWalletStatus } = await import("../index");
95
+ const { processReferral } = await import("./processReferral");
96
+
97
+ vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
98
+ vi.mocked(watchWalletStatus).mockResolvedValue(null as any);
99
+ vi.mocked(processReferral).mockResolvedValue("success");
100
+
101
+ const result = await referralInteraction(mockClient);
102
+
103
+ expect(result).toBe("success");
104
+ });
105
+
106
+ test("should return undefined on error", async () => {
107
+ const { FrakContextManager } = await import("../../utils");
108
+ const { watchWalletStatus } = await import("../index");
109
+ const { processReferral } = await import("./processReferral");
110
+
111
+ vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
112
+ vi.mocked(watchWalletStatus).mockResolvedValue(null as any);
113
+ vi.mocked(processReferral).mockRejectedValue(new Error("Test error"));
114
+
115
+ const consoleSpy = vi
116
+ .spyOn(console, "warn")
117
+ .mockImplementation(() => {});
118
+
119
+ const result = await referralInteraction(mockClient);
120
+
121
+ expect(result).toBeUndefined();
122
+ expect(consoleSpy).toHaveBeenCalled();
123
+
124
+ consoleSpy.mockRestore();
125
+ });
126
+
127
+ test("should work with empty options", async () => {
128
+ const { FrakContextManager } = await import("../../utils");
129
+ const { watchWalletStatus } = await import("../index");
130
+ const { processReferral } = await import("./processReferral");
131
+
132
+ vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
133
+ vi.mocked(watchWalletStatus).mockResolvedValue(null as any);
134
+ vi.mocked(processReferral).mockResolvedValue("no-referrer");
135
+
136
+ const result = await referralInteraction(mockClient, {});
137
+
138
+ expect(result).toBe("no-referrer");
139
+ expect(processReferral).toHaveBeenCalledWith(
140
+ mockClient,
141
+ expect.objectContaining({
142
+ modalConfig: undefined,
143
+ options: undefined,
144
+ })
145
+ );
146
+ });
147
+ });
@@ -0,0 +1,52 @@
1
+ import type { DisplayEmbeddedWalletParamsType, FrakClient } from "../../types";
2
+ import { FrakContextManager } from "../../utils";
3
+ import { watchWalletStatus } from "../index";
4
+ import {
5
+ type ProcessReferralOptions,
6
+ processReferral,
7
+ } from "./processReferral";
8
+
9
+ /**
10
+ * Function used to handle referral interactions
11
+ * @param client - The current Frak Client
12
+ * @param args
13
+ * @param args.modalConfig - The modal configuration to display if the user is not logged in
14
+ * @param args.options - Some options for the referral interaction
15
+ *
16
+ * @returns A promise with the resulting referral state, or undefined in case of an error
17
+ *
18
+ * @description This function will automatically handle the referral interaction process
19
+ *
20
+ * @see {@link processReferral} for more details on the automatic referral handling process
21
+ * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
22
+ */
23
+ export async function referralInteraction(
24
+ client: FrakClient,
25
+ {
26
+ modalConfig,
27
+ options,
28
+ }: {
29
+ modalConfig?: DisplayEmbeddedWalletParamsType;
30
+ options?: ProcessReferralOptions;
31
+ } = {}
32
+ ) {
33
+ // Get the current frak context
34
+ const frakContext = FrakContextManager.parse({
35
+ url: window.location.href,
36
+ });
37
+
38
+ // Get the current wallet status
39
+ const currentWalletStatus = await watchWalletStatus(client);
40
+
41
+ try {
42
+ return await processReferral(client, {
43
+ walletStatus: currentWalletStatus,
44
+ frakContext,
45
+ modalConfig,
46
+ options,
47
+ });
48
+ } catch (error) {
49
+ console.warn("Error processing referral", { error });
50
+ }
51
+ return;
52
+ }
@@ -0,0 +1,24 @@
1
+ import type { FrakClient } from "../types";
2
+ import type { SendInteractionParamsType } from "../types/rpc/interaction";
3
+
4
+ /**
5
+ * Send an interaction to the backend via the listener RPC.
6
+ * Fire-and-forget: errors are caught and logged, not thrown.
7
+ *
8
+ * @param client - The Frak client instance
9
+ * @param params - The interaction parameters
10
+ */
11
+ export async function sendInteraction(
12
+ client: FrakClient,
13
+ params: SendInteractionParamsType
14
+ ): Promise<void> {
15
+ try {
16
+ await client.request({
17
+ method: "frak_sendInteraction",
18
+ params: [params],
19
+ });
20
+ } catch {
21
+ // Silent failure - fire-and-forget
22
+ console.warn("[Frak SDK] Failed to send interaction:", params.type);
23
+ }
24
+ }