@frak-labs/core-sdk
Version:
Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.
194 lines (178 loc) • 5.7 kB
text/typescript
import { getClientId } from "../../config/clientId";
import { FrakContextManager } from "../../context";
import { areAddressesEqual } from "../../context/address";
import type {
FrakClient,
FrakContext,
FrakContextV2,
WalletStatusReturnType,
} from "../../types";
import { isV1Context, isV2Context } from "../../types";
import { trackEvent } from "../../utils";
import { sendInteraction } from "../sendInteraction";
/**
* Options for the referral auto-interaction process.
*/
export type ProcessReferralOptions = {
/**
* If true, always replace the URL with the current user's referral context
* so the next visitor gets referred by this user.
* @defaultValue false
*/
alwaysAppendUrl?: boolean;
/**
* Merchant ID for building the current user's referral context.
* Required when `alwaysAppendUrl` is true and the incoming context is V1.
* For V2 contexts, the merchantId is already embedded in the context.
*/
merchantId?: string;
};
/**
* The different states of the referral process.
* @inline
*/
type ReferralState =
| "idle"
| "processing"
| "success"
| "no-referrer"
| "self-referral";
/**
* Track an arrival event if the context version is recognized.
* Sends both tracking analytics and the arrival interaction RPC.
*
* @returns true if the context was valid and tracked, false otherwise
*/
function trackArrivalIfValid(
client: FrakClient,
frakContext: FrakContext,
walletStatus?: WalletStatusReturnType
): boolean {
if (isV2Context(frakContext)) {
trackEvent(client, "user_referred_started", {
referrerClientId: frakContext.c,
referrerWallet: frakContext.w,
walletStatus: walletStatus?.key,
});
sendInteraction(client, {
type: "arrival",
referrerClientId: frakContext.c,
referrerMerchantId: frakContext.m,
referrerWallet: frakContext.w,
referralTimestamp: frakContext.t,
});
return true;
}
if (isV1Context(frakContext)) {
trackEvent(client, "user_referred_started", {
referrer: frakContext.r,
walletStatus: walletStatus?.key,
});
sendInteraction(client, {
type: "arrival",
referrerWallet: frakContext.r,
});
return true;
}
return false;
}
/**
* Build a V2 context representing the current user for URL replacement.
*
* Emits both `c` (anonymous clientId) and `w` (wallet) when available — wallet
* is the preferred identity signal and takes attribution precedence downstream.
* Returns null when neither identifier is available.
*/
function buildCurrentUserContext(
merchantId: string,
wallet?: WalletStatusReturnType["wallet"]
): FrakContextV2 | null {
const clientId = getClientId();
if (!clientId && !wallet) return null;
return {
v: 2,
m: merchantId,
t: Math.floor(Date.now() / 1000),
...(clientId ? { c: clientId } : {}),
...(wallet ? { w: wallet } : {}),
};
}
/**
* Client-side self-referral preflight check.
* Prevents unnecessary backend round-trips for obvious self-referrals.
*/
function isSelfReferral(
frakContext: FrakContext,
walletStatus?: WalletStatusReturnType
): boolean {
if (isV2Context(frakContext)) {
// Wallet match takes precedence — it's the strongest signal we have.
if (frakContext.w && walletStatus?.wallet) {
return areAddressesEqual(frakContext.w, walletStatus.wallet);
}
if (frakContext.c) {
return getClientId() === frakContext.c;
}
return false;
}
if (isV1Context(frakContext) && walletStatus?.wallet) {
return areAddressesEqual(frakContext.r, walletStatus.wallet);
}
return false;
}
/**
* Handle the full referral interaction flow:
*
* 1. Check if the user has been referred (if not, early exit)
* 2. Preflight self-referral check (if yes, early exit)
* 3. Track the arrival event
* 4. Replace the current URL with the user's own referral context
* 5. Return the resulting referral state
*
* @param client - The current Frak Client
* @param args
* @param args.walletStatus - The current user wallet status
* @param args.frakContext - The referral context parsed from the URL
* @param args.options - Options for URL replacement and merchant context
* @returns The referral state
*
* @see {@link @frak-labs/core-sdk!ModalStepTypes} for modal step types
*/
export function processReferral(
client: FrakClient,
{
walletStatus,
frakContext,
options,
}: {
walletStatus?: WalletStatusReturnType;
frakContext?: FrakContext | null;
options?: ProcessReferralOptions;
}
): ReferralState {
if (!frakContext) {
return "no-referrer";
}
if (isSelfReferral(frakContext, walletStatus)) {
return "self-referral";
}
if (!trackArrivalIfValid(client, frakContext, walletStatus)) {
return "no-referrer";
}
// V2 context embeds merchantId; V1 falls back to options
const contextMerchantId = isV2Context(frakContext)
? frakContext.m
: options?.merchantId;
const replaceContext =
options?.alwaysAppendUrl && contextMerchantId
? buildCurrentUserContext(contextMerchantId, walletStatus?.wallet)
: null;
FrakContextManager.replaceUrl({
url: window.location?.href,
context: replaceContext,
});
trackEvent(client, "user_referred_completed", {
status: "success",
});
return "success";
}