@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.
- package/README.md +58 -0
- package/cdn/bundle.js +3 -8
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -1400
- package/dist/actions.d.ts +3 -1400
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -13
- package/dist/bundle.d.cts +4 -1927
- package/dist/bundle.d.ts +4 -1927
- package/dist/bundle.js +1 -13
- package/dist/computeLegacyProductId-CscYhyUi.d.cts +525 -0
- package/dist/computeLegacyProductId-WbD1gXV9.d.ts +525 -0
- package/dist/index.cjs +1 -13
- package/dist/index.d.cts +3 -1269
- package/dist/index.d.ts +3 -1269
- package/dist/index.js +1 -13
- package/dist/openSso-CC1-loUk.d.cts +1019 -0
- package/dist/openSso-tkqaDQLV.d.ts +1019 -0
- package/dist/setupClient-BjIbK6XJ.cjs +13 -0
- package/dist/setupClient-D_HId3e2.js +13 -0
- package/dist/siweAuthenticate-B_Z2OZmj.cjs +1 -0
- package/dist/siweAuthenticate-CQ4OfPuA.js +1 -0
- package/dist/siweAuthenticate-CR4Dpji6.d.cts +467 -0
- package/dist/siweAuthenticate-udoruuy9.d.ts +467 -0
- package/dist/trackEvent-CGIryq5h.cjs +1 -0
- package/dist/trackEvent-YfUh4jrx.js +1 -0
- package/package.json +24 -30
- package/src/actions/displayEmbeddedWallet.test.ts +194 -0
- package/src/actions/displayEmbeddedWallet.ts +20 -0
- package/src/actions/displayModal.test.ts +388 -0
- package/src/actions/displayModal.ts +120 -0
- package/src/actions/getMerchantInformation.test.ts +116 -0
- package/src/actions/getMerchantInformation.ts +9 -0
- package/src/actions/index.ts +29 -0
- package/src/actions/openSso.ts +116 -0
- package/src/actions/prepareSso.test.ts +223 -0
- package/src/actions/prepareSso.ts +48 -0
- package/src/actions/referral/processReferral.test.ts +248 -0
- package/src/actions/referral/processReferral.ts +232 -0
- package/src/actions/referral/referralInteraction.test.ts +147 -0
- package/src/actions/referral/referralInteraction.ts +52 -0
- package/src/actions/sendInteraction.ts +24 -0
- package/src/actions/trackPurchaseStatus.test.ts +287 -0
- package/src/actions/trackPurchaseStatus.ts +56 -0
- package/src/actions/watchWalletStatus.test.ts +372 -0
- package/src/actions/watchWalletStatus.ts +93 -0
- package/src/actions/wrapper/modalBuilder.test.ts +239 -0
- package/src/actions/wrapper/modalBuilder.ts +203 -0
- package/src/actions/wrapper/sendTransaction.test.ts +164 -0
- package/src/actions/wrapper/sendTransaction.ts +62 -0
- package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
- package/src/actions/wrapper/siweAuthenticate.ts +94 -0
- package/src/bundle.ts +2 -0
- package/src/clients/DebugInfo.test.ts +418 -0
- package/src/clients/DebugInfo.ts +182 -0
- package/src/clients/createIFrameFrakClient.ts +289 -0
- package/src/clients/index.ts +3 -0
- package/src/clients/setupClient.test.ts +343 -0
- package/src/clients/setupClient.ts +73 -0
- package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
- package/src/clients/transports/iframeLifecycleManager.ts +174 -0
- package/src/constants/interactionTypes.ts +15 -0
- package/src/constants/locales.ts +14 -0
- package/src/index.ts +110 -0
- package/src/types/client.ts +14 -0
- package/src/types/compression.ts +22 -0
- package/src/types/config.ts +117 -0
- package/src/types/context.ts +13 -0
- package/src/types/index.ts +75 -0
- package/src/types/lifecycle/client.ts +69 -0
- package/src/types/lifecycle/iframe.ts +41 -0
- package/src/types/lifecycle/index.ts +2 -0
- package/src/types/rpc/displayModal.ts +82 -0
- package/src/types/rpc/embedded/index.ts +68 -0
- package/src/types/rpc/embedded/loggedIn.ts +55 -0
- package/src/types/rpc/embedded/loggedOut.ts +28 -0
- package/src/types/rpc/interaction.ts +30 -0
- package/src/types/rpc/merchantInformation.ts +77 -0
- package/src/types/rpc/modal/final.ts +46 -0
- package/src/types/rpc/modal/generic.ts +46 -0
- package/src/types/rpc/modal/index.ts +16 -0
- package/src/types/rpc/modal/login.ts +36 -0
- package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
- package/src/types/rpc/modal/transaction.ts +33 -0
- package/src/types/rpc/sso.ts +80 -0
- package/src/types/rpc/walletStatus.ts +29 -0
- package/src/types/rpc.ts +146 -0
- package/src/types/tracking.ts +60 -0
- package/src/types/transport.ts +34 -0
- package/src/utils/FrakContext.test.ts +407 -0
- package/src/utils/FrakContext.ts +158 -0
- package/src/utils/backendUrl.test.ts +83 -0
- package/src/utils/backendUrl.ts +62 -0
- package/src/utils/clientId.test.ts +41 -0
- package/src/utils/clientId.ts +40 -0
- package/src/utils/compression/b64.test.ts +181 -0
- package/src/utils/compression/b64.ts +29 -0
- package/src/utils/compression/compress.test.ts +123 -0
- package/src/utils/compression/compress.ts +11 -0
- package/src/utils/compression/decompress.test.ts +149 -0
- package/src/utils/compression/decompress.ts +11 -0
- package/src/utils/compression/index.ts +3 -0
- package/src/utils/computeLegacyProductId.ts +11 -0
- package/src/utils/constants.test.ts +23 -0
- package/src/utils/constants.ts +14 -0
- package/src/utils/deepLinkWithFallback.test.ts +243 -0
- package/src/utils/deepLinkWithFallback.ts +97 -0
- package/src/utils/formatAmount.test.ts +113 -0
- package/src/utils/formatAmount.ts +18 -0
- package/src/utils/getCurrencyAmountKey.test.ts +44 -0
- package/src/utils/getCurrencyAmountKey.ts +15 -0
- package/src/utils/getSupportedCurrency.test.ts +51 -0
- package/src/utils/getSupportedCurrency.ts +14 -0
- package/src/utils/getSupportedLocale.test.ts +64 -0
- package/src/utils/getSupportedLocale.ts +16 -0
- package/src/utils/iframeHelper.test.ts +450 -0
- package/src/utils/iframeHelper.ts +147 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/merchantId.test.ts +564 -0
- package/src/utils/merchantId.ts +122 -0
- package/src/utils/sso.ts +126 -0
- package/src/utils/ssoUrlListener.test.ts +252 -0
- package/src/utils/ssoUrlListener.ts +60 -0
- package/src/utils/trackEvent.test.ts +180 -0
- package/src/utils/trackEvent.ts +31 -0
- package/cdn/bundle.js.LICENSE.txt +0 -10
- package/dist/interactions.cjs +0 -1
- package/dist/interactions.d.cts +0 -182
- package/dist/interactions.d.ts +0 -182
- 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
|
+
}
|