@onborn/billing 0.1.0-beta.1

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.
@@ -0,0 +1,58 @@
1
+ import { type CustomerEntitlementsResponse, type GetOfferingResponse, type GetPaywallResponse, type PurchaseValidationResponse, type RestorePurchasesRequest, type ValidatePurchaseRequest } from "@onborn/sdk-contracts";
2
+ export type BillingClientOptions = {
3
+ sourceId?: string;
4
+ };
5
+ export declare class BillingClient {
6
+ private readonly options;
7
+ private readonly config;
8
+ private readonly userId;
9
+ private readonly fetchImpl;
10
+ private readonly emitAnalyticsEvents;
11
+ constructor(options?: BillingClientOptions);
12
+ loadPaywall(paywallId: string): Promise<GetPaywallResponse>;
13
+ loadOffering(offeringId: string): Promise<GetOfferingResponse>;
14
+ validatePurchase(input: Omit<ValidatePurchaseRequest, "userId">): Promise<PurchaseValidationResponse>;
15
+ restorePurchases(input: Omit<RestorePurchasesRequest, "userId">): Promise<PurchaseValidationResponse>;
16
+ loadCustomerEntitlements(): Promise<CustomerEntitlementsResponse>;
17
+ trackPaywallPackageSelected(params: PaywallPackageEvent & {
18
+ packageId: string;
19
+ }): Promise<void>;
20
+ trackPaywallPurchaseStarted(params: PaywallPackageEvent): Promise<void>;
21
+ trackPaywallTrialStarted(params: PaywallPackageEvent & {
22
+ trialPeriod?: string;
23
+ }): Promise<void>;
24
+ trackPaywallPurchaseFailed(params: PaywallPackageEvent & {
25
+ reason: "cancelled" | "error" | "pending";
26
+ message?: string;
27
+ }): Promise<void>;
28
+ trackPaywallConverted(params: PaywallBaseEvent & {
29
+ productId: string;
30
+ priceUsd?: number;
31
+ }): Promise<void>;
32
+ trackPaywallRestoreStarted(params: PaywallBaseEvent): Promise<void>;
33
+ trackPaywallRestoreCompleted(params: PaywallBaseEvent & {
34
+ restored: boolean;
35
+ }): Promise<void>;
36
+ trackPaywallRestoreFailed(params: PaywallBaseEvent & {
37
+ message?: string;
38
+ }): Promise<void>;
39
+ flushEvents(): Promise<void>;
40
+ private runtimeUrl;
41
+ private getJson;
42
+ private sendPurchaseRequest;
43
+ private authorizationHeaders;
44
+ private track;
45
+ }
46
+ type PaywallBaseEvent = {
47
+ sessionId: string;
48
+ stepId: string;
49
+ paywallId?: string;
50
+ paywallTemplate: string;
51
+ variant?: string;
52
+ };
53
+ type PaywallPackageEvent = PaywallBaseEvent & {
54
+ packageId?: string;
55
+ productId?: string;
56
+ };
57
+ export declare function createBillingClient(options?: BillingClientOptions): BillingClient;
58
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,148 @@
1
+ import { Onborn, } from "@onborn/analytics";
2
+ import { resolveOnbornBillingConfig } from "./runtime";
3
+ import { CustomerEntitlementsResponseSchema, GetOfferingResponseSchema, GetPaywallResponseSchema, PurchaseValidationResponseSchema, } from "@onborn/sdk-contracts";
4
+ const ONBORN_API_BASE_URL = "https://api.testing.onborn.app";
5
+ export class BillingClient {
6
+ options;
7
+ config;
8
+ userId;
9
+ fetchImpl;
10
+ emitAnalyticsEvents;
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ this.config = resolveOnbornBillingConfig();
14
+ this.userId = this.config.userId ?? createAnonymousUserId();
15
+ this.fetchImpl = this.config.fetchImpl ?? fetch;
16
+ this.emitAnalyticsEvents = this.config.emitAnalyticsEvents !== false;
17
+ }
18
+ async loadPaywall(paywallId) {
19
+ const url = this.runtimeUrl(`/paywalls/${encodeURIComponent(paywallId)}`);
20
+ const payload = await this.getJson(url, `paywall '${paywallId}'`);
21
+ const parsed = GetPaywallResponseSchema.safeParse(payload);
22
+ if (!parsed.success) {
23
+ throw new Error("Invalid paywall response payload");
24
+ }
25
+ return parsed.data;
26
+ }
27
+ async loadOffering(offeringId) {
28
+ const url = this.runtimeUrl(`/offerings/${encodeURIComponent(offeringId)}`);
29
+ const payload = await this.getJson(url, `offering '${offeringId}'`);
30
+ const parsed = GetOfferingResponseSchema.safeParse(payload);
31
+ if (!parsed.success) {
32
+ throw new Error("Invalid offering response payload");
33
+ }
34
+ return parsed.data;
35
+ }
36
+ async validatePurchase(input) {
37
+ return this.sendPurchaseRequest("/purchases/validate", {
38
+ ...input,
39
+ userId: this.userId,
40
+ });
41
+ }
42
+ async restorePurchases(input) {
43
+ return this.sendPurchaseRequest("/purchases/restore", {
44
+ ...input,
45
+ userId: this.userId,
46
+ });
47
+ }
48
+ async loadCustomerEntitlements() {
49
+ const url = new URL(`${ONBORN_API_BASE_URL}/entitlements`);
50
+ url.searchParams.set("userId", this.userId);
51
+ const payload = await this.getJson(url, "customer entitlements");
52
+ const parsed = CustomerEntitlementsResponseSchema.safeParse(payload);
53
+ if (!parsed.success) {
54
+ throw new Error("Invalid customer entitlements response payload");
55
+ }
56
+ return parsed.data;
57
+ }
58
+ trackPaywallPackageSelected(params) {
59
+ return this.track({ type: "paywall_package_selected", ...params });
60
+ }
61
+ trackPaywallPurchaseStarted(params) {
62
+ return this.track({ type: "paywall_purchase_started", ...params });
63
+ }
64
+ trackPaywallTrialStarted(params) {
65
+ return this.track({ type: "paywall_trial_started", ...params });
66
+ }
67
+ trackPaywallPurchaseFailed(params) {
68
+ return this.track({ type: "paywall_purchase_failed", ...params });
69
+ }
70
+ trackPaywallConverted(params) {
71
+ return this.track({ type: "paywall_converted", ...params });
72
+ }
73
+ trackPaywallRestoreStarted(params) {
74
+ return this.track({ type: "paywall_restore_started", ...params });
75
+ }
76
+ trackPaywallRestoreCompleted(params) {
77
+ return this.track({ type: "paywall_restore_completed", ...params });
78
+ }
79
+ trackPaywallRestoreFailed(params) {
80
+ return this.track({ type: "paywall_restore_failed", ...params });
81
+ }
82
+ async flushEvents() {
83
+ if (this.emitAnalyticsEvents) {
84
+ await Onborn.flush();
85
+ }
86
+ }
87
+ runtimeUrl(path) {
88
+ const url = new URL(`${ONBORN_API_BASE_URL}${path}`);
89
+ appendParam(url, "userId", this.userId);
90
+ appendParam(url, "locale", this.config.locale);
91
+ appendParam(url, "platform", this.config.platform);
92
+ appendParam(url, "country", this.config.country);
93
+ appendParam(url, "appVersion", this.config.appVersion);
94
+ appendParam(url, "userType", this.config.userType);
95
+ return url;
96
+ }
97
+ async getJson(url, label) {
98
+ const response = await this.fetchImpl(url.toString(), {
99
+ headers: this.authorizationHeaders(),
100
+ });
101
+ if (!response.ok) {
102
+ throw new Error(`Failed to fetch ${label} (${response.status})`);
103
+ }
104
+ return response.json();
105
+ }
106
+ async sendPurchaseRequest(path, payload) {
107
+ const response = await this.fetchImpl(`${ONBORN_API_BASE_URL}${path}`, {
108
+ method: "POST",
109
+ headers: {
110
+ ...this.authorizationHeaders(),
111
+ "Content-Type": "application/json",
112
+ },
113
+ body: JSON.stringify(payload),
114
+ });
115
+ if (!response.ok) {
116
+ throw new Error(`Purchase request failed (${response.status})`);
117
+ }
118
+ const parsed = PurchaseValidationResponseSchema.safeParse(await response.json());
119
+ if (!parsed.success) {
120
+ throw new Error("Invalid purchase validation response payload");
121
+ }
122
+ return parsed.data;
123
+ }
124
+ authorizationHeaders() {
125
+ return { Authorization: `Bearer ${this.config.apiKey}` };
126
+ }
127
+ async track(input) {
128
+ if (!this.emitAnalyticsEvents) {
129
+ return;
130
+ }
131
+ await Onborn.track({
132
+ ...input,
133
+ flowId: this.options.sourceId ?? "billing",
134
+ userId: this.userId,
135
+ });
136
+ }
137
+ }
138
+ export function createBillingClient(options = {}) {
139
+ return new BillingClient(options);
140
+ }
141
+ function appendParam(url, key, value) {
142
+ if (value) {
143
+ url.searchParams.set(key, value);
144
+ }
145
+ }
146
+ function createAnonymousUserId() {
147
+ return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
148
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./adapters";
2
+ export * from "./client";
3
+ export * from "./runtime";
4
+ export * from "./types";
5
+ export * from "./useOnbornEntitlements";
6
+ export * from "./useOnbornOffering";
7
+ export * from "./useOnbornPaywall";
8
+ export * from "./utils";
9
+ export * from "./validation";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./adapters";
2
+ export * from "./client";
3
+ export * from "./runtime";
4
+ export * from "./types";
5
+ export * from "./useOnbornEntitlements";
6
+ export * from "./useOnbornOffering";
7
+ export * from "./useOnbornPaywall";
8
+ export * from "./utils";
9
+ export * from "./validation";
@@ -0,0 +1,5 @@
1
+ import { Onborn, type OnbornConfig } from "@onborn/analytics";
2
+ export { Onborn };
3
+ export type { OnbornConfig };
4
+ export declare function resolveOnbornBillingConfig<T extends object>(overrides?: T): T & OnbornConfig;
5
+ export declare function useOnbornBillingConfig<T extends object>(overrides?: T): T & OnbornConfig;
@@ -0,0 +1,38 @@
1
+ import { Onborn, } from "@onborn/analytics";
2
+ const GLOBAL_CONFIG_KEYS = [
3
+ "apiKey",
4
+ "userId",
5
+ "locale",
6
+ "appId",
7
+ "platform",
8
+ "country",
9
+ "appVersion",
10
+ "userType",
11
+ "sdkVersion",
12
+ "fetchImpl",
13
+ "emitAnalyticsEvents",
14
+ "emitSdkConnectionSignal",
15
+ "autoFlushMs",
16
+ "maxAnalyticsBatchSize",
17
+ "maxAnalyticsQueueSize",
18
+ "analyticsQueueKey",
19
+ "analyticsStorage",
20
+ ];
21
+ export { Onborn };
22
+ export function resolveOnbornBillingConfig(overrides) {
23
+ const globalConfig = Onborn.getConfig();
24
+ if (!globalConfig) {
25
+ throw new Error("Onborn is not initialized. Call Onborn.init({ apiKey, ...config }) before using Onborn billing hooks.");
26
+ }
27
+ const safeOverrides = { ...(overrides ?? {}) };
28
+ for (const key of GLOBAL_CONFIG_KEYS) {
29
+ delete safeOverrides[key];
30
+ }
31
+ return {
32
+ ...globalConfig,
33
+ ...safeOverrides,
34
+ };
35
+ }
36
+ export function useOnbornBillingConfig(overrides) {
37
+ return resolveOnbornBillingConfig(overrides);
38
+ }
@@ -0,0 +1,78 @@
1
+ import type { BillingOffering, BillingPlatform, BillingPackage, BillingProduct, CustomerEntitlement, GetFlowResponse, GetOfferingResponse, GetPaywallResponse, NativeStoreRestoredPurchase, PaywallConfig, PurchaseStatus } from "@onborn/sdk-contracts";
2
+ export type { CustomerEntitlement } from "@onborn/sdk-contracts";
3
+ export type OnbornPackageWithProduct = {
4
+ package: BillingPackage;
5
+ product?: BillingProduct;
6
+ };
7
+ export type OnbornPurchaseInput = {
8
+ paywall?: PaywallConfig;
9
+ offering: BillingOffering;
10
+ package: BillingPackage;
11
+ product?: BillingProduct;
12
+ userId?: string;
13
+ };
14
+ export type OnbornPurchaseResult = {
15
+ success: boolean;
16
+ status?: PurchaseStatus;
17
+ purchaseId?: string;
18
+ transactionId?: string;
19
+ purchaseToken?: string;
20
+ receipt?: string;
21
+ productId?: string;
22
+ packageId?: string;
23
+ entitlementIds?: string[];
24
+ activeProductIds?: string[];
25
+ entitlements?: CustomerEntitlement[];
26
+ raw?: unknown;
27
+ };
28
+ export type OnbornRestoreInput = {
29
+ paywall?: PaywallConfig;
30
+ offering?: BillingOffering;
31
+ products: BillingProduct[];
32
+ userId?: string;
33
+ };
34
+ export type OnbornLoadProductsInput = {
35
+ paywall?: PaywallConfig;
36
+ offering?: BillingOffering;
37
+ products: BillingProduct[];
38
+ userId?: string;
39
+ };
40
+ export type OnbornRestoreResult = {
41
+ success: boolean;
42
+ status?: PurchaseStatus;
43
+ purchaseId?: string;
44
+ entitlementIds?: string[];
45
+ activeProductIds?: string[];
46
+ purchases?: NativeStoreRestoredPurchase[];
47
+ entitlements?: CustomerEntitlement[];
48
+ raw?: unknown;
49
+ };
50
+ export type OnbornBillingAdapter = {
51
+ loadProducts?: (input: OnbornLoadProductsInput) => Promise<BillingProduct[]>;
52
+ purchasePackage: (input: OnbornPurchaseInput) => Promise<OnbornPurchaseResult>;
53
+ restorePurchases?: (input: OnbornRestoreInput) => Promise<OnbornRestoreResult>;
54
+ refetchCustomerEntitlements?: (input: {
55
+ userId?: string;
56
+ }) => Promise<OnbornRestoreResult>;
57
+ };
58
+ export type OnbornPaywallRuntimeContext = {
59
+ paywall?: PaywallConfig;
60
+ offering?: BillingOffering;
61
+ products?: BillingProduct[];
62
+ platform?: BillingPlatform;
63
+ presentationMode?: "standalone" | "flow";
64
+ experiment?: NonNullable<GetFlowResponse["experiment"]>;
65
+ selectedPackageId?: string;
66
+ onSelectPackage?: (packageId: string) => void;
67
+ onPurchaseSelectedPackage?: () => void;
68
+ onRestorePurchases?: () => void;
69
+ onDismissPaywall?: () => void;
70
+ onLinkPress?: (link: {
71
+ url: string;
72
+ label?: string;
73
+ }) => void;
74
+ purchasing?: boolean;
75
+ restoring?: boolean;
76
+ };
77
+ export type OnbornPaywallData = GetPaywallResponse;
78
+ export type OnbornOfferingData = GetOfferingResponse;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { type CustomerEntitlementsResponse } from "@onborn/sdk-contracts";
2
+ export type UseOnbornEntitlementsOptions = {
3
+ autoLoad?: boolean;
4
+ };
5
+ export type UseOnbornEntitlementsState = {
6
+ data: CustomerEntitlementsResponse | null;
7
+ loading: boolean;
8
+ error: string | null;
9
+ reload: () => Promise<CustomerEntitlementsResponse>;
10
+ hasEntitlement: (keyOrId: string) => boolean;
11
+ };
12
+ export declare function useOnbornEntitlements(options?: UseOnbornEntitlementsOptions): UseOnbornEntitlementsState;
@@ -0,0 +1,59 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { createBillingClient } from "./client";
3
+ import { useOnbornBillingConfig } from "./runtime";
4
+ export function useOnbornEntitlements(options = {}) {
5
+ const runtimeOptions = useOnbornBillingConfig(options);
6
+ const [data, setData] = useState(null);
7
+ const [loading, setLoading] = useState(options.autoLoad !== false);
8
+ const [error, setError] = useState(null);
9
+ const client = useMemo(() => createBillingClient({
10
+ sourceId: "entitlements",
11
+ }), [
12
+ runtimeOptions.apiKey,
13
+ runtimeOptions.appId,
14
+ runtimeOptions.appVersion,
15
+ runtimeOptions.autoFlushMs,
16
+ runtimeOptions.emitAnalyticsEvents,
17
+ runtimeOptions.emitSdkConnectionSignal,
18
+ runtimeOptions.fetchImpl,
19
+ runtimeOptions.locale,
20
+ runtimeOptions.platform,
21
+ runtimeOptions.sdkVersion,
22
+ runtimeOptions.userId,
23
+ ]);
24
+ const reload = useCallback(async () => {
25
+ setLoading(true);
26
+ setError(null);
27
+ try {
28
+ const response = await client.loadCustomerEntitlements();
29
+ setData(response);
30
+ return response;
31
+ }
32
+ catch (loadError) {
33
+ const message = toError(loadError).message;
34
+ setError(message);
35
+ throw loadError;
36
+ }
37
+ finally {
38
+ setLoading(false);
39
+ }
40
+ }, [client]);
41
+ useEffect(() => {
42
+ if (options.autoLoad === false) {
43
+ return;
44
+ }
45
+ void reload();
46
+ }, [options.autoLoad, reload]);
47
+ const hasEntitlement = useCallback((keyOrId) => data?.entitlements.some((item) => item.active &&
48
+ (item.key === keyOrId || item.entitlementId === keyOrId)) ?? false, [data?.entitlements]);
49
+ return {
50
+ data,
51
+ loading,
52
+ error,
53
+ reload,
54
+ hasEntitlement,
55
+ };
56
+ }
57
+ function toError(error) {
58
+ return error instanceof Error ? error : new Error("Unknown error");
59
+ }
@@ -0,0 +1,29 @@
1
+ import { type CustomerEntitlement, type GetOfferingResponse } from "@onborn/sdk-contracts";
2
+ import type { OnbornBillingAdapter, OnbornPackageWithProduct, OnbornPurchaseResult, OnbornRestoreResult } from "./types";
3
+ export type UseOnbornOfferingOptions = {
4
+ offeringId: string;
5
+ initialPackageId?: string;
6
+ billingAdapter?: OnbornBillingAdapter;
7
+ onPurchaseStarted?: (item: OnbornPackageWithProduct) => void;
8
+ onPurchaseCompleted?: (result: OnbornPurchaseResult) => void;
9
+ onPurchaseFailed?: (error: Error) => void;
10
+ onRestoreCompleted?: (result: OnbornRestoreResult) => void;
11
+ onRestoreFailed?: (error: Error) => void;
12
+ onEntitlementsChanged?: (entitlements: CustomerEntitlement[]) => void;
13
+ };
14
+ export type UseOnbornOfferingState = {
15
+ data: GetOfferingResponse | null;
16
+ packages: OnbornPackageWithProduct[];
17
+ selectedPackage: OnbornPackageWithProduct | null;
18
+ selectedPackageId: string | null;
19
+ loading: boolean;
20
+ purchasing: boolean;
21
+ restoring: boolean;
22
+ error: string | null;
23
+ selectPackage: (packageId: string) => void;
24
+ reload: () => Promise<void>;
25
+ purchasePackage: (packageId?: string) => Promise<OnbornPurchaseResult>;
26
+ restorePurchases: () => Promise<OnbornRestoreResult>;
27
+ refetchCustomerEntitlements: () => Promise<OnbornRestoreResult>;
28
+ };
29
+ export declare function useOnbornOffering(options: UseOnbornOfferingOptions): UseOnbornOfferingState;
@@ -0,0 +1,194 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { createBillingClient } from "./client";
3
+ import { useOnbornBillingConfig } from "./runtime";
4
+ import { findPackageWithProduct, getPackagesWithProducts, resolveDefaultPackageId, } from "./utils";
5
+ import { validateBillingPurchase, validateBillingRestore } from "./validation";
6
+ export function useOnbornOffering(options) {
7
+ const runtimeOptions = useOnbornBillingConfig(options);
8
+ const [data, setData] = useState(null);
9
+ const [selectedPackageId, setSelectedPackageId] = useState(runtimeOptions.initialPackageId ?? null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [purchasing, setPurchasing] = useState(false);
12
+ const [restoring, setRestoring] = useState(false);
13
+ const [error, setError] = useState(null);
14
+ const client = useMemo(() => createBillingClient({
15
+ sourceId: `offering:${runtimeOptions.offeringId}`,
16
+ }), [
17
+ runtimeOptions.apiKey,
18
+ runtimeOptions.appId,
19
+ runtimeOptions.appVersion,
20
+ runtimeOptions.autoFlushMs,
21
+ runtimeOptions.country,
22
+ runtimeOptions.emitAnalyticsEvents,
23
+ runtimeOptions.emitSdkConnectionSignal,
24
+ runtimeOptions.fetchImpl,
25
+ runtimeOptions.locale,
26
+ runtimeOptions.offeringId,
27
+ runtimeOptions.platform,
28
+ runtimeOptions.sdkVersion,
29
+ runtimeOptions.userId,
30
+ runtimeOptions.userType,
31
+ ]);
32
+ const packages = useMemo(() => getPackagesWithProducts(data?.offering, data?.products, runtimeOptions.platform), [data?.offering, data?.products, runtimeOptions.platform]);
33
+ const selectedPackage = useMemo(() => {
34
+ const fallbackPackageId = selectedPackageId ?? resolveDefaultPackageId(data?.offering);
35
+ return findPackageWithProduct(packages, fallbackPackageId) ?? null;
36
+ }, [data?.offering, packages, selectedPackageId]);
37
+ const load = useCallback(async () => {
38
+ setLoading(true);
39
+ setError(null);
40
+ try {
41
+ const response = await client.loadOffering(runtimeOptions.offeringId);
42
+ const products = await loadLocalizedProducts(runtimeOptions.billingAdapter, {
43
+ offering: response.offering,
44
+ products: response.products,
45
+ userId: runtimeOptions.userId,
46
+ });
47
+ setData({ ...response, products });
48
+ setSelectedPackageId((current) => {
49
+ if (current &&
50
+ response.offering.packages.some((item) => item.id === current)) {
51
+ return current;
52
+ }
53
+ return resolveDefaultPackageId(response.offering) ?? null;
54
+ });
55
+ }
56
+ catch (loadError) {
57
+ setError(toErrorMessage(loadError));
58
+ }
59
+ finally {
60
+ setLoading(false);
61
+ }
62
+ }, [
63
+ client,
64
+ runtimeOptions.billingAdapter,
65
+ runtimeOptions.offeringId,
66
+ runtimeOptions.userId,
67
+ ]);
68
+ useEffect(() => {
69
+ void load();
70
+ }, [load]);
71
+ const selectPackage = useCallback((packageId) => {
72
+ setSelectedPackageId(packageId);
73
+ }, []);
74
+ const purchasePackage = useCallback(async (packageId) => {
75
+ const item = findPackageWithProduct(packages, packageId) ?? selectedPackage;
76
+ if (!item || !data?.offering) {
77
+ throw new Error("No offering package selected");
78
+ }
79
+ if (!runtimeOptions.billingAdapter) {
80
+ throw new Error("Missing ONBORN billingAdapter. Provide one to purchase from a custom paywall.");
81
+ }
82
+ setPurchasing(true);
83
+ runtimeOptions.onPurchaseStarted?.(item);
84
+ try {
85
+ const adapterResult = await runtimeOptions.billingAdapter.purchasePackage({
86
+ offering: data.offering,
87
+ package: item.package,
88
+ product: item.product,
89
+ userId: runtimeOptions.userId,
90
+ });
91
+ const result = adapterResult.success
92
+ ? await validateBillingPurchase({
93
+ client,
94
+ offering: data.offering,
95
+ item,
96
+ result: adapterResult,
97
+ })
98
+ : adapterResult;
99
+ runtimeOptions.onPurchaseCompleted?.(result);
100
+ notifyEntitlementsChanged(result.entitlements, runtimeOptions.onEntitlementsChanged);
101
+ return result;
102
+ }
103
+ catch (purchaseError) {
104
+ const errorObject = toError(purchaseError);
105
+ runtimeOptions.onPurchaseFailed?.(errorObject);
106
+ throw errorObject;
107
+ }
108
+ finally {
109
+ setPurchasing(false);
110
+ }
111
+ }, [client, data?.offering, runtimeOptions, packages, selectedPackage]);
112
+ const restorePurchases = useCallback(async () => {
113
+ if (!runtimeOptions.billingAdapter?.restorePurchases) {
114
+ throw new Error("Missing restorePurchases on ONBORN billingAdapter.");
115
+ }
116
+ setRestoring(true);
117
+ try {
118
+ const adapterResult = await runtimeOptions.billingAdapter.restorePurchases({
119
+ offering: data?.offering,
120
+ products: data?.products ?? [],
121
+ userId: runtimeOptions.userId,
122
+ });
123
+ const result = await validateBillingRestore({
124
+ client,
125
+ offering: data?.offering,
126
+ result: adapterResult,
127
+ });
128
+ runtimeOptions.onRestoreCompleted?.(result);
129
+ notifyEntitlementsChanged(result.entitlements, runtimeOptions.onEntitlementsChanged);
130
+ return result;
131
+ }
132
+ catch (restoreError) {
133
+ const errorObject = toError(restoreError);
134
+ runtimeOptions.onRestoreFailed?.(errorObject);
135
+ throw errorObject;
136
+ }
137
+ finally {
138
+ setRestoring(false);
139
+ }
140
+ }, [client, data, runtimeOptions]);
141
+ const refetchCustomerEntitlements = useCallback(async () => {
142
+ if (!runtimeOptions.billingAdapter?.refetchCustomerEntitlements) {
143
+ throw new Error("Missing refetchCustomerEntitlements on ONBORN billingAdapter.");
144
+ }
145
+ const adapterResult = await runtimeOptions.billingAdapter.refetchCustomerEntitlements({
146
+ userId: runtimeOptions.userId,
147
+ });
148
+ const result = await validateBillingRestore({
149
+ client,
150
+ offering: data?.offering,
151
+ result: adapterResult,
152
+ });
153
+ notifyEntitlementsChanged(result.entitlements, runtimeOptions.onEntitlementsChanged);
154
+ return result;
155
+ }, [client, data?.offering, runtimeOptions]);
156
+ return {
157
+ data,
158
+ packages,
159
+ selectedPackage,
160
+ selectedPackageId: selectedPackage?.package.id ?? null,
161
+ loading,
162
+ purchasing,
163
+ restoring,
164
+ error,
165
+ selectPackage,
166
+ reload: load,
167
+ purchasePackage,
168
+ restorePurchases,
169
+ refetchCustomerEntitlements,
170
+ };
171
+ }
172
+ function toErrorMessage(error) {
173
+ return toError(error).message;
174
+ }
175
+ function toError(error) {
176
+ return error instanceof Error ? error : new Error("Unknown error");
177
+ }
178
+ async function loadLocalizedProducts(billingAdapter, input) {
179
+ if (!billingAdapter?.loadProducts) {
180
+ return input.products;
181
+ }
182
+ try {
183
+ return await billingAdapter.loadProducts(input);
184
+ }
185
+ catch {
186
+ return input.products;
187
+ }
188
+ }
189
+ function notifyEntitlementsChanged(entitlements, callback) {
190
+ if (!entitlements) {
191
+ return;
192
+ }
193
+ callback?.(entitlements);
194
+ }
@@ -0,0 +1,31 @@
1
+ import { type CustomerEntitlement, type GetPaywallResponse } from "@onborn/sdk-contracts";
2
+ import type { OnbornBillingAdapter, OnbornPackageWithProduct, OnbornPurchaseResult, OnbornRestoreResult } from "./types";
3
+ export type UseOnbornPaywallOptions = {
4
+ paywallId: string;
5
+ initialPackageId?: string;
6
+ billingAdapter?: OnbornBillingAdapter;
7
+ onStartTrial?: (item: OnbornPackageWithProduct) => void | false | Promise<void | false>;
8
+ onPurchaseStarted?: (item: OnbornPackageWithProduct) => void;
9
+ onPurchaseCompleted?: (result: OnbornPurchaseResult) => void;
10
+ onPurchaseFailed?: (error: Error) => void;
11
+ onRestoreCompleted?: (result: OnbornRestoreResult) => void;
12
+ onRestoreFailed?: (error: Error) => void;
13
+ onEntitlementsChanged?: (entitlements: CustomerEntitlement[]) => void;
14
+ };
15
+ export type UseOnbornPaywallState = {
16
+ data: GetPaywallResponse | null;
17
+ packages: OnbornPackageWithProduct[];
18
+ selectedPackage: OnbornPackageWithProduct | null;
19
+ selectedPackageId: string | null;
20
+ loading: boolean;
21
+ purchasing: boolean;
22
+ restoring: boolean;
23
+ error: string | null;
24
+ selectPackage: (packageId: string) => void;
25
+ reload: () => Promise<void>;
26
+ purchasePackage: (packageId?: string) => Promise<OnbornPurchaseResult>;
27
+ startTrialPackage: (packageId?: string) => Promise<OnbornPurchaseResult>;
28
+ restorePurchases: () => Promise<OnbornRestoreResult>;
29
+ refetchCustomerEntitlements: () => Promise<OnbornRestoreResult>;
30
+ };
31
+ export declare function useOnbornPaywall(options: UseOnbornPaywallOptions): UseOnbornPaywallState;