@frak-labs/core-sdk
Version:
Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.
368 lines (307 loc) • 11.6 kB
text/typescript
import type { Address } from "viem";
import { vi } from "vitest";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "../../../tests/vitest-fixtures";
import type {
FrakClient,
FrakContext,
FrakContextV2,
WalletStatusReturnType,
} from "../../types";
import { processReferral } from "./processReferral";
vi.mock("../sendInteraction", () => ({
sendInteraction: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../context", () => ({
FrakContextManager: {
replaceUrl: vi.fn(),
},
}));
vi.mock("../../config/clientId", () => ({
getClientId: vi.fn().mockReturnValue("test-client-id"),
}));
vi.mock("../../utils", () => ({
trackEvent: vi.fn(),
}));
describe("processReferral", () => {
let mockClient: FrakClient;
let mockAddress: Address;
let mockWalletStatus: WalletStatusReturnType;
beforeEach(async () => {
vi.clearAllMocks();
mockAddress = "0x1234567890123456789012345678901234567890" as Address;
mockClient = {
openPanel: {
track: vi.fn(),
},
config: {
metadata: {
name: "Test App",
},
domain: "example.com",
},
request: vi.fn(),
} as unknown as FrakClient;
mockWalletStatus = {
key: "connected" as const,
wallet: mockAddress,
};
Object.defineProperty(window, "location", {
value: {
href: "https://example.com/test",
},
writable: true,
});
});
afterEach(() => {
vi.clearAllMocks();
});
it("should return 'no-referrer' when frakContext is null", async () => {
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: null,
});
expect(result).toBe("no-referrer");
});
describe("V2 context", () => {
const v2Context: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
it("should successfully process v2 referral", async () => {
const utils = await import("../../utils");
const { sendInteraction } = await import("../sendInteraction");
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2Context,
});
expect(result).toBe("success");
expect(utils.trackEvent).toHaveBeenCalledWith(
mockClient,
"user_referred_started",
{
referrerClientId: "referrer-client-id",
referrerWallet: undefined,
walletStatus: "connected",
}
);
expect(sendInteraction).toHaveBeenCalledWith(mockClient, {
type: "arrival",
referrerClientId: "referrer-client-id",
referrerMerchantId: "merchant-uuid",
referrerWallet: undefined,
referralTimestamp: 1709654400,
});
});
it("should return 'self-referral' when v2 context has same clientId as current user", async () => {
const clientIdMod = await import("../../config/clientId");
vi.mocked(clientIdMod.getClientId).mockReturnValue(
"referrer-client-id"
);
const v2SelfReferralContext: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2SelfReferralContext,
});
expect(result).toBe("self-referral");
vi.mocked(clientIdMod.getClientId).mockReturnValue(
"test-client-id"
);
});
it("should successfully process v2 referral with wallet only (no clientId)", async () => {
await import("../../utils");
const { sendInteraction } = await import("../sendInteraction");
const referrerWallet =
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as Address;
const v2WithWalletOnly: FrakContextV2 = {
v: 2,
m: "merchant-uuid",
t: 1709654400,
w: referrerWallet,
};
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2WithWalletOnly,
});
expect(result).toBe("success");
expect(sendInteraction).toHaveBeenCalledWith(mockClient, {
type: "arrival",
referrerClientId: undefined,
referrerMerchantId: "merchant-uuid",
referrerWallet,
referralTimestamp: 1709654400,
});
});
it("should return 'self-referral' when v2 wallet matches current wallet", async () => {
const v2SelfReferralByWallet: FrakContextV2 = {
v: 2,
m: "merchant-uuid",
t: 1709654400,
w: mockAddress,
};
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2SelfReferralByWallet,
});
expect(result).toBe("self-referral");
});
it("should prefer wallet over clientId for self-referral when both are present", async () => {
const clientIdMod = await import("../../config/clientId");
// clientId does NOT match current user, but wallet does → still self-referral
vi.mocked(clientIdMod.getClientId).mockReturnValue(
"some-other-client"
);
const v2Hybrid: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
w: mockAddress,
};
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2Hybrid,
});
expect(result).toBe("self-referral");
vi.mocked(clientIdMod.getClientId).mockReturnValue(
"test-client-id"
);
});
});
describe("V1 context (backward compat)", () => {
const v1Context: FrakContext = {
r: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" as Address,
};
it("should successfully process v1 referral", async () => {
const utils = await import("../../utils");
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v1Context,
});
expect(result).toBe("success");
expect(utils.trackEvent).toHaveBeenCalledWith(
mockClient,
"user_referred_started",
{
referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
walletStatus: "connected",
}
);
});
it("should return 'self-referral' when v1 referrer matches current wallet", async () => {
const result = await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: {
r: mockAddress,
},
});
expect(result).toBe("self-referral");
});
});
it("should update URL context when alwaysAppendUrl is true", async () => {
const clientIdMod = await import("../../config/clientId");
const contextMod = await import("../../context");
const v2Context: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2Context,
options: {
alwaysAppendUrl: true,
},
});
expect(clientIdMod.getClientId()).toBe("test-client-id");
expect(contextMod.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
url: window.location.href,
context: expect.objectContaining({
v: 2,
c: "test-client-id",
m: "merchant-uuid",
w: mockAddress,
}),
});
});
it("should remove URL context when alwaysAppendUrl is false", async () => {
const contextMod = await import("../../context");
const v2Context: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2Context,
options: {
alwaysAppendUrl: false,
},
});
expect(contextMod.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
url: window.location.href,
context: null,
});
});
it("should emit wallet in replacement context when alwaysAppendUrl is true and user is connected", async () => {
const clientIdMod = await import("../../config/clientId");
const contextMod = await import("../../context");
vi.mocked(clientIdMod.getClientId).mockReturnValue(null as never);
const v2Context: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
await processReferral(mockClient, {
walletStatus: mockWalletStatus,
frakContext: v2Context,
options: { alwaysAppendUrl: true },
});
// clientId is null, but wallet is available — should still emit {w, m}
expect(contextMod.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
url: window.location.href,
context: expect.objectContaining({
v: 2,
m: "merchant-uuid",
w: mockAddress,
}),
});
vi.mocked(clientIdMod.getClientId).mockReturnValue("test-client-id");
});
it("should return null replacement context when both clientId and wallet are missing", async () => {
const clientIdMod = await import("../../config/clientId");
const contextMod = await import("../../context");
vi.mocked(clientIdMod.getClientId).mockReturnValue(null as never);
const v2Context: FrakContextV2 = {
v: 2,
c: "referrer-client-id",
m: "merchant-uuid",
t: 1709654400,
};
await processReferral(mockClient, {
walletStatus: { key: "not-connected" as const },
frakContext: v2Context,
options: { alwaysAppendUrl: true },
});
expect(contextMod.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
url: window.location.href,
context: null,
});
vi.mocked(clientIdMod.getClientId).mockReturnValue("test-client-id");
});
});