Packages added: - @bytelyst/referral-client — referral API client + share helpers - @bytelyst/subscription-client — subscription/plan API client + cache - @bytelyst/celebrations — milestone triggers, confetti, positive messages - @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases - @bytelyst/accessibility — VoiceOver/TalkBack label generators - @bytelyst/quick-actions — progressive disclosure, smart defaults - @bytelyst/time-references — familiar duration references - @bytelyst/org-client — org/workspace/membership/license API client - @bytelyst/marketplace-client — listing/review/install API client All packages: pure TS, ESM, globalThis.fetch, no Node.js deps. 99 Vitest tests across 9 packages, 79/79 public methods covered. Review fixes applied: - time-references: fix module-level mutable state leak + add clearCustomReferences() - accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords - subscription-client: fix flaky daysRemaining test (ms boundary race)
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
/**
|
|
* Browser/React Native-safe referral client for platform-service.
|
|
*
|
|
* Wraps platform-service /referrals/* endpoints.
|
|
* No Node.js dependencies — uses globalThis.fetch.
|
|
*/
|
|
|
|
import type { ReferralClient, ReferralClientConfig, ReferralDoc } from './types.js';
|
|
|
|
function generateRequestId(): string {
|
|
return typeof globalThis.crypto?.randomUUID === 'function'
|
|
? globalThis.crypto.randomUUID()
|
|
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
}
|
|
|
|
export function createReferralClient(config: ReferralClientConfig): ReferralClient {
|
|
const { baseUrl, productId, getAccessToken, defaultRewardTokens } = config;
|
|
|
|
const defaultReferrerTokens = defaultRewardTokens?.referrer ?? 1000;
|
|
const defaultReferredTokens = defaultRewardTokens?.referred ?? 500;
|
|
|
|
function headers(): Record<string, string> {
|
|
const h: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'x-product-id': productId,
|
|
'x-request-id': generateRequestId(),
|
|
};
|
|
const token = getAccessToken();
|
|
if (token) h['Authorization'] = `Bearer ${token}`;
|
|
return h;
|
|
}
|
|
|
|
async function listMyReferrals(
|
|
referrerId: string
|
|
): Promise<{ referrals: ReferralDoc[]; count: number }> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/referrals/by-referrer/${encodeURIComponent(referrerId)}`,
|
|
{ headers: headers() }
|
|
);
|
|
if (!res.ok) throw new Error(`listMyReferrals failed: ${res.status}`);
|
|
const data = (await res.json()) as { referrals: ReferralDoc[]; count: number };
|
|
return data;
|
|
}
|
|
|
|
async function getReferralStats(): Promise<{
|
|
total: number;
|
|
completed: number;
|
|
rewarded: number;
|
|
}> {
|
|
const res = await globalThis.fetch(`${baseUrl}/referrals/stats`, { headers: headers() });
|
|
if (!res.ok) throw new Error(`getReferralStats failed: ${res.status}`);
|
|
return (await res.json()) as { total: number; completed: number; rewarded: number };
|
|
}
|
|
|
|
async function createReferral(input: {
|
|
referrerId: string;
|
|
referrerEmail: string;
|
|
referredEmail: string;
|
|
}): Promise<ReferralDoc> {
|
|
const body = {
|
|
...input,
|
|
productId,
|
|
referrerRewardTokens: defaultReferrerTokens,
|
|
referredRewardTokens: defaultReferredTokens,
|
|
};
|
|
const res = await globalThis.fetch(`${baseUrl}/referrals`, {
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) throw new Error(`createReferral failed: ${res.status}`);
|
|
return (await res.json()) as ReferralDoc;
|
|
}
|
|
|
|
async function updateReferralStatus(
|
|
id: string,
|
|
referrerId: string,
|
|
status: ReferralDoc['status']
|
|
): Promise<ReferralDoc> {
|
|
const res = await globalThis.fetch(`${baseUrl}/referrals/${encodeURIComponent(id)}`, {
|
|
method: 'PUT',
|
|
headers: headers(),
|
|
body: JSON.stringify({ referrerId, status }),
|
|
});
|
|
if (!res.ok) throw new Error(`updateReferralStatus failed: ${res.status}`);
|
|
return (await res.json()) as ReferralDoc;
|
|
}
|
|
|
|
async function getByEmail(email: string): Promise<ReferralDoc | null> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/referrals/by-email/${encodeURIComponent(email)}`,
|
|
{ headers: headers() }
|
|
);
|
|
if (res.status === 404) return null;
|
|
if (!res.ok) throw new Error(`getByEmail failed: ${res.status}`);
|
|
return (await res.json()) as ReferralDoc;
|
|
}
|
|
|
|
function buildShareLink(code: string): string {
|
|
return `https://bytelyst.com/refer/${encodeURIComponent(code)}?product=${encodeURIComponent(productId)}`;
|
|
}
|
|
|
|
function buildShareMessage(code: string, productName: string): string {
|
|
const link = buildShareLink(code);
|
|
return `Try ${productName}! Use my referral link to get started: ${link}`;
|
|
}
|
|
|
|
function calculateEarnedDays(conversions: number, daysPerReferral = 7): number {
|
|
return Math.max(0, conversions * daysPerReferral);
|
|
}
|
|
|
|
return {
|
|
listMyReferrals,
|
|
getReferralStats,
|
|
createReferral,
|
|
updateReferralStatus,
|
|
getByEmail,
|
|
buildShareLink,
|
|
buildShareMessage,
|
|
calculateEarnedDays,
|
|
};
|
|
}
|