@blocklet/payment-react-headless 1.26.0
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.
- package/.eslintrc.js +18 -0
- package/build.config.ts +30 -0
- package/es/checkout/context/CheckoutProvider.d.ts +6 -0
- package/es/checkout/context/CheckoutProvider.js +209 -0
- package/es/checkout/context/CustomerFormContext.d.ts +4 -0
- package/es/checkout/context/CustomerFormContext.js +9 -0
- package/es/checkout/context/ExchangeRateContext.d.ts +11 -0
- package/es/checkout/context/ExchangeRateContext.js +9 -0
- package/es/checkout/context/PaymentMethodContext.d.ts +26 -0
- package/es/checkout/context/PaymentMethodContext.js +9 -0
- package/es/checkout/context/SessionContext.d.ts +45 -0
- package/es/checkout/context/SessionContext.js +9 -0
- package/es/checkout/context/SubmitContext.d.ts +4 -0
- package/es/checkout/context/SubmitContext.js +9 -0
- package/es/checkout/context/index.d.ts +6 -0
- package/es/checkout/context/index.js +6 -0
- package/es/checkout/core/billingInterval.d.ts +15 -0
- package/es/checkout/core/billingInterval.js +36 -0
- package/es/checkout/core/crossSell.d.ts +4 -0
- package/es/checkout/core/crossSell.js +30 -0
- package/es/checkout/core/customerForm.d.ts +5 -0
- package/es/checkout/core/customerForm.js +105 -0
- package/es/checkout/core/exchangeRate.d.ts +11 -0
- package/es/checkout/core/exchangeRate.js +25 -0
- package/es/checkout/core/index.d.ts +10 -0
- package/es/checkout/core/index.js +55 -0
- package/es/checkout/core/lineItems.d.ts +7 -0
- package/es/checkout/core/lineItems.js +59 -0
- package/es/checkout/core/paymentMethod.d.ts +23 -0
- package/es/checkout/core/paymentMethod.js +85 -0
- package/es/checkout/core/pricing.d.ts +32 -0
- package/es/checkout/core/pricing.js +221 -0
- package/es/checkout/core/promotion.d.ts +10 -0
- package/es/checkout/core/promotion.js +39 -0
- package/es/checkout/core/session.d.ts +26 -0
- package/es/checkout/core/session.js +50 -0
- package/es/checkout/core/submit.d.ts +40 -0
- package/es/checkout/core/submit.js +66 -0
- package/es/checkout/hooks/index.d.ts +34 -0
- package/es/checkout/hooks/index.js +19 -0
- package/es/checkout/hooks/useBillingInterval.d.ts +14 -0
- package/es/checkout/hooks/useBillingInterval.js +50 -0
- package/es/checkout/hooks/useCheckout.d.ts +2 -0
- package/es/checkout/hooks/useCheckout.js +212 -0
- package/es/checkout/hooks/useCheckoutSession.d.ts +58 -0
- package/es/checkout/hooks/useCheckoutSession.js +107 -0
- package/es/checkout/hooks/useCheckoutStatus.d.ts +10 -0
- package/es/checkout/hooks/useCheckoutStatus.js +16 -0
- package/es/checkout/hooks/useCrossSell.d.ts +8 -0
- package/es/checkout/hooks/useCrossSell.js +57 -0
- package/es/checkout/hooks/useCustomerForm.d.ts +14 -0
- package/es/checkout/hooks/useCustomerForm.js +116 -0
- package/es/checkout/hooks/useCustomerFormFeature.d.ts +2 -0
- package/es/checkout/hooks/useCustomerFormFeature.js +4 -0
- package/es/checkout/hooks/useExchangeRate.d.ts +11 -0
- package/es/checkout/hooks/useExchangeRate.js +15 -0
- package/es/checkout/hooks/useLineItems.d.ts +22 -0
- package/es/checkout/hooks/useLineItems.js +139 -0
- package/es/checkout/hooks/usePaymentMethod.d.ts +26 -0
- package/es/checkout/hooks/usePaymentMethod.js +101 -0
- package/es/checkout/hooks/usePaymentMethodFeature.d.ts +2 -0
- package/es/checkout/hooks/usePaymentMethodFeature.js +4 -0
- package/es/checkout/hooks/usePricing.d.ts +57 -0
- package/es/checkout/hooks/usePricing.js +174 -0
- package/es/checkout/hooks/usePricingFeature.d.ts +28 -0
- package/es/checkout/hooks/usePricingFeature.js +36 -0
- package/es/checkout/hooks/useProduct.d.ts +32 -0
- package/es/checkout/hooks/useProduct.js +5 -0
- package/es/checkout/hooks/usePromotion.d.ts +12 -0
- package/es/checkout/hooks/usePromotion.js +48 -0
- package/es/checkout/hooks/useSlippage.d.ts +8 -0
- package/es/checkout/hooks/useSlippage.js +29 -0
- package/es/checkout/hooks/useSubmit.d.ts +38 -0
- package/es/checkout/hooks/useSubmit.js +493 -0
- package/es/checkout/hooks/useSubmitFeature.d.ts +2 -0
- package/es/checkout/hooks/useSubmitFeature.js +4 -0
- package/es/checkout/hooks/useUpsell.d.ts +5 -0
- package/es/checkout/hooks/useUpsell.js +25 -0
- package/es/checkout/index.d.ts +37 -0
- package/es/checkout/index.js +28 -0
- package/es/checkout/types.d.ts +262 -0
- package/es/checkout/types.js +0 -0
- package/es/index.d.ts +1 -0
- package/es/index.js +28 -0
- package/es/shared/api.d.ts +41 -0
- package/es/shared/api.js +81 -0
- package/es/shared/format.d.ts +38 -0
- package/es/shared/format.js +229 -0
- package/es/shared/polling.d.ts +15 -0
- package/es/shared/polling.js +20 -0
- package/es/shared/types.d.ts +10 -0
- package/es/shared/types.js +0 -0
- package/es/shared/validation.d.ts +38 -0
- package/es/shared/validation.js +190 -0
- package/es/types/checkout-augmented.d.ts +42 -0
- package/es/types/checkout-augmented.js +17 -0
- package/es/types/external.d.ts +18 -0
- package/examples/01-basic-checkout.tsx +159 -0
- package/examples/01-credit-recharge.tsx +19 -0
- package/examples/02-subscription.tsx +40 -0
- package/examples/03-upsell.tsx +60 -0
- package/examples/04-cross-sell.tsx +54 -0
- package/examples/05-full-checkout.tsx +126 -0
- package/jest.config.js +15 -0
- package/lib/checkout/context/CheckoutProvider.d.ts +6 -0
- package/lib/checkout/context/CheckoutProvider.js +181 -0
- package/lib/checkout/context/CustomerFormContext.d.ts +4 -0
- package/lib/checkout/context/CustomerFormContext.js +16 -0
- package/lib/checkout/context/ExchangeRateContext.d.ts +11 -0
- package/lib/checkout/context/ExchangeRateContext.js +16 -0
- package/lib/checkout/context/PaymentMethodContext.d.ts +26 -0
- package/lib/checkout/context/PaymentMethodContext.js +16 -0
- package/lib/checkout/context/SessionContext.d.ts +45 -0
- package/lib/checkout/context/SessionContext.js +16 -0
- package/lib/checkout/context/SubmitContext.d.ts +4 -0
- package/lib/checkout/context/SubmitContext.js +16 -0
- package/lib/checkout/context/index.d.ts +6 -0
- package/lib/checkout/context/index.js +77 -0
- package/lib/checkout/core/billingInterval.d.ts +15 -0
- package/lib/checkout/core/billingInterval.js +42 -0
- package/lib/checkout/core/crossSell.d.ts +4 -0
- package/lib/checkout/core/crossSell.js +43 -0
- package/lib/checkout/core/customerForm.d.ts +5 -0
- package/lib/checkout/core/customerForm.js +106 -0
- package/lib/checkout/core/exchangeRate.d.ts +11 -0
- package/lib/checkout/core/exchangeRate.js +45 -0
- package/lib/checkout/core/index.d.ts +10 -0
- package/lib/checkout/core/index.js +297 -0
- package/lib/checkout/core/lineItems.d.ts +7 -0
- package/lib/checkout/core/lineItems.js +76 -0
- package/lib/checkout/core/paymentMethod.d.ts +23 -0
- package/lib/checkout/core/paymentMethod.js +114 -0
- package/lib/checkout/core/pricing.d.ts +32 -0
- package/lib/checkout/core/pricing.js +216 -0
- package/lib/checkout/core/promotion.d.ts +10 -0
- package/lib/checkout/core/promotion.js +62 -0
- package/lib/checkout/core/session.d.ts +26 -0
- package/lib/checkout/core/session.js +58 -0
- package/lib/checkout/core/submit.d.ts +40 -0
- package/lib/checkout/core/submit.js +84 -0
- package/lib/checkout/hooks/index.d.ts +34 -0
- package/lib/checkout/hooks/index.js +138 -0
- package/lib/checkout/hooks/useBillingInterval.d.ts +14 -0
- package/lib/checkout/hooks/useBillingInterval.js +63 -0
- package/lib/checkout/hooks/useCheckout.d.ts +2 -0
- package/lib/checkout/hooks/useCheckout.js +190 -0
- package/lib/checkout/hooks/useCheckoutSession.d.ts +58 -0
- package/lib/checkout/hooks/useCheckoutSession.js +119 -0
- package/lib/checkout/hooks/useCheckoutStatus.d.ts +10 -0
- package/lib/checkout/hooks/useCheckoutStatus.js +28 -0
- package/lib/checkout/hooks/useCrossSell.d.ts +8 -0
- package/lib/checkout/hooks/useCrossSell.js +75 -0
- package/lib/checkout/hooks/useCustomerForm.d.ts +14 -0
- package/lib/checkout/hooks/useCustomerForm.js +135 -0
- package/lib/checkout/hooks/useCustomerFormFeature.d.ts +2 -0
- package/lib/checkout/hooks/useCustomerFormFeature.js +10 -0
- package/lib/checkout/hooks/useExchangeRate.d.ts +11 -0
- package/lib/checkout/hooks/useExchangeRate.js +29 -0
- package/lib/checkout/hooks/useLineItems.d.ts +22 -0
- package/lib/checkout/hooks/useLineItems.js +142 -0
- package/lib/checkout/hooks/usePaymentMethod.d.ts +26 -0
- package/lib/checkout/hooks/usePaymentMethod.js +101 -0
- package/lib/checkout/hooks/usePaymentMethodFeature.d.ts +2 -0
- package/lib/checkout/hooks/usePaymentMethodFeature.js +10 -0
- package/lib/checkout/hooks/usePricing.d.ts +57 -0
- package/lib/checkout/hooks/usePricing.js +168 -0
- package/lib/checkout/hooks/usePricingFeature.d.ts +28 -0
- package/lib/checkout/hooks/usePricingFeature.js +48 -0
- package/lib/checkout/hooks/useProduct.d.ts +32 -0
- package/lib/checkout/hooks/useProduct.js +21 -0
- package/lib/checkout/hooks/usePromotion.d.ts +12 -0
- package/lib/checkout/hooks/usePromotion.js +57 -0
- package/lib/checkout/hooks/useSlippage.d.ts +8 -0
- package/lib/checkout/hooks/useSlippage.js +39 -0
- package/lib/checkout/hooks/useSubmit.d.ts +38 -0
- package/lib/checkout/hooks/useSubmit.js +504 -0
- package/lib/checkout/hooks/useSubmitFeature.d.ts +2 -0
- package/lib/checkout/hooks/useSubmitFeature.js +10 -0
- package/lib/checkout/hooks/useUpsell.d.ts +5 -0
- package/lib/checkout/hooks/useUpsell.js +40 -0
- package/lib/checkout/index.d.ts +37 -0
- package/lib/checkout/index.js +182 -0
- package/lib/checkout/types.d.ts +262 -0
- package/lib/checkout/types.js +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +162 -0
- package/lib/shared/api.d.ts +41 -0
- package/lib/shared/api.js +88 -0
- package/lib/shared/format.d.ts +38 -0
- package/lib/shared/format.js +262 -0
- package/lib/shared/polling.d.ts +15 -0
- package/lib/shared/polling.js +32 -0
- package/lib/shared/types.d.ts +10 -0
- package/lib/shared/types.js +1 -0
- package/lib/shared/validation.d.ts +38 -0
- package/lib/shared/validation.js +212 -0
- package/lib/types/checkout-augmented.d.ts +42 -0
- package/lib/types/checkout-augmented.js +24 -0
- package/lib/types/external.d.ts +18 -0
- package/package.json +64 -0
- package/src/checkout/context/CheckoutProvider.tsx +269 -0
- package/src/checkout/context/CustomerFormContext.ts +14 -0
- package/src/checkout/context/ExchangeRateContext.ts +21 -0
- package/src/checkout/context/PaymentMethodContext.ts +36 -0
- package/src/checkout/context/SessionContext.ts +49 -0
- package/src/checkout/context/SubmitContext.ts +14 -0
- package/src/checkout/context/index.ts +6 -0
- package/src/checkout/core/billingInterval.ts +62 -0
- package/src/checkout/core/crossSell.ts +52 -0
- package/src/checkout/core/customerForm.ts +122 -0
- package/src/checkout/core/exchangeRate.ts +38 -0
- package/src/checkout/core/index.ts +60 -0
- package/src/checkout/core/lineItems.ts +106 -0
- package/src/checkout/core/paymentMethod.ts +113 -0
- package/src/checkout/core/pricing.ts +347 -0
- package/src/checkout/core/promotion.ts +59 -0
- package/src/checkout/core/session.ts +62 -0
- package/src/checkout/core/submit.ts +109 -0
- package/src/checkout/hooks/index.ts +41 -0
- package/src/checkout/hooks/useBillingInterval.ts +71 -0
- package/src/checkout/hooks/useCheckout.ts +267 -0
- package/src/checkout/hooks/useCheckoutSession.ts +217 -0
- package/src/checkout/hooks/useCheckoutStatus.ts +31 -0
- package/src/checkout/hooks/useCrossSell.ts +80 -0
- package/src/checkout/hooks/useCustomerForm.ts +156 -0
- package/src/checkout/hooks/useCustomerFormFeature.ts +7 -0
- package/src/checkout/hooks/useExchangeRate.ts +28 -0
- package/src/checkout/hooks/useLineItems.ts +191 -0
- package/src/checkout/hooks/usePaymentMethod.ts +165 -0
- package/src/checkout/hooks/usePaymentMethodFeature.ts +8 -0
- package/src/checkout/hooks/usePricing.ts +274 -0
- package/src/checkout/hooks/usePricingFeature.ts +73 -0
- package/src/checkout/hooks/useProduct.ts +32 -0
- package/src/checkout/hooks/usePromotion.ts +67 -0
- package/src/checkout/hooks/useSlippage.ts +39 -0
- package/src/checkout/hooks/useSubmit.ts +684 -0
- package/src/checkout/hooks/useSubmitFeature.ts +7 -0
- package/src/checkout/hooks/useUpsell.ts +35 -0
- package/src/checkout/index.ts +65 -0
- package/src/checkout/types.ts +292 -0
- package/src/index.ts +64 -0
- package/src/shared/api.ts +118 -0
- package/src/shared/format.ts +318 -0
- package/src/shared/polling.ts +49 -0
- package/src/shared/types.ts +13 -0
- package/src/shared/validation.ts +254 -0
- package/src/types/checkout-augmented.ts +77 -0
- package/src/types/external.d.ts +18 -0
- package/tools/jest.js +1 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { BN, fromUnitToToken, fromTokenToUnit } from '@ocap/util';
|
|
2
|
+
import type { TCheckoutSessionExpanded, TPaymentCurrency, TLineItemExpanded } from '@blocklet/payment-types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatDynamicPrice,
|
|
6
|
+
formatUsdAmount,
|
|
7
|
+
getUsdAmountFromTokenUnits,
|
|
8
|
+
getCheckoutAmount,
|
|
9
|
+
getPriceUnitAmountByCurrency,
|
|
10
|
+
} from '../../shared/format';
|
|
11
|
+
import type { CheckoutSessionRuntime } from '../../types/checkout-augmented';
|
|
12
|
+
import type { SessionData } from '../hooks/useCheckoutSession';
|
|
13
|
+
|
|
14
|
+
export interface CalculatedAmounts {
|
|
15
|
+
subtotal: string;
|
|
16
|
+
paymentAmount: string;
|
|
17
|
+
total: string;
|
|
18
|
+
discount: string | null;
|
|
19
|
+
tax: {
|
|
20
|
+
amount: string;
|
|
21
|
+
rate: number;
|
|
22
|
+
inclusive: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
staking: string | null;
|
|
25
|
+
usdEquivalent: string | null;
|
|
26
|
+
subtotalUsdEquivalent?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Calculate staking amount for subscription (mirrors original getStakingSetup in payment-react/summary.tsx)
|
|
30
|
+
function getStakingSetup(items: TLineItemExpanded[], currency: TPaymentCurrency, billingThreshold = 0): string {
|
|
31
|
+
const staking = { licensed: new BN(0), metered: new BN(0) };
|
|
32
|
+
|
|
33
|
+
const recurringItems = items
|
|
34
|
+
.map((x) => x.upsell_price || x.price)
|
|
35
|
+
.filter((x) => x?.type === 'recurring' && x?.recurring);
|
|
36
|
+
|
|
37
|
+
if (recurringItems.length > 0) {
|
|
38
|
+
if (+billingThreshold) {
|
|
39
|
+
return fromTokenToUnit(billingThreshold, currency.decimal).toString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
items.forEach((x) => {
|
|
43
|
+
const price = x.upsell_price || x.price;
|
|
44
|
+
const unit = getPriceUnitAmountByCurrency(price, currency);
|
|
45
|
+
const amount = new BN(unit).mul(new BN(x.quantity));
|
|
46
|
+
if (price?.type === 'recurring' && price?.recurring) {
|
|
47
|
+
if (price.recurring.usage_type === 'licensed') {
|
|
48
|
+
staking.licensed = staking.licensed.add(amount);
|
|
49
|
+
}
|
|
50
|
+
if (price.recurring.usage_type === 'metered') {
|
|
51
|
+
staking.metered = staking.metered.add(amount);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return staking.licensed.add(staking.metered).toString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return '0';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate coupon discount amount.
|
|
64
|
+
* @param recurringOnly - if true, skip coupons with duration='once' (for "next charge" display)
|
|
65
|
+
*/
|
|
66
|
+
function calculateCouponDiscount(
|
|
67
|
+
items: TLineItemExpanded[],
|
|
68
|
+
currency: TPaymentCurrency,
|
|
69
|
+
session: TCheckoutSessionExpanded | undefined | null,
|
|
70
|
+
hasDynamicPricing: boolean,
|
|
71
|
+
exchangeRate: string | null,
|
|
72
|
+
trialing: boolean,
|
|
73
|
+
recurringOnly: boolean
|
|
74
|
+
): BN {
|
|
75
|
+
const discounts = session?.discounts || [];
|
|
76
|
+
if (discounts.length === 0) return new BN(0);
|
|
77
|
+
|
|
78
|
+
const coupon = ((discounts[0] as Record<string, unknown>).coupon_details || {}) as {
|
|
79
|
+
percent_off?: number;
|
|
80
|
+
amount_off?: string;
|
|
81
|
+
currency_options?: Record<string, { amount_off?: string }>;
|
|
82
|
+
duration?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// 'once' coupons only apply to the first charge, not recurring
|
|
86
|
+
if (recurringOnly && coupon.duration === 'once') return new BN(0);
|
|
87
|
+
|
|
88
|
+
const discountableItems = items.filter((item) => {
|
|
89
|
+
if (item.discountable === false) return false;
|
|
90
|
+
const price = item.upsell_price || item.price;
|
|
91
|
+
if (price?.recurring?.usage_type === 'metered') return false;
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
const discountableResult = getCheckoutAmount(discountableItems, currency, trialing, true, {
|
|
95
|
+
exchangeRate: hasDynamicPricing ? exchangeRate : null,
|
|
96
|
+
});
|
|
97
|
+
const discountableSubtotalBN = new BN(discountableResult.total);
|
|
98
|
+
if (discountableSubtotalBN.lte(new BN(0))) return new BN(0);
|
|
99
|
+
|
|
100
|
+
if (coupon.percent_off && coupon.percent_off > 0) {
|
|
101
|
+
return discountableSubtotalBN.mul(new BN(coupon.percent_off)).div(new BN(100));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (coupon.amount_off && coupon.amount_off !== '0') {
|
|
105
|
+
const amountOff = coupon.currency_options?.[currency.id]?.amount_off || coupon.amount_off;
|
|
106
|
+
if (amountOff && amountOff !== '0') {
|
|
107
|
+
const amountOffBN = new BN(amountOff);
|
|
108
|
+
return amountOffBN.lt(discountableSubtotalBN) ? amountOffBN : discountableSubtotalBN;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new BN(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function calculateAmounts(
|
|
116
|
+
items: TLineItemExpanded[],
|
|
117
|
+
currency: TPaymentCurrency | null,
|
|
118
|
+
session: TCheckoutSessionExpanded | undefined | null,
|
|
119
|
+
exchangeRate: string | null,
|
|
120
|
+
hasDynamicPricing: boolean,
|
|
121
|
+
paymentMethodType?: string | null
|
|
122
|
+
): CalculatedAmounts {
|
|
123
|
+
if (!currency || items.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
subtotal: '0',
|
|
126
|
+
paymentAmount: '0',
|
|
127
|
+
total: '0',
|
|
128
|
+
discount: null,
|
|
129
|
+
tax: null,
|
|
130
|
+
staking: null,
|
|
131
|
+
usdEquivalent: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const subData = session?.subscription_data;
|
|
136
|
+
const trialDays = Number(subData?.trial_period_days || 0);
|
|
137
|
+
const trialing = trialDays > 0;
|
|
138
|
+
|
|
139
|
+
const result = getCheckoutAmount(items, currency, trialing, true, {
|
|
140
|
+
exchangeRate: hasDynamicPricing ? exchangeRate : null,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const subtotalBN = new BN(result.total);
|
|
144
|
+
const subtotalFormatted = formatDynamicPrice(fromUnitToToken(subtotalBN, currency.decimal), hasDynamicPricing);
|
|
145
|
+
|
|
146
|
+
// Calculate discount client-side from coupon data
|
|
147
|
+
const discountBN = calculateCouponDiscount(items, currency, session, hasDynamicPricing, exchangeRate, trialing, false);
|
|
148
|
+
const discount = discountBN.gt(new BN(0))
|
|
149
|
+
? formatDynamicPrice(fromUnitToToken(discountBN.toString(), currency.decimal), hasDynamicPricing)
|
|
150
|
+
: null;
|
|
151
|
+
|
|
152
|
+
// Tax from session (backend-calculated)
|
|
153
|
+
const taxAmount = session?.total_details?.amount_tax;
|
|
154
|
+
const taxBN = new BN(taxAmount || '0');
|
|
155
|
+
const totalDetails = session?.total_details as
|
|
156
|
+
| (typeof session extends undefined
|
|
157
|
+
? never
|
|
158
|
+
: NonNullable<typeof session>['total_details'] & { tax_rate?: number; tax_inclusive?: boolean })
|
|
159
|
+
| undefined;
|
|
160
|
+
const tax =
|
|
161
|
+
taxAmount && taxAmount !== '0'
|
|
162
|
+
? {
|
|
163
|
+
amount: fromUnitToToken(taxAmount, currency.decimal),
|
|
164
|
+
rate: totalDetails?.tax_rate || 0,
|
|
165
|
+
inclusive: totalDetails?.tax_inclusive || false,
|
|
166
|
+
}
|
|
167
|
+
: null;
|
|
168
|
+
|
|
169
|
+
// Staking calculation (for arcblock payment method, non-credit currencies)
|
|
170
|
+
const noStake = subData?.no_stake || false;
|
|
171
|
+
const billingThreshold = Math.max(
|
|
172
|
+
Number(subData?.billing_threshold_amount || 0),
|
|
173
|
+
Number(subData?.min_stake_amount || 0)
|
|
174
|
+
);
|
|
175
|
+
const shouldShowStaking = paymentMethodType === 'arcblock' && currency.type !== 'credit' && !noStake;
|
|
176
|
+
const stakingUnitStr = shouldShowStaking ? getStakingSetup(items, currency, billingThreshold) : '0';
|
|
177
|
+
const stakingBN = new BN(stakingUnitStr);
|
|
178
|
+
|
|
179
|
+
// Subtotal includes staking when trialing (payment=0 + staking)
|
|
180
|
+
const subtotalWithStakingBN = subtotalBN.add(stakingBN);
|
|
181
|
+
|
|
182
|
+
// Total = subtotal + staking - discount + tax
|
|
183
|
+
let totalBN = subtotalWithStakingBN.sub(discountBN);
|
|
184
|
+
if (taxBN.gt(new BN(0)) && !tax?.inclusive) {
|
|
185
|
+
totalBN = totalBN.add(taxBN);
|
|
186
|
+
}
|
|
187
|
+
if (totalBN.isNeg()) {
|
|
188
|
+
totalBN = new BN(0);
|
|
189
|
+
}
|
|
190
|
+
const totalFormatted = formatDynamicPrice(fromUnitToToken(totalBN, currency.decimal), hasDynamicPricing);
|
|
191
|
+
|
|
192
|
+
// For display: subtotal should reflect total before discount (includes staking)
|
|
193
|
+
const displaySubtotalBN = subtotalWithStakingBN;
|
|
194
|
+
const displaySubtotalFormatted = formatDynamicPrice(
|
|
195
|
+
fromUnitToToken(displaySubtotalBN, currency.decimal),
|
|
196
|
+
hasDynamicPricing
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// USD equivalents for crypto
|
|
200
|
+
let usdEquivalent: string | null = null;
|
|
201
|
+
let subtotalUsdEquivalent: string | null = null;
|
|
202
|
+
if (hasDynamicPricing && exchangeRate && currency) {
|
|
203
|
+
const totalUsd = getUsdAmountFromTokenUnits(totalBN.toString(), currency.decimal, exchangeRate);
|
|
204
|
+
if (totalUsd) {
|
|
205
|
+
usdEquivalent = formatUsdAmount(totalUsd) || null;
|
|
206
|
+
}
|
|
207
|
+
const subtotalUsd = getUsdAmountFromTokenUnits(displaySubtotalBN.toString(), currency.decimal, exchangeRate);
|
|
208
|
+
if (subtotalUsd) {
|
|
209
|
+
subtotalUsdEquivalent = formatUsdAmount(subtotalUsd) || null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const stakingFormatted = stakingBN.gt(new BN(0))
|
|
214
|
+
? `${formatDynamicPrice(fromUnitToToken(stakingBN.toString(), currency.decimal), hasDynamicPricing)} ${currency.symbol}`
|
|
215
|
+
: null;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
subtotal: `${displaySubtotalFormatted} ${currency.symbol}`,
|
|
219
|
+
paymentAmount: `${subtotalFormatted} ${currency.symbol}`,
|
|
220
|
+
total: `${totalFormatted} ${currency.symbol}`,
|
|
221
|
+
discount: discount ? `${discount} ${currency.symbol}` : null,
|
|
222
|
+
tax,
|
|
223
|
+
staking: stakingFormatted,
|
|
224
|
+
usdEquivalent: usdEquivalent ? `$${usdEquivalent}` : null,
|
|
225
|
+
subtotalUsdEquivalent: subtotalUsdEquivalent ? `$${subtotalUsdEquivalent}` : null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface QuoteMeta {
|
|
230
|
+
locked: boolean;
|
|
231
|
+
lockedAt: number | null;
|
|
232
|
+
expiresAt: number | null;
|
|
233
|
+
expired: boolean;
|
|
234
|
+
baseCurrency: string | null;
|
|
235
|
+
slippagePercent: number | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function calculateQuoteMeta(
|
|
239
|
+
items: TLineItemExpanded[],
|
|
240
|
+
hasDynamicPricing: boolean,
|
|
241
|
+
sessionData: SessionData | null
|
|
242
|
+
): QuoteMeta {
|
|
243
|
+
if (!items.length || !hasDynamicPricing) {
|
|
244
|
+
return {
|
|
245
|
+
locked: false,
|
|
246
|
+
lockedAt: null,
|
|
247
|
+
expiresAt: null,
|
|
248
|
+
expired: false,
|
|
249
|
+
baseCurrency: null,
|
|
250
|
+
slippagePercent: null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const session = sessionData?.checkoutSession as CheckoutSessionRuntime | undefined;
|
|
255
|
+
let baseCurrency: string | null = null;
|
|
256
|
+
let expiresAt: number | null = null;
|
|
257
|
+
let slippagePercent: number | null = null;
|
|
258
|
+
|
|
259
|
+
items.forEach((item) => {
|
|
260
|
+
const price = item.upsell_price || item.price;
|
|
261
|
+
if (!baseCurrency && price?.base_currency) baseCurrency = price.base_currency;
|
|
262
|
+
|
|
263
|
+
const expires = item.expires_at as number | undefined;
|
|
264
|
+
if (expires) {
|
|
265
|
+
expiresAt = expiresAt === null ? expires : Math.min(expiresAt!, expires);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const itemSlippage = item.slippage_percent as number | undefined;
|
|
269
|
+
if (slippagePercent === null && Number.isFinite(Number(itemSlippage))) {
|
|
270
|
+
slippagePercent = Number(itemSlippage);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const piCreated = sessionData?.paymentIntent?.created_at
|
|
275
|
+
? new Date(sessionData.paymentIntent.created_at).getTime()
|
|
276
|
+
: null;
|
|
277
|
+
const sessionLock = session?.quote_locked_at;
|
|
278
|
+
const lockedAt: number | null = piCreated || sessionLock || null;
|
|
279
|
+
const locked = !!lockedAt;
|
|
280
|
+
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const expired = expiresAt ? now > expiresAt : false;
|
|
283
|
+
|
|
284
|
+
return { locked, lockedAt, expiresAt, expired, baseCurrency, slippagePercent };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function calculateTrial(
|
|
288
|
+
session: TCheckoutSessionExpanded | undefined | null,
|
|
289
|
+
currencyId: string | null | undefined,
|
|
290
|
+
items: TLineItemExpanded[],
|
|
291
|
+
currency: TPaymentCurrency | null,
|
|
292
|
+
hasDynamicPricing: boolean,
|
|
293
|
+
exchangeRate: string | null
|
|
294
|
+
) {
|
|
295
|
+
const subData = session?.subscription_data;
|
|
296
|
+
let trialDays = Number(subData?.trial_period_days || 0);
|
|
297
|
+
|
|
298
|
+
if (trialDays > 0 && currencyId) {
|
|
299
|
+
const trialCurrencyIds = (subData?.trial_currency || '')
|
|
300
|
+
.split(',')
|
|
301
|
+
.map((s: string) => s.trim())
|
|
302
|
+
.filter(Boolean);
|
|
303
|
+
if (trialCurrencyIds.length > 0 && !trialCurrencyIds.includes(currencyId)) {
|
|
304
|
+
trialDays = 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const trialActive = trialDays > 0;
|
|
309
|
+
|
|
310
|
+
let afterTrialPrice: string | null = null;
|
|
311
|
+
let afterTrialInterval: string | null = null;
|
|
312
|
+
if (trialActive && currency && items.length > 0) {
|
|
313
|
+
const noTrialResult = getCheckoutAmount(items, currency, false, true, {
|
|
314
|
+
exchangeRate: hasDynamicPricing ? exchangeRate : null,
|
|
315
|
+
});
|
|
316
|
+
let afterTrialBN = new BN(noTrialResult.total);
|
|
317
|
+
|
|
318
|
+
// Apply recurring discount (repeating/forever coupons) to next charge amount
|
|
319
|
+
const recurringDiscount = calculateCouponDiscount(
|
|
320
|
+
items, currency, session, hasDynamicPricing, exchangeRate, false, true
|
|
321
|
+
);
|
|
322
|
+
if (recurringDiscount.gt(new BN(0))) {
|
|
323
|
+
afterTrialBN = afterTrialBN.sub(recurringDiscount);
|
|
324
|
+
if (afterTrialBN.isNeg()) afterTrialBN = new BN(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
afterTrialPrice = `${formatDynamicPrice(
|
|
328
|
+
fromUnitToToken(afterTrialBN, currency.decimal),
|
|
329
|
+
hasDynamicPricing
|
|
330
|
+
)} ${currency.symbol}`;
|
|
331
|
+
const recurringItem = items.find((item) => {
|
|
332
|
+
const price = item.upsell_price || item.price;
|
|
333
|
+
return price?.type === 'recurring' && price?.recurring;
|
|
334
|
+
});
|
|
335
|
+
if (recurringItem) {
|
|
336
|
+
const price = recurringItem.upsell_price || recurringItem.price;
|
|
337
|
+
afterTrialInterval = price?.recurring?.interval || null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
active: trialActive,
|
|
343
|
+
days: trialDays,
|
|
344
|
+
afterTrialPrice,
|
|
345
|
+
afterTrialInterval,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TCheckoutSessionExpanded } from '@blocklet/payment-types';
|
|
2
|
+
|
|
3
|
+
import api, { API } from '../../shared/api';
|
|
4
|
+
import { getErrorMessage } from '../../types/checkout-augmented';
|
|
5
|
+
|
|
6
|
+
export function extractPromotionCodeFromSession(session: TCheckoutSessionExpanded | undefined | null): string | null {
|
|
7
|
+
const discounts = session?.discounts || [];
|
|
8
|
+
if (discounts.length === 0) return null;
|
|
9
|
+
|
|
10
|
+
const firstDiscount = discounts[0] as Record<string, unknown>;
|
|
11
|
+
const details = firstDiscount.promotion_code_details as { code?: string } | undefined;
|
|
12
|
+
const verification = firstDiscount.verification_data as { code?: string } | undefined;
|
|
13
|
+
return (
|
|
14
|
+
details?.code ||
|
|
15
|
+
verification?.code ||
|
|
16
|
+
(firstDiscount.promotion_code as string) ||
|
|
17
|
+
(firstDiscount.coupon as string) ||
|
|
18
|
+
null
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function applyPromotionCode(
|
|
23
|
+
sessionId: string,
|
|
24
|
+
code: string,
|
|
25
|
+
currencyId: string | null | undefined
|
|
26
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
27
|
+
try {
|
|
28
|
+
const { data } = await api.post(API.APPLY_PROMOTION(sessionId), {
|
|
29
|
+
promotion_code: code,
|
|
30
|
+
currency_id: currencyId,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (data.error) {
|
|
34
|
+
return { success: false, error: data.error };
|
|
35
|
+
}
|
|
36
|
+
return { success: true };
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
return { success: false, error: getErrorMessage(err) || 'Invalid promotion code' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function removePromotionCode(sessionId: string): Promise<void> {
|
|
43
|
+
await api.delete(API.REMOVE_PROMOTION(sessionId));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function recalculatePromotion(sessionId: string, currencyId: string | null | undefined): Promise<void> {
|
|
47
|
+
await api.post(API.RECALCULATE_PROMOTION_SESSION(sessionId), {
|
|
48
|
+
currency_id: currencyId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isPromotionActive(session: TCheckoutSessionExpanded | undefined | null): boolean {
|
|
53
|
+
return session?.allow_promotion_codes !== false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hasAppliedDiscounts(session: TCheckoutSessionExpanded | undefined | null): boolean {
|
|
57
|
+
const discounts = session?.discounts || [];
|
|
58
|
+
return discounts.length > 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TCheckoutSessionExpanded } from '@blocklet/payment-types';
|
|
2
|
+
|
|
3
|
+
export function parseProduct(session: TCheckoutSessionExpanded) {
|
|
4
|
+
const items = session.line_items || [];
|
|
5
|
+
if (items.length === 0) return null;
|
|
6
|
+
|
|
7
|
+
const firstItem = items[0];
|
|
8
|
+
const price = firstItem.upsell_price || firstItem.price;
|
|
9
|
+
const product = price?.product;
|
|
10
|
+
if (!product) return null;
|
|
11
|
+
|
|
12
|
+
const recurring = price?.recurring;
|
|
13
|
+
const mode = session.mode === 'subscription' ? 'subscription' : 'payment';
|
|
14
|
+
|
|
15
|
+
const intervalMap: Record<string, string> = {
|
|
16
|
+
day: 'daily',
|
|
17
|
+
week: 'weekly',
|
|
18
|
+
month: 'monthly',
|
|
19
|
+
year: 'yearly',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: product.name || '',
|
|
24
|
+
description: product.description || '',
|
|
25
|
+
images: product.images || [],
|
|
26
|
+
features: (product.features || []).map((f: { name?: string; icon?: string }) => ({
|
|
27
|
+
name: f.name || '',
|
|
28
|
+
icon: f.icon,
|
|
29
|
+
})),
|
|
30
|
+
billing: {
|
|
31
|
+
mode: mode as 'payment' | 'subscription',
|
|
32
|
+
interval: (recurring?.interval as 'month' | 'year' | 'week' | 'day' | undefined) || null,
|
|
33
|
+
intervalCount: recurring?.interval_count || 1,
|
|
34
|
+
displayInterval: recurring ? intervalMap[recurring.interval] || recurring.interval : '',
|
|
35
|
+
},
|
|
36
|
+
metadata: product.metadata || {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseSubscription(session: TCheckoutSessionExpanded) {
|
|
41
|
+
const subData = session.subscription_data;
|
|
42
|
+
if (!subData && session.mode === 'payment') return null;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
mode: (session.mode || 'payment') as 'payment' | 'subscription' | 'setup',
|
|
46
|
+
showStake: !subData?.no_stake,
|
|
47
|
+
confirmMessage: '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parsePageInfo(session: TCheckoutSessionExpanded) {
|
|
52
|
+
const metadata = session.metadata as Record<string, any> | undefined;
|
|
53
|
+
const pageInfo = metadata?.page_info;
|
|
54
|
+
|
|
55
|
+
// show_product_features is stored as boolean string on metadata directly (V1 compat)
|
|
56
|
+
const showRaw = metadata?.show_product_features ?? pageInfo?.show_product_features;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
formPurposeDescription: pageInfo?.form_purpose_description,
|
|
60
|
+
showProductFeatures: `${showRaw}` === 'true',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { TCheckoutSessionExpanded, TLineItemExpanded } from '@blocklet/payment-types';
|
|
2
|
+
|
|
3
|
+
import api, { API } from '../../shared/api';
|
|
4
|
+
import { generateIdempotencyKey } from '../../shared/polling';
|
|
5
|
+
import type { CheckoutFormData } from '../types';
|
|
6
|
+
|
|
7
|
+
// Quote-related error codes that trigger auto-refresh
|
|
8
|
+
export const QUOTE_ERROR_CODES = [
|
|
9
|
+
'QUOTE_LOCK_EXPIRED',
|
|
10
|
+
'QUOTE_AMOUNT_MISMATCH',
|
|
11
|
+
'QUOTE_EXPIRED_OR_USED',
|
|
12
|
+
'QUOTE_NOT_FOUND',
|
|
13
|
+
'QUOTE_REQUIRED',
|
|
14
|
+
'QUOTE_MAX_PAYABLE_EXCEEDED',
|
|
15
|
+
'quote_validation_failed',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
// WebSocket relay utilities
|
|
19
|
+
export const RELAY_SOCKET_PREFIX = '/.well-known/service/relay';
|
|
20
|
+
|
|
21
|
+
export function getAppId(): string {
|
|
22
|
+
// appPid/appId are injected at runtime by blocklet-server but not in the static type declaration
|
|
23
|
+
const blocklet = window.blocklet as { appPid?: string; appId?: string } | undefined;
|
|
24
|
+
return blocklet?.appPid || blocklet?.appId || '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRelayChannel(token: string): string {
|
|
28
|
+
return `relay:${getAppId()}:${token}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getRelayProtocol(): string {
|
|
32
|
+
return window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getSocketHost(): string {
|
|
36
|
+
return new URL(window.location.href).host;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute a fingerprint of the payment context for idempotency key stability.
|
|
41
|
+
* Key changes only when session state actually changes (quantity, upsell, currency).
|
|
42
|
+
* Same fingerprint → reuse Quote (intent: "retry with same Quote").
|
|
43
|
+
*/
|
|
44
|
+
export function getSessionFingerprint(
|
|
45
|
+
session: TCheckoutSessionExpanded | undefined | null,
|
|
46
|
+
currencyId: string | null
|
|
47
|
+
): string {
|
|
48
|
+
if (!session) return '';
|
|
49
|
+
const items = (session.line_items || []) as TLineItemExpanded[];
|
|
50
|
+
const sig = items.map((i) => `${i.upsell_price_id || i.price_id}:${i.quantity}`).join('|');
|
|
51
|
+
return `${session.id}-${currencyId}-${sig}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildSubmitPayload(
|
|
55
|
+
sessionId: string,
|
|
56
|
+
currencyId: string | null,
|
|
57
|
+
formValues: CheckoutFormData,
|
|
58
|
+
session: TCheckoutSessionExpanded | undefined | null,
|
|
59
|
+
priceConfirmed = false,
|
|
60
|
+
idempotencyKey?: string
|
|
61
|
+
) {
|
|
62
|
+
const lineItems = (session?.line_items || []) as TLineItemExpanded[];
|
|
63
|
+
const matchedItem = lineItems.find((item) => item.exchange_rate);
|
|
64
|
+
const previewRate = matchedItem?.exchange_rate as string | undefined;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...formValues,
|
|
68
|
+
idempotency_key: idempotencyKey || generateIdempotencyKey(sessionId, currencyId || ''),
|
|
69
|
+
preview_rate: previewRate || undefined,
|
|
70
|
+
...(priceConfirmed && { price_confirmed: true }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isQuoteError(errorCode: string): boolean {
|
|
75
|
+
return (QUOTE_ERROR_CODES as readonly string[]).includes(errorCode) || errorCode === 'QUOTE_UPDATED';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function abortStripePayment(sessionId: string): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
await api.post(API.ABORT_STRIPE(sessionId));
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore abort error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function submitCheckout(
|
|
87
|
+
sessionId: string,
|
|
88
|
+
isDonation: boolean,
|
|
89
|
+
payload: Record<string, unknown>
|
|
90
|
+
): Promise<Record<string, unknown>> {
|
|
91
|
+
const url = isDonation ? API.DONATE_SUBMIT(sessionId) : API.SUBMIT(sessionId);
|
|
92
|
+
const { data } = await api.put(url, payload);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function confirmFastCheckout(sessionId: string): Promise<Record<string, unknown>> {
|
|
97
|
+
const { data } = await api.post(API.FAST_CHECKOUT_CONFIRM(sessionId), {});
|
|
98
|
+
return data;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function updateSlippage(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
config: { mode: string; percent: number }
|
|
104
|
+
): Promise<Record<string, unknown>> {
|
|
105
|
+
const { data } = await api.put(API.SLIPPAGE(sessionId), {
|
|
106
|
+
slippage_config: config,
|
|
107
|
+
});
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Backward-compatible orchestrator (no Provider needed)
|
|
2
|
+
export { useCheckout } from './useCheckout';
|
|
3
|
+
|
|
4
|
+
// Param-based hooks (used by useCheckout internally, also usable standalone)
|
|
5
|
+
export { useCheckoutSession } from './useCheckoutSession';
|
|
6
|
+
export type { UseCheckoutSessionReturn, SessionData } from './useCheckoutSession';
|
|
7
|
+
export { usePaymentMethod } from './usePaymentMethod';
|
|
8
|
+
export type { UsePaymentMethodReturn } from './usePaymentMethod';
|
|
9
|
+
export { usePricing } from './usePricing';
|
|
10
|
+
export type { UsePricingReturn } from './usePricing';
|
|
11
|
+
export { useCustomerForm } from './useCustomerForm';
|
|
12
|
+
export type { UseCustomerFormReturn } from './useCustomerForm';
|
|
13
|
+
export { useSubmit } from './useSubmit';
|
|
14
|
+
export type { UseSubmitReturn, VendorStatus, VendorOrderStatus } from './useSubmit';
|
|
15
|
+
|
|
16
|
+
// Feature hooks (require <CheckoutProvider>)
|
|
17
|
+
export { useProduct } from './useProduct';
|
|
18
|
+
export type { UseProductReturn } from './useProduct';
|
|
19
|
+
export { useLineItems } from './useLineItems';
|
|
20
|
+
export type { UseLineItemsReturn } from './useLineItems';
|
|
21
|
+
export { useBillingInterval } from './useBillingInterval';
|
|
22
|
+
export type { UseBillingIntervalReturn, BillingIntervalData } from './useBillingInterval';
|
|
23
|
+
export { useUpsell } from './useUpsell';
|
|
24
|
+
export type { UseUpsellReturn } from './useUpsell';
|
|
25
|
+
export { useCrossSell } from './useCrossSell';
|
|
26
|
+
export type { UseCrossSellReturn } from './useCrossSell';
|
|
27
|
+
export { usePromotion } from './usePromotion';
|
|
28
|
+
export type { UsePromotionReturn } from './usePromotion';
|
|
29
|
+
export { useExchangeRate } from './useExchangeRate';
|
|
30
|
+
export type { UseExchangeRateReturn } from './useExchangeRate';
|
|
31
|
+
export { useSlippage } from './useSlippage';
|
|
32
|
+
export type { UseSlippageReturn } from './useSlippage';
|
|
33
|
+
export { useCheckoutStatus } from './useCheckoutStatus';
|
|
34
|
+
export type { UseCheckoutStatusReturn } from './useCheckoutStatus';
|
|
35
|
+
|
|
36
|
+
// Context-aware wrappers for existing hooks (require <CheckoutProvider>)
|
|
37
|
+
export { usePricingFeature } from './usePricingFeature';
|
|
38
|
+
export type { UsePricingFeatureReturn } from './usePricingFeature';
|
|
39
|
+
export { usePaymentMethodFeature } from './usePaymentMethodFeature';
|
|
40
|
+
export { useCustomerFormFeature } from './useCustomerFormFeature';
|
|
41
|
+
export { useSubmitFeature } from './useSubmitFeature';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { useMemoizedFn } from 'ahooks';
|
|
3
|
+
|
|
4
|
+
import { getErrorMessage } from '../../types/checkout-augmented';
|
|
5
|
+
import { useSessionContext } from '../context/SessionContext';
|
|
6
|
+
import { usePaymentMethodContext } from '../context/PaymentMethodContext';
|
|
7
|
+
import { parseBillingInterval, type BillingIntervalType } from '../core/billingInterval';
|
|
8
|
+
import { performUpsell, performDownsell } from '../core/lineItems';
|
|
9
|
+
|
|
10
|
+
export interface BillingIntervalData {
|
|
11
|
+
current: BillingIntervalType | null;
|
|
12
|
+
available: Array<{
|
|
13
|
+
interval: BillingIntervalType;
|
|
14
|
+
priceId: string;
|
|
15
|
+
amount: string;
|
|
16
|
+
savings: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
switch: (interval: BillingIntervalType) => Promise<void>;
|
|
19
|
+
switching: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UseBillingIntervalReturn = BillingIntervalData | null;
|
|
23
|
+
|
|
24
|
+
export function useBillingInterval(): UseBillingIntervalReturn {
|
|
25
|
+
const { items, session, effectiveSessionId, refresh } = useSessionContext();
|
|
26
|
+
const { currency } = usePaymentMethodContext();
|
|
27
|
+
const currencyId = currency?.id || null;
|
|
28
|
+
const [switching, setSwitching] = useState(false);
|
|
29
|
+
|
|
30
|
+
const upsell = useMemoizedFn(async (fromId: string, toId: string) => {
|
|
31
|
+
try {
|
|
32
|
+
await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
|
|
33
|
+
} catch (err: unknown) {
|
|
34
|
+
console.error('Failed to upsell:', getErrorMessage(err));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const downsell = useMemoizedFn(async (priceId: string) => {
|
|
39
|
+
try {
|
|
40
|
+
await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
|
|
41
|
+
} catch (err: unknown) {
|
|
42
|
+
console.error('Failed to downsell:', getErrorMessage(err));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return useMemo(() => {
|
|
47
|
+
const parsed = parseBillingInterval(items);
|
|
48
|
+
if (!parsed) return null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
current: parsed.current,
|
|
52
|
+
available: parsed.available,
|
|
53
|
+
switching,
|
|
54
|
+
switch: async (interval: BillingIntervalType) => {
|
|
55
|
+
const target = parsed.available.find((a) => a.interval === interval);
|
|
56
|
+
if (!target || switching) return;
|
|
57
|
+
|
|
58
|
+
setSwitching(true);
|
|
59
|
+
try {
|
|
60
|
+
if (!parsed.firstItem.upsell_price_id && target.priceId) {
|
|
61
|
+
await upsell(parsed.firstItem.price_id, target.priceId);
|
|
62
|
+
} else if (parsed.firstItem.upsell_price_id) {
|
|
63
|
+
await downsell(parsed.firstItem.upsell_price_id);
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
setSwitching(false);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}, [items, effectiveSessionId, switching]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
71
|
+
}
|