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)
220 lines
7.6 KiB
TypeScript
220 lines
7.6 KiB
TypeScript
/**
|
|
* Browser/React Native-safe marketplace client for platform-service.
|
|
*
|
|
* Wraps platform-service /marketplace/* endpoints.
|
|
* No Node.js dependencies — uses globalThis.fetch.
|
|
*/
|
|
|
|
import type {
|
|
CreateListingInput,
|
|
MarketplaceClient,
|
|
MarketplaceClientConfig,
|
|
MarketplaceInstallDoc,
|
|
MarketplaceListingDoc,
|
|
MarketplaceReviewDoc,
|
|
} 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 createMarketplaceClient(config: MarketplaceClientConfig): MarketplaceClient {
|
|
const { baseUrl, productId, getAccessToken } = config;
|
|
|
|
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;
|
|
}
|
|
|
|
// ── Listings ──────────────────────────────────────
|
|
|
|
async function listListings(query?: {
|
|
templateType?: string;
|
|
category?: string;
|
|
tags?: string;
|
|
pricingModel?: string;
|
|
sortBy?: string;
|
|
q?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<{ listings: MarketplaceListingDoc[]; total: number }> {
|
|
const params = new URLSearchParams();
|
|
if (query?.templateType) params.set('templateType', query.templateType);
|
|
if (query?.category) params.set('category', query.category);
|
|
if (query?.tags) params.set('tags', query.tags);
|
|
if (query?.pricingModel) params.set('pricingModel', query.pricingModel);
|
|
if (query?.sortBy) params.set('sortBy', query.sortBy);
|
|
if (query?.q) params.set('q', query.q);
|
|
if (query?.limit) params.set('limit', String(query.limit));
|
|
if (query?.offset) params.set('offset', String(query.offset));
|
|
const qs = params.toString();
|
|
const url = qs ? `${baseUrl}/marketplace/listings?${qs}` : `${baseUrl}/marketplace/listings`;
|
|
const res = await globalThis.fetch(url, { headers: headers() });
|
|
if (!res.ok) throw new Error(`listListings failed: ${res.status}`);
|
|
return (await res.json()) as { listings: MarketplaceListingDoc[]; total: number };
|
|
}
|
|
|
|
async function getListing(id: string): Promise<MarketplaceListingDoc> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}`,
|
|
{ headers: headers() }
|
|
);
|
|
if (!res.ok) throw new Error(`getListing failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceListingDoc;
|
|
}
|
|
|
|
async function createListing(input: CreateListingInput): Promise<MarketplaceListingDoc> {
|
|
const res = await globalThis.fetch(`${baseUrl}/marketplace/listings`, {
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!res.ok) throw new Error(`createListing failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceListingDoc;
|
|
}
|
|
|
|
async function updateListing(
|
|
id: string,
|
|
updates: Partial<MarketplaceListingDoc>
|
|
): Promise<MarketplaceListingDoc> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}`,
|
|
{
|
|
method: 'PATCH',
|
|
headers: headers(),
|
|
body: JSON.stringify(updates),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`updateListing failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceListingDoc;
|
|
}
|
|
|
|
async function submitForCertification(
|
|
id: string,
|
|
notes?: string
|
|
): Promise<MarketplaceListingDoc> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}/submit`,
|
|
{
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify({ notes }),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`submitForCertification failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceListingDoc;
|
|
}
|
|
|
|
// ── Installs ──────────────────────────────────────
|
|
|
|
async function installListing(listingId: string): Promise<MarketplaceInstallDoc> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/install`,
|
|
{
|
|
method: 'POST',
|
|
headers: headers(),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`installListing failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceInstallDoc;
|
|
}
|
|
|
|
async function uninstallListing(listingId: string): Promise<void> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/uninstall`,
|
|
{
|
|
method: 'POST',
|
|
headers: headers(),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`uninstallListing failed: ${res.status}`);
|
|
}
|
|
|
|
async function listMyInstalls(query?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<MarketplaceInstallDoc[]> {
|
|
const params = new URLSearchParams();
|
|
if (query?.limit) params.set('limit', String(query.limit));
|
|
if (query?.offset) params.set('offset', String(query.offset));
|
|
const qs = params.toString();
|
|
const url = qs ? `${baseUrl}/marketplace/installs?${qs}` : `${baseUrl}/marketplace/installs`;
|
|
const res = await globalThis.fetch(url, { headers: headers() });
|
|
if (!res.ok) throw new Error(`listMyInstalls failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceInstallDoc[];
|
|
}
|
|
|
|
// ── Reviews ───────────────────────────────────────
|
|
|
|
async function listReviews(
|
|
listingId: string,
|
|
query?: { sortBy?: string; limit?: number }
|
|
): Promise<MarketplaceReviewDoc[]> {
|
|
const params = new URLSearchParams();
|
|
if (query?.sortBy) params.set('sortBy', query.sortBy);
|
|
if (query?.limit) params.set('limit', String(query.limit));
|
|
const qs = params.toString();
|
|
const url = qs
|
|
? `${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews?${qs}`
|
|
: `${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews`;
|
|
const res = await globalThis.fetch(url, { headers: headers() });
|
|
if (!res.ok) throw new Error(`listReviews failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceReviewDoc[];
|
|
}
|
|
|
|
async function createReview(
|
|
listingId: string,
|
|
input: { rating: number; title: string; body: string }
|
|
): Promise<MarketplaceReviewDoc> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews`,
|
|
{
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify(input),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`createReview failed: ${res.status}`);
|
|
return (await res.json()) as MarketplaceReviewDoc;
|
|
}
|
|
|
|
// ── Reports ───────────────────────────────────────
|
|
|
|
async function reportListing(
|
|
listingId: string,
|
|
input: { reason: string; details: string }
|
|
): Promise<void> {
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/report`,
|
|
{
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify(input),
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`reportListing failed: ${res.status}`);
|
|
}
|
|
|
|
return {
|
|
listListings,
|
|
getListing,
|
|
createListing,
|
|
updateListing,
|
|
submitForCertification,
|
|
installListing,
|
|
uninstallListing,
|
|
listMyInstalls,
|
|
listReviews,
|
|
createReview,
|
|
reportListing,
|
|
};
|
|
}
|