recur-tw 0.0.4-beta.4 â 0.1.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/README.md +117 -0
- package/dist/index.cjs +263 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +263 -2
- package/dist/index.js.map +1 -1
- package/dist/recur.umd.js +124 -18
- package/dist/recur.umd.js.map +1 -1
- package/package.json +16 -15
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Taiwan-specific subscription checkout SDK with embedded payment forms powered by
|
|
|
8
8
|
|
|
9
9
|
## đ Features
|
|
10
10
|
|
|
11
|
+
- â
**Hosted Checkout** - Zero-backend integration with `<recur-checkout>` Web Component
|
|
11
12
|
- â
**React SDK** - Full React integration with hooks (`usePlans`, `useRecur`)
|
|
12
13
|
- â
**Vanilla JS** - Use with plain HTML/JavaScript (no framework required)
|
|
13
14
|
- â
**Embedded Checkout** - Native payment forms with PAYUNi credit card fields
|
|
@@ -130,6 +131,43 @@ Perfect for landing pages, marketing sites, or any static HTML:
|
|
|
130
131
|
</html>
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
### Option 3: Hosted Checkout (Zero Backend)
|
|
135
|
+
|
|
136
|
+
**NEW!** The simplest integration - redirect to Recur's hosted checkout page:
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<!DOCTYPE html>
|
|
140
|
+
<html>
|
|
141
|
+
<body>
|
|
142
|
+
<!-- Drop-in checkout button - no JavaScript required! -->
|
|
143
|
+
<recur-checkout
|
|
144
|
+
publishable-key="pk_test_xxx"
|
|
145
|
+
product-id="prod_xxx"
|
|
146
|
+
success-url="https://yoursite.com/success"
|
|
147
|
+
cancel-url="https://yoursite.com/cancel">
|
|
148
|
+
č¨éąćšćĄ
|
|
149
|
+
</recur-checkout>
|
|
150
|
+
|
|
151
|
+
<script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Or use the JavaScript API:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const recur = RecurCheckout.init({
|
|
160
|
+
publishableKey: 'pk_test_xxx'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Redirect to hosted checkout
|
|
164
|
+
await recur.redirectToCheckout({
|
|
165
|
+
productId: 'prod_xxx',
|
|
166
|
+
successUrl: 'https://yoursite.com/success',
|
|
167
|
+
cancelUrl: 'https://yoursite.com/cancel',
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
133
171
|
---
|
|
134
172
|
|
|
135
173
|
## đ Get Your API Key
|
|
@@ -182,6 +220,85 @@ await checkout({
|
|
|
182
220
|
});
|
|
183
221
|
```
|
|
184
222
|
|
|
223
|
+
#### Hosted Checkout Mode (NEW!)
|
|
224
|
+
|
|
225
|
+
The simplest integration - no iframe or embedded form needed:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const recur = RecurCheckout.init({
|
|
229
|
+
publishableKey: 'pk_test_xxx'
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Method 1: Redirect to hosted checkout
|
|
233
|
+
await recur.redirectToCheckout({
|
|
234
|
+
productId: 'prod_xxx',
|
|
235
|
+
successUrl: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
|
|
236
|
+
cancelUrl: 'https://yoursite.com/cancel',
|
|
237
|
+
customerEmail: 'user@example.com', // optional, pre-fill email
|
|
238
|
+
mode: 'SUBSCRIPTION', // PAYMENT, SUBSCRIPTION, or SETUP
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Method 2: Get session URL without redirecting
|
|
242
|
+
const session = await recur.createCheckoutSession({
|
|
243
|
+
productId: 'prod_xxx',
|
|
244
|
+
successUrl: 'https://yoursite.com/success',
|
|
245
|
+
cancelUrl: 'https://yoursite.com/cancel',
|
|
246
|
+
});
|
|
247
|
+
console.log(session.url); // https://checkout.recur.tw/cs_xxx
|
|
248
|
+
console.log(session.expiresAt); // Session expires in 30 minutes
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Checkout Modes:**
|
|
252
|
+
- `PAYMENT` - One-time payment
|
|
253
|
+
- `SUBSCRIPTION` - Recurring subscription (default)
|
|
254
|
+
- `SETUP` - Save card for future charges
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `<recur-checkout>` Web Component
|
|
259
|
+
|
|
260
|
+
A drop-in checkout button that requires zero JavaScript:
|
|
261
|
+
|
|
262
|
+
```html
|
|
263
|
+
<recur-checkout
|
|
264
|
+
publishable-key="pk_test_xxx"
|
|
265
|
+
product-id="prod_xxx"
|
|
266
|
+
success-url="/success"
|
|
267
|
+
cancel-url="/cancel"
|
|
268
|
+
customer-email="user@example.com"
|
|
269
|
+
mode="SUBSCRIPTION"
|
|
270
|
+
button-text="Subscribe Now"
|
|
271
|
+
button-style="gradient">
|
|
272
|
+
</recur-checkout>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Attributes:**
|
|
276
|
+
|
|
277
|
+
| Attribute | Required | Description |
|
|
278
|
+
|-----------|----------|-------------|
|
|
279
|
+
| `publishable-key` | Yes | Your Recur publishable key |
|
|
280
|
+
| `product-id` | Yes | The product ID to purchase |
|
|
281
|
+
| `success-url` | Yes | Redirect URL after successful payment |
|
|
282
|
+
| `cancel-url` | Yes | Redirect URL if user cancels |
|
|
283
|
+
| `customer-email` | No | Pre-fill customer email |
|
|
284
|
+
| `mode` | No | `PAYMENT`, `SUBSCRIPTION`, or `SETUP` |
|
|
285
|
+
| `button-text` | No | Button label (default: button content) |
|
|
286
|
+
| `button-style` | No | `primary`, `outline`, or `gradient` |
|
|
287
|
+
| `disabled` | No | Disable the button |
|
|
288
|
+
| `api-base-url` | No | Custom API URL (for development) |
|
|
289
|
+
|
|
290
|
+
**Events:**
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
document.querySelector('recur-checkout').addEventListener('checkout-started', (e) => {
|
|
294
|
+
console.log('Redirecting to:', e.detail.url);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
document.querySelector('recur-checkout').addEventListener('checkout-error', (e) => {
|
|
298
|
+
console.error('Error:', e.detail.message);
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
185
302
|
---
|
|
186
303
|
|
|
187
304
|
### React Hooks
|
package/dist/index.cjs
CHANGED
|
@@ -1885,6 +1885,265 @@ var init_payment_form = __esm({
|
|
|
1885
1885
|
}
|
|
1886
1886
|
});
|
|
1887
1887
|
|
|
1888
|
+
// src/components/checkout-button.ts
|
|
1889
|
+
var checkout_button_exports = {};
|
|
1890
|
+
__export(checkout_button_exports, {
|
|
1891
|
+
RecurCheckoutButton: () => RecurCheckoutButton
|
|
1892
|
+
});
|
|
1893
|
+
var RecurCheckoutButton;
|
|
1894
|
+
var init_checkout_button = __esm({
|
|
1895
|
+
"src/components/checkout-button.ts"() {
|
|
1896
|
+
RecurCheckoutButton = class extends HTMLElement {
|
|
1897
|
+
constructor() {
|
|
1898
|
+
super();
|
|
1899
|
+
__publicField(this, "_isLoading", false);
|
|
1900
|
+
__publicField(this, "handleClick", async (e) => {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
if (this._isLoading || this.hasAttribute("disabled")) {
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const publishableKey = this.getAttribute("publishable-key");
|
|
1906
|
+
const productId = this.getAttribute("product-id");
|
|
1907
|
+
const successUrl = this.getAttribute("success-url");
|
|
1908
|
+
const cancelUrl = this.getAttribute("cancel-url");
|
|
1909
|
+
if (!publishableKey) {
|
|
1910
|
+
this.dispatchError("Missing required attribute: publishable-key");
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (!productId) {
|
|
1914
|
+
this.dispatchError("Missing required attribute: product-id");
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (!successUrl) {
|
|
1918
|
+
this.dispatchError("Missing required attribute: success-url");
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (!cancelUrl) {
|
|
1922
|
+
this.dispatchError("Missing required attribute: cancel-url");
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
this._isLoading = true;
|
|
1926
|
+
this.render();
|
|
1927
|
+
this.setupEventListeners();
|
|
1928
|
+
try {
|
|
1929
|
+
const session = await this.createCheckoutSession({
|
|
1930
|
+
publishableKey,
|
|
1931
|
+
productId,
|
|
1932
|
+
successUrl: this.resolveUrl(successUrl),
|
|
1933
|
+
cancelUrl: this.resolveUrl(cancelUrl),
|
|
1934
|
+
customerEmail: this.getAttribute("customer-email") || void 0,
|
|
1935
|
+
mode: this.getAttribute("mode")
|
|
1936
|
+
});
|
|
1937
|
+
this.dispatchEvent(new CustomEvent("checkout-started", {
|
|
1938
|
+
detail: { sessionId: session.id, url: session.url },
|
|
1939
|
+
bubbles: true,
|
|
1940
|
+
composed: true
|
|
1941
|
+
}));
|
|
1942
|
+
window.location.href = session.url;
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
this._isLoading = false;
|
|
1945
|
+
this.render();
|
|
1946
|
+
this.setupEventListeners();
|
|
1947
|
+
this.dispatchError(error.message || "Failed to create checkout session");
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
this.attachShadow({ mode: "open" });
|
|
1951
|
+
}
|
|
1952
|
+
static get observedAttributes() {
|
|
1953
|
+
return [
|
|
1954
|
+
"publishable-key",
|
|
1955
|
+
"product-id",
|
|
1956
|
+
"success-url",
|
|
1957
|
+
"cancel-url",
|
|
1958
|
+
"customer-email",
|
|
1959
|
+
"mode",
|
|
1960
|
+
"button-text",
|
|
1961
|
+
"button-style",
|
|
1962
|
+
"disabled"
|
|
1963
|
+
];
|
|
1964
|
+
}
|
|
1965
|
+
connectedCallback() {
|
|
1966
|
+
this.render();
|
|
1967
|
+
this.setupEventListeners();
|
|
1968
|
+
}
|
|
1969
|
+
disconnectedCallback() {
|
|
1970
|
+
const button = this.shadowRoot?.querySelector("button");
|
|
1971
|
+
if (button) {
|
|
1972
|
+
button.removeEventListener("click", this.handleClick);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
1976
|
+
if (oldValue !== newValue) {
|
|
1977
|
+
this.render();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
render() {
|
|
1981
|
+
const buttonText = this.getAttribute("button-text") || this.textContent?.trim() || "\u8A02\u95B1";
|
|
1982
|
+
const buttonStyle = this.getAttribute("button-style") || "primary";
|
|
1983
|
+
const isDisabled = this.hasAttribute("disabled") || this._isLoading;
|
|
1984
|
+
this.shadowRoot.innerHTML = `
|
|
1985
|
+
<style>
|
|
1986
|
+
:host {
|
|
1987
|
+
display: inline-block;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
button {
|
|
1991
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1992
|
+
font-size: 16px;
|
|
1993
|
+
font-weight: 500;
|
|
1994
|
+
padding: 12px 24px;
|
|
1995
|
+
border-radius: 8px;
|
|
1996
|
+
cursor: pointer;
|
|
1997
|
+
transition: all 0.2s ease;
|
|
1998
|
+
display: inline-flex;
|
|
1999
|
+
align-items: center;
|
|
2000
|
+
justify-content: center;
|
|
2001
|
+
gap: 8px;
|
|
2002
|
+
min-width: 120px;
|
|
2003
|
+
border: none;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
button:disabled {
|
|
2007
|
+
opacity: 0.6;
|
|
2008
|
+
cursor: not-allowed;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/* Primary style (default) */
|
|
2012
|
+
button.primary {
|
|
2013
|
+
background: #18181b;
|
|
2014
|
+
color: #ffffff;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
button.primary:hover:not(:disabled) {
|
|
2018
|
+
background: #27272a;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
button.primary:active:not(:disabled) {
|
|
2022
|
+
background: #3f3f46;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/* Outline style */
|
|
2026
|
+
button.outline {
|
|
2027
|
+
background: transparent;
|
|
2028
|
+
color: #18181b;
|
|
2029
|
+
border: 2px solid #18181b;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
button.outline:hover:not(:disabled) {
|
|
2033
|
+
background: #f4f4f5;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
button.outline:active:not(:disabled) {
|
|
2037
|
+
background: #e4e4e7;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/* Gradient style */
|
|
2041
|
+
button.gradient {
|
|
2042
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2043
|
+
color: #ffffff;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
button.gradient:hover:not(:disabled) {
|
|
2047
|
+
opacity: 0.9;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
button.gradient:active:not(:disabled) {
|
|
2051
|
+
opacity: 0.8;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* Loading spinner */
|
|
2055
|
+
.spinner {
|
|
2056
|
+
width: 16px;
|
|
2057
|
+
height: 16px;
|
|
2058
|
+
border: 2px solid currentColor;
|
|
2059
|
+
border-top-color: transparent;
|
|
2060
|
+
border-radius: 50%;
|
|
2061
|
+
animation: spin 0.6s linear infinite;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
@keyframes spin {
|
|
2065
|
+
to { transform: rotate(360deg); }
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/* Focus state */
|
|
2069
|
+
button:focus-visible {
|
|
2070
|
+
outline: 2px solid #3b82f6;
|
|
2071
|
+
outline-offset: 2px;
|
|
2072
|
+
}
|
|
2073
|
+
</style>
|
|
2074
|
+
|
|
2075
|
+
<button
|
|
2076
|
+
class="${buttonStyle}"
|
|
2077
|
+
${isDisabled ? "disabled" : ""}
|
|
2078
|
+
aria-busy="${this._isLoading}"
|
|
2079
|
+
>
|
|
2080
|
+
${this._isLoading ? '<span class="spinner"></span>' : ""}
|
|
2081
|
+
<span>${this._isLoading ? "\u8655\u7406\u4E2D..." : buttonText}</span>
|
|
2082
|
+
</button>
|
|
2083
|
+
`;
|
|
2084
|
+
}
|
|
2085
|
+
setupEventListeners() {
|
|
2086
|
+
const button = this.shadowRoot?.querySelector("button");
|
|
2087
|
+
if (button) {
|
|
2088
|
+
button.addEventListener("click", this.handleClick);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async createCheckoutSession(options) {
|
|
2092
|
+
const baseUrl = this.getApiBaseUrl();
|
|
2093
|
+
const response = await fetch(`${baseUrl}/v1/checkout/sessions`, {
|
|
2094
|
+
method: "POST",
|
|
2095
|
+
headers: {
|
|
2096
|
+
"Content-Type": "application/json",
|
|
2097
|
+
"X-Recur-Publishable-Key": options.publishableKey
|
|
2098
|
+
},
|
|
2099
|
+
body: JSON.stringify({
|
|
2100
|
+
productId: options.productId,
|
|
2101
|
+
mode: options.mode,
|
|
2102
|
+
successUrl: options.successUrl,
|
|
2103
|
+
cancelUrl: options.cancelUrl,
|
|
2104
|
+
customerEmail: options.customerEmail
|
|
2105
|
+
})
|
|
2106
|
+
});
|
|
2107
|
+
if (!response.ok) {
|
|
2108
|
+
const error = await response.json().catch(() => ({}));
|
|
2109
|
+
throw new Error(error.error?.message || `HTTP ${response.status}: Failed to create checkout session`);
|
|
2110
|
+
}
|
|
2111
|
+
return response.json();
|
|
2112
|
+
}
|
|
2113
|
+
getApiBaseUrl() {
|
|
2114
|
+
const customBaseUrl = this.getAttribute("api-base-url");
|
|
2115
|
+
if (customBaseUrl) {
|
|
2116
|
+
return customBaseUrl;
|
|
2117
|
+
}
|
|
2118
|
+
if (typeof window !== "undefined") {
|
|
2119
|
+
const hostname = window.location.hostname;
|
|
2120
|
+
if (hostname === "localhost" || hostname.includes(".test") || hostname.includes(".local") || hostname === "127.0.0.1") {
|
|
2121
|
+
return `${window.location.protocol}//${window.location.host}/api`;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return "https://api.recur.tw";
|
|
2125
|
+
}
|
|
2126
|
+
resolveUrl(url) {
|
|
2127
|
+
if (url.startsWith("/")) {
|
|
2128
|
+
return `${window.location.origin}${url}`;
|
|
2129
|
+
}
|
|
2130
|
+
return url;
|
|
2131
|
+
}
|
|
2132
|
+
dispatchError(message) {
|
|
2133
|
+
console.error("[recur-checkout]", message);
|
|
2134
|
+
this.dispatchEvent(new CustomEvent("checkout-error", {
|
|
2135
|
+
detail: { message },
|
|
2136
|
+
bubbles: true,
|
|
2137
|
+
composed: true
|
|
2138
|
+
}));
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
if (typeof window !== "undefined" && !customElements.get("recur-checkout")) {
|
|
2142
|
+
customElements.define("recur-checkout", RecurCheckoutButton);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
|
|
1888
2147
|
// src/components/index.ts
|
|
1889
2148
|
async function registerComponents() {
|
|
1890
2149
|
if (typeof window === "undefined") {
|
|
@@ -1898,7 +2157,8 @@ async function registerComponents() {
|
|
|
1898
2157
|
Promise.resolve().then(() => (init_skeleton_loader(), skeleton_loader_exports)),
|
|
1899
2158
|
Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
|
|
1900
2159
|
Promise.resolve().then(() => (init_toast(), toast_exports)),
|
|
1901
|
-
Promise.resolve().then(() => (init_payment_form(), payment_form_exports))
|
|
2160
|
+
Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
|
|
2161
|
+
Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports))
|
|
1902
2162
|
]);
|
|
1903
2163
|
const components = [
|
|
1904
2164
|
"recur-loading-spinner",
|
|
@@ -1908,7 +2168,8 @@ async function registerComponents() {
|
|
|
1908
2168
|
"recur-payment-form-skeleton",
|
|
1909
2169
|
"recur-toast",
|
|
1910
2170
|
"recur-toast-container",
|
|
1911
|
-
"recur-payment-form"
|
|
2171
|
+
"recur-payment-form",
|
|
2172
|
+
"recur-checkout"
|
|
1912
2173
|
];
|
|
1913
2174
|
const unregistered = components.filter((name) => !customElements.get(name));
|
|
1914
2175
|
if (unregistered.length > 0) {
|