@funnelfox/billing 0.9.0-beta.7 → 0.9.0-ffb-467-adyen.15
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/README.md +211 -0
- package/dist/chunk-adyen-card-form.cjs.js +165 -0
- package/dist/chunk-adyen-card-form.es.js +163 -0
- package/dist/chunk-adyen-loader.cjs.js +38 -0
- package/dist/chunk-adyen-loader.es.js +36 -0
- package/dist/chunk-adyen-wallet.cjs.js +200 -0
- package/dist/chunk-adyen-wallet.es.js +197 -0
- package/dist/chunk-index.cjs.js +179 -10
- package/dist/chunk-index.es.js +178 -11
- package/dist/chunk-stripe-card-form.cjs.js +94 -10
- package/dist/chunk-stripe-card-form.es.js +94 -10
- package/dist/chunk-stripe-loader.es.js +1 -1
- package/dist/chunk-stripe-wallet.cjs.js +37 -7
- package/dist/chunk-stripe-wallet.es.js +37 -7
- package/dist/funnelfox-billing.esm.js +1 -1
- package/dist/funnelfox-billing.js +683 -27
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +109 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A modern TypeScript SDK for subscription payments with Primer Headless Checkout
|
|
|
11
11
|
- 🔧 **Robust**: Built-in error handling, retries, and validation
|
|
12
12
|
- 📦 **Lightweight**: Minimal dependencies, browser-optimized
|
|
13
13
|
- 🎨 **Headless Checkout**: Full control over checkout UI with Primer Headless Checkout
|
|
14
|
+
- 💳 **Stripe Integration**: Native Stripe Elements card form and Apple Pay / Google Pay wallets
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -711,6 +712,216 @@ const headlessCheckout = await Primer.createHeadless(session.clientToken, {
|
|
|
711
712
|
await headlessCheckout.start();
|
|
712
713
|
```
|
|
713
714
|
|
|
715
|
+
## Stripe Integration
|
|
716
|
+
|
|
717
|
+
The `Billing.stripe` namespace provides a Stripe-native checkout experience — no Primer dependency required. It supports card payments via Stripe Elements and native wallet payments (Apple Pay / Google Pay) via the Payment Request API.
|
|
718
|
+
|
|
719
|
+
> **Note:** `@primer-io/checkout-web` is **not** required for Stripe integration. Only `@funnelfox/billing` and a Stripe-enabled price in your Funnelfox account are needed.
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
### `Billing.stripe.createCardForm(element, params)`
|
|
724
|
+
|
|
725
|
+
Mounts a Stripe Elements payment form into a DOM element. Returns a `{ submit() }` handle — you control when payment is triggered (e.g. on your own button click).
|
|
726
|
+
|
|
727
|
+
```javascript
|
|
728
|
+
const element = document.getElementById('card-form');
|
|
729
|
+
|
|
730
|
+
const cardForm = await Billing.stripe.createCardForm(element, {
|
|
731
|
+
// Required
|
|
732
|
+
priceId: 'price_123',
|
|
733
|
+
externalId: 'user_456',
|
|
734
|
+
|
|
735
|
+
// Optional
|
|
736
|
+
orgId: 'your-org-id',
|
|
737
|
+
email: 'user@example.com',
|
|
738
|
+
countryCode: 'US',
|
|
739
|
+
showWallets: false, // show Apple Pay / Google Pay inside the form
|
|
740
|
+
appearance: {
|
|
741
|
+
// Stripe Elements appearance API
|
|
742
|
+
theme: 'stripe',
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
// Callbacks
|
|
746
|
+
onRenderSuccess: () => {
|
|
747
|
+
document.getElementById('pay-button').disabled = false;
|
|
748
|
+
},
|
|
749
|
+
onLoaderChange: loading => {
|
|
750
|
+
document.getElementById('pay-button').disabled = loading;
|
|
751
|
+
},
|
|
752
|
+
onPaymentSuccess: (paymentMethod, orderId) => {
|
|
753
|
+
window.location.href = '/success?order=' + orderId;
|
|
754
|
+
},
|
|
755
|
+
onPaymentFail: error => {
|
|
756
|
+
console.error('Payment failed:', error.message);
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Wire up your own submit button
|
|
761
|
+
document.getElementById('pay-button').addEventListener('click', async () => {
|
|
762
|
+
await cardForm.submit();
|
|
763
|
+
});
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
**Key parameters:**
|
|
767
|
+
|
|
768
|
+
| Parameter | Type | Description |
|
|
769
|
+
| ---------------- | -------- | --------------------------------------------------------------------------------- |
|
|
770
|
+
| `priceId` | string | Price identifier |
|
|
771
|
+
| `externalId` | string | Your user identifier |
|
|
772
|
+
| `email` | string? | Customer email |
|
|
773
|
+
| `orgId` | string? | Org ID (if not globally configured) |
|
|
774
|
+
| `showWallets` | boolean? | Show Apple Pay / Google Pay inside the Stripe form |
|
|
775
|
+
| `appearance` | object? | [Stripe Elements Appearance API](https://stripe.com/docs/elements/appearance-api) |
|
|
776
|
+
| `clientMetadata` | object? | Custom metadata attached to the order |
|
|
777
|
+
|
|
778
|
+
**Returns:** `Promise<{ submit: () => Promise<void> }>`
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
### `Billing.stripe.getAvailableWallet(params)`
|
|
783
|
+
|
|
784
|
+
Checks whether Apple Pay or Google Pay is available on the current device and browser. Use this to conditionally show a wallet button before attempting payment.
|
|
785
|
+
|
|
786
|
+
```javascript
|
|
787
|
+
const wallet = await Billing.stripe.getAvailableWallet({
|
|
788
|
+
priceId: 'price_123',
|
|
789
|
+
externalId: 'user_456',
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
if (wallet === 'APPLE_PAY') {
|
|
793
|
+
document.getElementById('apple-pay-btn').style.display = 'block';
|
|
794
|
+
} else if (wallet === 'GOOGLE_PAY') {
|
|
795
|
+
document.getElementById('google-pay-btn').style.display = 'block';
|
|
796
|
+
} else {
|
|
797
|
+
// No wallet available — show card form only
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Returns:** `Promise<'APPLE_PAY' | 'GOOGLE_PAY' | null>`
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
### `Billing.stripe.purchaseWallet(params)`
|
|
806
|
+
|
|
807
|
+
Triggers the native Apple Pay or Google Pay payment sheet. Call this on button click after confirming a wallet is available via `getAvailableWallet`.
|
|
808
|
+
|
|
809
|
+
```javascript
|
|
810
|
+
document.getElementById('wallet-btn').addEventListener('click', async () => {
|
|
811
|
+
await Billing.stripe.purchaseWallet({
|
|
812
|
+
priceId: 'price_123',
|
|
813
|
+
externalId: 'user_456',
|
|
814
|
+
totalLabel: 'Premium Plan', // Label shown in the payment sheet
|
|
815
|
+
|
|
816
|
+
onPaymentSuccess: (paymentMethod, orderId) => {
|
|
817
|
+
window.location.href = '/success?order=' + orderId;
|
|
818
|
+
},
|
|
819
|
+
onPaymentFail: error => {
|
|
820
|
+
console.error('Wallet payment failed:', error.message);
|
|
821
|
+
},
|
|
822
|
+
onPaymentCancel: () => {
|
|
823
|
+
console.log('User cancelled');
|
|
824
|
+
},
|
|
825
|
+
onLoaderChange: loading => {
|
|
826
|
+
document.getElementById('wallet-btn').disabled = loading;
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Key parameters:**
|
|
833
|
+
|
|
834
|
+
| Parameter | Type | Description |
|
|
835
|
+
| ---------------- | ------- | --------------------------------------------------- |
|
|
836
|
+
| `priceId` | string | Price identifier |
|
|
837
|
+
| `externalId` | string | Your user identifier |
|
|
838
|
+
| `totalLabel` | string? | Label shown next to the amount in the payment sheet |
|
|
839
|
+
| `email` | string? | Customer email |
|
|
840
|
+
| `clientMetadata` | object? | Custom metadata attached to the order |
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
### `Billing.stripe.getAvailablePaymentMethods(params)`
|
|
845
|
+
|
|
846
|
+
Returns all Stripe payment methods available for the current device. Always includes `PAYMENT_CARD`; also includes a wallet method if one is detected.
|
|
847
|
+
|
|
848
|
+
```javascript
|
|
849
|
+
const methods = await Billing.stripe.getAvailablePaymentMethods({
|
|
850
|
+
priceId: 'price_123',
|
|
851
|
+
externalId: 'user_456',
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// methods: ['PAYMENT_CARD', 'APPLE_PAY'] or ['PAYMENT_CARD'] etc.
|
|
855
|
+
console.log('Available:', methods);
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Returns:** `Promise<PaymentMethod[]>` — always contains `PAYMENT_CARD`, optionally `APPLE_PAY` or `GOOGLE_PAY`
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
### Combined Example: Wallet Detection + Card Form Fallback
|
|
863
|
+
|
|
864
|
+
The recommended pattern — show a wallet button when available, always show the card form as fallback:
|
|
865
|
+
|
|
866
|
+
```javascript
|
|
867
|
+
import { Billing } from '@funnelfox/billing';
|
|
868
|
+
|
|
869
|
+
Billing.configure({ orgId: 'your-org-id' });
|
|
870
|
+
|
|
871
|
+
const params = {
|
|
872
|
+
priceId: 'price_123',
|
|
873
|
+
externalId: 'user_456',
|
|
874
|
+
email: 'user@example.com',
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
async function initCheckout() {
|
|
878
|
+
// 1. Check for wallet availability
|
|
879
|
+
const wallet = await Billing.stripe.getAvailableWallet(params);
|
|
880
|
+
|
|
881
|
+
if (wallet) {
|
|
882
|
+
const walletBtn = document.getElementById('wallet-btn');
|
|
883
|
+
walletBtn.textContent =
|
|
884
|
+
wallet === 'APPLE_PAY' ? 'Pay with Apple Pay' : 'Pay with Google Pay';
|
|
885
|
+
walletBtn.style.display = 'block';
|
|
886
|
+
|
|
887
|
+
walletBtn.addEventListener('click', () => {
|
|
888
|
+
Billing.stripe.purchaseWallet({
|
|
889
|
+
...params,
|
|
890
|
+
totalLabel: 'Premium Plan',
|
|
891
|
+
onPaymentSuccess: (_, orderId) => {
|
|
892
|
+
window.location.href = '/success?order=' + orderId;
|
|
893
|
+
},
|
|
894
|
+
onPaymentFail: err => alert(err.message),
|
|
895
|
+
onPaymentCancel: () => console.log('Cancelled'),
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 2. Always mount card form as fallback
|
|
901
|
+
const cardForm = await Billing.stripe.createCardForm(
|
|
902
|
+
document.getElementById('card-form'),
|
|
903
|
+
{
|
|
904
|
+
...params,
|
|
905
|
+
onPaymentSuccess: (_, orderId) => {
|
|
906
|
+
window.location.href = '/success?order=' + orderId;
|
|
907
|
+
},
|
|
908
|
+
onPaymentFail: err => alert(err.message),
|
|
909
|
+
onLoaderChange: loading => {
|
|
910
|
+
document.getElementById('pay-btn').disabled = loading;
|
|
911
|
+
},
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
document.getElementById('pay-btn').addEventListener('click', () => {
|
|
916
|
+
cardForm.submit();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
initCheckout();
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
714
925
|
## Browser Support
|
|
715
926
|
|
|
716
927
|
- Chrome 60+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @funnelfox/billing v0.1.0
|
|
3
|
+
* JavaScript SDK for Funnelfox billing with Primer integration
|
|
4
|
+
*
|
|
5
|
+
* @author Funnelfox
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var adyenLoader = require('./chunk-adyen-loader.cjs.js');
|
|
11
|
+
require('./chunk-index.cjs.js');
|
|
12
|
+
|
|
13
|
+
const TAX_RECALC_DEBOUNCE_MS = 600;
|
|
14
|
+
async function mountAdyenCardForm(element, session, params) {
|
|
15
|
+
const { adyen_client_key, adyen_payment_methods, amount, amount_total, currency, is_livemode, order_id, client_token, detected_country_code, show_country_selector_field, show_postal_code_field, } = session.data;
|
|
16
|
+
const taxEnabled = session.data.tax_enabled ?? params.enableTax ?? false;
|
|
17
|
+
const currencyUpper = (currency || 'usd').toUpperCase();
|
|
18
|
+
const AdyenCheckout = await adyenLoader.getAdyenCheckout(!!is_livemode);
|
|
19
|
+
// Remounting (e.g. picking another price) must replace the form, not stack a second one.
|
|
20
|
+
element.replaceChildren();
|
|
21
|
+
let taxCalculationId;
|
|
22
|
+
let taxAddress = {};
|
|
23
|
+
let flushTaxRecalc = async () => { };
|
|
24
|
+
let resolvePayment;
|
|
25
|
+
let rejectPayment;
|
|
26
|
+
const runRecalc = async () => {
|
|
27
|
+
const country = taxAddress.country || detected_country_code;
|
|
28
|
+
// US tax needs the ZIP to resolve a jurisdiction; skip until it's entered.
|
|
29
|
+
if (!country || (country === 'US' && !taxAddress.postalCode))
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
const tax = await params.apiClient.recalculateTax({
|
|
33
|
+
orderId: order_id,
|
|
34
|
+
clientToken: client_token,
|
|
35
|
+
countryCode: country,
|
|
36
|
+
postalCode: taxAddress.postalCode,
|
|
37
|
+
subdivision: taxAddress.state,
|
|
38
|
+
});
|
|
39
|
+
taxCalculationId = tax.tax_calculation_id;
|
|
40
|
+
params.onTaxChange?.({
|
|
41
|
+
amountTotal: tax.amount_total,
|
|
42
|
+
taxAmount: tax.tax_amount,
|
|
43
|
+
currency: tax.currency,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Best-effort: keep the current total if recalculation fails.
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const finalize = async (raw, component) => {
|
|
51
|
+
const result = params.apiClient.processPaymentResponse(raw);
|
|
52
|
+
if (result.type === 'action_required') {
|
|
53
|
+
// Adyen packs the next action (3DS) JSON into action_required_token; the challenge result
|
|
54
|
+
// comes back through onAdditionalDetails, which resumes the payment.
|
|
55
|
+
component.handleAction(JSON.parse(result.clientToken));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
params.onPaymentSuccess?.(order_id);
|
|
59
|
+
resolvePayment?.();
|
|
60
|
+
};
|
|
61
|
+
const checkout = await AdyenCheckout({
|
|
62
|
+
environment: is_livemode ? 'live' : 'test',
|
|
63
|
+
clientKey: adyen_client_key,
|
|
64
|
+
paymentMethodsResponse: adyen_payment_methods,
|
|
65
|
+
amount: { value: amount_total ?? amount ?? 0, currency: currencyUpper },
|
|
66
|
+
locale: 'en-US',
|
|
67
|
+
onSubmit: (state, component) => {
|
|
68
|
+
void (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const address = state.data.billingAddress || {};
|
|
71
|
+
const raw = await params.apiClient.createPayment({
|
|
72
|
+
orderId: order_id,
|
|
73
|
+
paymentMethodToken: JSON.stringify(state.data),
|
|
74
|
+
email: params.email,
|
|
75
|
+
countryCode: taxEnabled ? address.country : params.countryCode,
|
|
76
|
+
postalCode: taxEnabled ? address.postalCode : undefined,
|
|
77
|
+
subdivision: taxEnabled ? address.stateOrProvince : undefined,
|
|
78
|
+
taxCalculationId: taxEnabled ? taxCalculationId : undefined,
|
|
79
|
+
clientMetadata: params.clientMetadata,
|
|
80
|
+
});
|
|
81
|
+
await finalize(raw, component);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
params.onPaymentFail?.(err);
|
|
85
|
+
rejectPayment?.(err);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
},
|
|
89
|
+
onAdditionalDetails: (state, component) => {
|
|
90
|
+
void (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const raw = await params.apiClient.resumePayment({
|
|
93
|
+
orderId: order_id,
|
|
94
|
+
resumeToken: JSON.stringify(state.data),
|
|
95
|
+
});
|
|
96
|
+
await finalize(raw, component);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
params.onPaymentFail?.(err);
|
|
100
|
+
rejectPayment?.(err);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
},
|
|
104
|
+
onError: error => params.onPaymentFail?.(error),
|
|
105
|
+
});
|
|
106
|
+
const billingFields = [];
|
|
107
|
+
if (show_country_selector_field)
|
|
108
|
+
billingFields.push('country');
|
|
109
|
+
if (show_postal_code_field)
|
|
110
|
+
billingFields.push('postalCode');
|
|
111
|
+
let debounce;
|
|
112
|
+
const cardConfig = {
|
|
113
|
+
showPayButton: false,
|
|
114
|
+
billingAddressRequired: billingFields.length > 0,
|
|
115
|
+
billingAddressRequiredFields: billingFields,
|
|
116
|
+
onChange: (state) => {
|
|
117
|
+
if (!taxEnabled)
|
|
118
|
+
return;
|
|
119
|
+
const address = state.data.billingAddress;
|
|
120
|
+
if (!address)
|
|
121
|
+
return;
|
|
122
|
+
taxAddress = {
|
|
123
|
+
country: address.country,
|
|
124
|
+
postalCode: address.postalCode || undefined,
|
|
125
|
+
state: address.stateOrProvince || undefined,
|
|
126
|
+
};
|
|
127
|
+
if (debounce)
|
|
128
|
+
clearTimeout(debounce);
|
|
129
|
+
debounce = setTimeout(() => void runRecalc(), TAX_RECALC_DEBOUNCE_MS);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
if (detected_country_code) {
|
|
133
|
+
cardConfig.data = { billingAddress: { country: detected_country_code } };
|
|
134
|
+
}
|
|
135
|
+
const card = checkout.create('card', cardConfig);
|
|
136
|
+
card.mount(element);
|
|
137
|
+
flushTaxRecalc = async () => {
|
|
138
|
+
if (debounce) {
|
|
139
|
+
clearTimeout(debounce);
|
|
140
|
+
debounce = undefined;
|
|
141
|
+
}
|
|
142
|
+
if (taxEnabled)
|
|
143
|
+
await runRecalc();
|
|
144
|
+
};
|
|
145
|
+
params.onRenderSuccess?.();
|
|
146
|
+
return {
|
|
147
|
+
submit: async () => {
|
|
148
|
+
params.onLoaderChange?.(true);
|
|
149
|
+
const done = new Promise((resolve, reject) => {
|
|
150
|
+
resolvePayment = resolve;
|
|
151
|
+
rejectPayment = reject;
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
await flushTaxRecalc();
|
|
155
|
+
card.submit();
|
|
156
|
+
await done;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
params.onLoaderChange?.(false);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
exports.mountAdyenCardForm = mountAdyenCardForm;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @funnelfox/billing v0.1.0
|
|
3
|
+
* JavaScript SDK for Funnelfox billing with Primer integration
|
|
4
|
+
*
|
|
5
|
+
* @author Funnelfox
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
import { g as getAdyenCheckout } from './chunk-adyen-loader.es.js';
|
|
9
|
+
import './chunk-index.es.js';
|
|
10
|
+
|
|
11
|
+
const TAX_RECALC_DEBOUNCE_MS = 600;
|
|
12
|
+
async function mountAdyenCardForm(element, session, params) {
|
|
13
|
+
const { adyen_client_key, adyen_payment_methods, amount, amount_total, currency, is_livemode, order_id, client_token, detected_country_code, show_country_selector_field, show_postal_code_field, } = session.data;
|
|
14
|
+
const taxEnabled = session.data.tax_enabled ?? params.enableTax ?? false;
|
|
15
|
+
const currencyUpper = (currency || 'usd').toUpperCase();
|
|
16
|
+
const AdyenCheckout = await getAdyenCheckout(!!is_livemode);
|
|
17
|
+
// Remounting (e.g. picking another price) must replace the form, not stack a second one.
|
|
18
|
+
element.replaceChildren();
|
|
19
|
+
let taxCalculationId;
|
|
20
|
+
let taxAddress = {};
|
|
21
|
+
let flushTaxRecalc = async () => { };
|
|
22
|
+
let resolvePayment;
|
|
23
|
+
let rejectPayment;
|
|
24
|
+
const runRecalc = async () => {
|
|
25
|
+
const country = taxAddress.country || detected_country_code;
|
|
26
|
+
// US tax needs the ZIP to resolve a jurisdiction; skip until it's entered.
|
|
27
|
+
if (!country || (country === 'US' && !taxAddress.postalCode))
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const tax = await params.apiClient.recalculateTax({
|
|
31
|
+
orderId: order_id,
|
|
32
|
+
clientToken: client_token,
|
|
33
|
+
countryCode: country,
|
|
34
|
+
postalCode: taxAddress.postalCode,
|
|
35
|
+
subdivision: taxAddress.state,
|
|
36
|
+
});
|
|
37
|
+
taxCalculationId = tax.tax_calculation_id;
|
|
38
|
+
params.onTaxChange?.({
|
|
39
|
+
amountTotal: tax.amount_total,
|
|
40
|
+
taxAmount: tax.tax_amount,
|
|
41
|
+
currency: tax.currency,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Best-effort: keep the current total if recalculation fails.
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const finalize = async (raw, component) => {
|
|
49
|
+
const result = params.apiClient.processPaymentResponse(raw);
|
|
50
|
+
if (result.type === 'action_required') {
|
|
51
|
+
// Adyen packs the next action (3DS) JSON into action_required_token; the challenge result
|
|
52
|
+
// comes back through onAdditionalDetails, which resumes the payment.
|
|
53
|
+
component.handleAction(JSON.parse(result.clientToken));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
params.onPaymentSuccess?.(order_id);
|
|
57
|
+
resolvePayment?.();
|
|
58
|
+
};
|
|
59
|
+
const checkout = await AdyenCheckout({
|
|
60
|
+
environment: is_livemode ? 'live' : 'test',
|
|
61
|
+
clientKey: adyen_client_key,
|
|
62
|
+
paymentMethodsResponse: adyen_payment_methods,
|
|
63
|
+
amount: { value: amount_total ?? amount ?? 0, currency: currencyUpper },
|
|
64
|
+
locale: 'en-US',
|
|
65
|
+
onSubmit: (state, component) => {
|
|
66
|
+
void (async () => {
|
|
67
|
+
try {
|
|
68
|
+
const address = state.data.billingAddress || {};
|
|
69
|
+
const raw = await params.apiClient.createPayment({
|
|
70
|
+
orderId: order_id,
|
|
71
|
+
paymentMethodToken: JSON.stringify(state.data),
|
|
72
|
+
email: params.email,
|
|
73
|
+
countryCode: taxEnabled ? address.country : params.countryCode,
|
|
74
|
+
postalCode: taxEnabled ? address.postalCode : undefined,
|
|
75
|
+
subdivision: taxEnabled ? address.stateOrProvince : undefined,
|
|
76
|
+
taxCalculationId: taxEnabled ? taxCalculationId : undefined,
|
|
77
|
+
clientMetadata: params.clientMetadata,
|
|
78
|
+
});
|
|
79
|
+
await finalize(raw, component);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
params.onPaymentFail?.(err);
|
|
83
|
+
rejectPayment?.(err);
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
},
|
|
87
|
+
onAdditionalDetails: (state, component) => {
|
|
88
|
+
void (async () => {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await params.apiClient.resumePayment({
|
|
91
|
+
orderId: order_id,
|
|
92
|
+
resumeToken: JSON.stringify(state.data),
|
|
93
|
+
});
|
|
94
|
+
await finalize(raw, component);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
params.onPaymentFail?.(err);
|
|
98
|
+
rejectPayment?.(err);
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
},
|
|
102
|
+
onError: error => params.onPaymentFail?.(error),
|
|
103
|
+
});
|
|
104
|
+
const billingFields = [];
|
|
105
|
+
if (show_country_selector_field)
|
|
106
|
+
billingFields.push('country');
|
|
107
|
+
if (show_postal_code_field)
|
|
108
|
+
billingFields.push('postalCode');
|
|
109
|
+
let debounce;
|
|
110
|
+
const cardConfig = {
|
|
111
|
+
showPayButton: false,
|
|
112
|
+
billingAddressRequired: billingFields.length > 0,
|
|
113
|
+
billingAddressRequiredFields: billingFields,
|
|
114
|
+
onChange: (state) => {
|
|
115
|
+
if (!taxEnabled)
|
|
116
|
+
return;
|
|
117
|
+
const address = state.data.billingAddress;
|
|
118
|
+
if (!address)
|
|
119
|
+
return;
|
|
120
|
+
taxAddress = {
|
|
121
|
+
country: address.country,
|
|
122
|
+
postalCode: address.postalCode || undefined,
|
|
123
|
+
state: address.stateOrProvince || undefined,
|
|
124
|
+
};
|
|
125
|
+
if (debounce)
|
|
126
|
+
clearTimeout(debounce);
|
|
127
|
+
debounce = setTimeout(() => void runRecalc(), TAX_RECALC_DEBOUNCE_MS);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
if (detected_country_code) {
|
|
131
|
+
cardConfig.data = { billingAddress: { country: detected_country_code } };
|
|
132
|
+
}
|
|
133
|
+
const card = checkout.create('card', cardConfig);
|
|
134
|
+
card.mount(element);
|
|
135
|
+
flushTaxRecalc = async () => {
|
|
136
|
+
if (debounce) {
|
|
137
|
+
clearTimeout(debounce);
|
|
138
|
+
debounce = undefined;
|
|
139
|
+
}
|
|
140
|
+
if (taxEnabled)
|
|
141
|
+
await runRecalc();
|
|
142
|
+
};
|
|
143
|
+
params.onRenderSuccess?.();
|
|
144
|
+
return {
|
|
145
|
+
submit: async () => {
|
|
146
|
+
params.onLoaderChange?.(true);
|
|
147
|
+
const done = new Promise((resolve, reject) => {
|
|
148
|
+
resolvePayment = resolve;
|
|
149
|
+
rejectPayment = reject;
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
await flushTaxRecalc();
|
|
153
|
+
card.submit();
|
|
154
|
+
await done;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
params.onLoaderChange?.(false);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { mountAdyenCardForm };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @funnelfox/billing v0.1.0
|
|
3
|
+
* JavaScript SDK for Funnelfox billing with Primer integration
|
|
4
|
+
*
|
|
5
|
+
* @author Funnelfox
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var index = require('./chunk-index.cjs.js');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @fileoverview Adyen Web SDK (Drop-in/Components) CDN loader.
|
|
14
|
+
*
|
|
15
|
+
* Adyen Web is loaded from the checkoutshopper CDN (not npm), so we declare the minimal shapes we
|
|
16
|
+
* use rather than depend on @adyen/adyen-web types.
|
|
17
|
+
*/
|
|
18
|
+
const ADYEN_WEB_VERSION = '5.66.0';
|
|
19
|
+
/**
|
|
20
|
+
* Loads Adyen Web (adyen.js + adyen.css) from the CDN and returns the AdyenCheckout factory.
|
|
21
|
+
* Host is livemode-aware; the loaders dedupe so repeated calls are cheap.
|
|
22
|
+
*/
|
|
23
|
+
async function getAdyenCheckout(isLivemode) {
|
|
24
|
+
const host = isLivemode
|
|
25
|
+
? 'https://checkoutshopper-live.adyen.com'
|
|
26
|
+
: 'https://checkoutshopper-test.adyen.com';
|
|
27
|
+
const base = `${host}/checkoutshopper/sdk/${ADYEN_WEB_VERSION}`;
|
|
28
|
+
await Promise.all([
|
|
29
|
+
index.loadStylesheet({ href: `${base}/adyen.css` }),
|
|
30
|
+
index.loadScript({ id: 'adyen-web-sdk', src: `${base}/adyen.js`, async: true }),
|
|
31
|
+
]);
|
|
32
|
+
const factory = window.AdyenCheckout;
|
|
33
|
+
if (!factory)
|
|
34
|
+
throw new Error('Failed to load Adyen Web SDK');
|
|
35
|
+
return factory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.getAdyenCheckout = getAdyenCheckout;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @funnelfox/billing v0.1.0
|
|
3
|
+
* JavaScript SDK for Funnelfox billing with Primer integration
|
|
4
|
+
*
|
|
5
|
+
* @author Funnelfox
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
import { l as loadStylesheet, a as loadScript } from './chunk-index.es.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @fileoverview Adyen Web SDK (Drop-in/Components) CDN loader.
|
|
12
|
+
*
|
|
13
|
+
* Adyen Web is loaded from the checkoutshopper CDN (not npm), so we declare the minimal shapes we
|
|
14
|
+
* use rather than depend on @adyen/adyen-web types.
|
|
15
|
+
*/
|
|
16
|
+
const ADYEN_WEB_VERSION = '5.66.0';
|
|
17
|
+
/**
|
|
18
|
+
* Loads Adyen Web (adyen.js + adyen.css) from the CDN and returns the AdyenCheckout factory.
|
|
19
|
+
* Host is livemode-aware; the loaders dedupe so repeated calls are cheap.
|
|
20
|
+
*/
|
|
21
|
+
async function getAdyenCheckout(isLivemode) {
|
|
22
|
+
const host = isLivemode
|
|
23
|
+
? 'https://checkoutshopper-live.adyen.com'
|
|
24
|
+
: 'https://checkoutshopper-test.adyen.com';
|
|
25
|
+
const base = `${host}/checkoutshopper/sdk/${ADYEN_WEB_VERSION}`;
|
|
26
|
+
await Promise.all([
|
|
27
|
+
loadStylesheet({ href: `${base}/adyen.css` }),
|
|
28
|
+
loadScript({ id: 'adyen-web-sdk', src: `${base}/adyen.js`, async: true }),
|
|
29
|
+
]);
|
|
30
|
+
const factory = window.AdyenCheckout;
|
|
31
|
+
if (!factory)
|
|
32
|
+
throw new Error('Failed to load Adyen Web SDK');
|
|
33
|
+
return factory;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { getAdyenCheckout as g };
|