recur-tw 0.13.3 → 0.15.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/dist/index.cjs +272 -4
- package/dist/index.d.cts +215 -3
- package/dist/index.d.ts +215 -3
- package/dist/index.js +273 -7
- package/dist/recur.umd.js +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.js +1 -1
- package/package.json +9 -2
package/dist/index.cjs
CHANGED
|
@@ -2814,7 +2814,7 @@ function toCamelCase(obj) {
|
|
|
2814
2814
|
|
|
2815
2815
|
// package.json
|
|
2816
2816
|
var package_default = {
|
|
2817
|
-
version: "0.
|
|
2817
|
+
version: "0.15.0"};
|
|
2818
2818
|
var SDK_VERSION = package_default.version;
|
|
2819
2819
|
var SDK_TYPE = "react";
|
|
2820
2820
|
var RecurContext = React.createContext(null);
|
|
@@ -3611,7 +3611,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
|
|
|
3611
3611
|
const fetchPlans = React.useCallback(
|
|
3612
3612
|
async () => {
|
|
3613
3613
|
const result = await fetchProducts({ type: "SUBSCRIPTION" });
|
|
3614
|
-
return { plans: result.
|
|
3614
|
+
return { plans: result.data };
|
|
3615
3615
|
},
|
|
3616
3616
|
[fetchProducts]
|
|
3617
3617
|
);
|
|
@@ -3679,8 +3679,8 @@ function useProducts(options = {}) {
|
|
|
3679
3679
|
setError(null);
|
|
3680
3680
|
try {
|
|
3681
3681
|
const result = await fetchProducts({ type });
|
|
3682
|
-
setData(result.
|
|
3683
|
-
onSuccess?.(result.
|
|
3682
|
+
setData(result.data);
|
|
3683
|
+
onSuccess?.(result.data);
|
|
3684
3684
|
} catch (err) {
|
|
3685
3685
|
const error2 = err instanceof Error ? err : new Error("Failed to fetch products");
|
|
3686
3686
|
setError(error2);
|
|
@@ -3748,6 +3748,272 @@ function useSubscribe(options = {}) {
|
|
|
3748
3748
|
reset
|
|
3749
3749
|
};
|
|
3750
3750
|
}
|
|
3751
|
+
var DEFAULT_BASE_URL = "https://api.recur.tw";
|
|
3752
|
+
var SDK_TYPE2 = "react";
|
|
3753
|
+
var SDK_VERSION2 = package_default.version;
|
|
3754
|
+
function usePromoCode(options = {}) {
|
|
3755
|
+
const context = React.useContext(RecurContext);
|
|
3756
|
+
const publishableKey = options.publishableKey || context?.config?.publishableKey;
|
|
3757
|
+
const baseUrl = options.baseUrl || context?.config?.baseUrl || DEFAULT_BASE_URL;
|
|
3758
|
+
if (!publishableKey) {
|
|
3759
|
+
throw new Error(
|
|
3760
|
+
'usePromoCode requires a publishableKey. Either wrap your app in <RecurProvider> or pass publishableKey directly: usePromoCode({ publishableKey: "pk_..." })'
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
const [status, setStatus] = React.useState("idle");
|
|
3764
|
+
const [code, setCode] = React.useState(null);
|
|
3765
|
+
const [discount, setDiscount] = React.useState(null);
|
|
3766
|
+
const [appliesToAllProducts, setAppliesToAllProducts] = React.useState(false);
|
|
3767
|
+
const [applicableProducts, setApplicableProducts] = React.useState([]);
|
|
3768
|
+
const [error, setError] = React.useState(null);
|
|
3769
|
+
const abortControllerRef = React.useRef(null);
|
|
3770
|
+
const apply = React.useCallback(
|
|
3771
|
+
async (inputCode) => {
|
|
3772
|
+
const trimmed = inputCode.trim();
|
|
3773
|
+
if (!trimmed) {
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
abortControllerRef.current?.abort();
|
|
3777
|
+
const controller = new AbortController();
|
|
3778
|
+
abortControllerRef.current = controller;
|
|
3779
|
+
setStatus("validating");
|
|
3780
|
+
setError(null);
|
|
3781
|
+
setDiscount(null);
|
|
3782
|
+
setCode(null);
|
|
3783
|
+
setAppliesToAllProducts(false);
|
|
3784
|
+
setApplicableProducts([]);
|
|
3785
|
+
try {
|
|
3786
|
+
const body = { code: trimmed };
|
|
3787
|
+
if (options.customerId) {
|
|
3788
|
+
body.external_id = options.customerId;
|
|
3789
|
+
}
|
|
3790
|
+
if (options.recurCustomerId) {
|
|
3791
|
+
body.customer_id = options.recurCustomerId;
|
|
3792
|
+
}
|
|
3793
|
+
const response = await fetch(`${baseUrl}/v1/promotion-codes/validate`, {
|
|
3794
|
+
method: "POST",
|
|
3795
|
+
headers: {
|
|
3796
|
+
"Content-Type": "application/json",
|
|
3797
|
+
"X-Recur-Publishable-Key": publishableKey,
|
|
3798
|
+
"X-Recur-SDK-Type": SDK_TYPE2,
|
|
3799
|
+
"X-Recur-SDK-Version": SDK_VERSION2,
|
|
3800
|
+
"X-Recur-Source": typeof window !== "undefined" ? window.location.origin : "server"
|
|
3801
|
+
},
|
|
3802
|
+
body: JSON.stringify(body),
|
|
3803
|
+
signal: controller.signal
|
|
3804
|
+
});
|
|
3805
|
+
if (controller.signal.aborted) return;
|
|
3806
|
+
if (!response.ok) {
|
|
3807
|
+
const errorData = await response.json().catch(() => ({}));
|
|
3808
|
+
const message = errorData?.error?.message || "\u9A57\u8B49\u5931\u6557\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66";
|
|
3809
|
+
setStatus("invalid");
|
|
3810
|
+
setError(message);
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
const raw = await response.json();
|
|
3814
|
+
const result = toCamelCase(raw);
|
|
3815
|
+
if (controller.signal.aborted) return;
|
|
3816
|
+
if (result.valid) {
|
|
3817
|
+
setStatus("valid");
|
|
3818
|
+
setCode(trimmed.toUpperCase());
|
|
3819
|
+
setDiscount(result.discount || null);
|
|
3820
|
+
setAppliesToAllProducts(result.appliesToAllProducts ?? false);
|
|
3821
|
+
setApplicableProducts(result.applicableProducts ?? []);
|
|
3822
|
+
} else {
|
|
3823
|
+
const errorStatus = result.errorCode === "PROMO_CODE_ALREADY_USED" ? "already_used" : "invalid";
|
|
3824
|
+
setStatus(errorStatus);
|
|
3825
|
+
setError(result.errorMessage || "\u6B64\u512A\u60E0\u78BC\u7121\u6548");
|
|
3826
|
+
}
|
|
3827
|
+
} catch (err) {
|
|
3828
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
3829
|
+
setStatus("invalid");
|
|
3830
|
+
setError("\u7DB2\u8DEF\u932F\u8AA4\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66");
|
|
3831
|
+
}
|
|
3832
|
+
},
|
|
3833
|
+
[publishableKey, baseUrl, options.customerId, options.recurCustomerId]
|
|
3834
|
+
);
|
|
3835
|
+
const clear = React.useCallback(() => {
|
|
3836
|
+
abortControllerRef.current?.abort();
|
|
3837
|
+
setStatus("idle");
|
|
3838
|
+
setCode(null);
|
|
3839
|
+
setDiscount(null);
|
|
3840
|
+
setAppliesToAllProducts(false);
|
|
3841
|
+
setApplicableProducts([]);
|
|
3842
|
+
setError(null);
|
|
3843
|
+
}, []);
|
|
3844
|
+
return {
|
|
3845
|
+
apply,
|
|
3846
|
+
clear,
|
|
3847
|
+
status,
|
|
3848
|
+
isLoading: status === "validating",
|
|
3849
|
+
isValid: status === "valid",
|
|
3850
|
+
code,
|
|
3851
|
+
discount,
|
|
3852
|
+
appliesToAllProducts,
|
|
3853
|
+
applicableProducts,
|
|
3854
|
+
error
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
var defaultStyles = {
|
|
3858
|
+
container: {
|
|
3859
|
+
display: "flex",
|
|
3860
|
+
flexDirection: "column",
|
|
3861
|
+
gap: "6px",
|
|
3862
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3863
|
+
fontSize: "14px"
|
|
3864
|
+
},
|
|
3865
|
+
row: {
|
|
3866
|
+
display: "flex",
|
|
3867
|
+
gap: "8px",
|
|
3868
|
+
alignItems: "center"
|
|
3869
|
+
},
|
|
3870
|
+
input: {
|
|
3871
|
+
flex: 1,
|
|
3872
|
+
padding: "8px 12px",
|
|
3873
|
+
border: "1px solid #d1d5db",
|
|
3874
|
+
borderRadius: "6px",
|
|
3875
|
+
fontSize: "14px",
|
|
3876
|
+
fontFamily: "inherit",
|
|
3877
|
+
outline: "none",
|
|
3878
|
+
textTransform: "uppercase",
|
|
3879
|
+
letterSpacing: "0.05em"
|
|
3880
|
+
},
|
|
3881
|
+
button: {
|
|
3882
|
+
padding: "8px 16px",
|
|
3883
|
+
border: "1px solid #d1d5db",
|
|
3884
|
+
borderRadius: "6px",
|
|
3885
|
+
fontSize: "14px",
|
|
3886
|
+
fontFamily: "inherit",
|
|
3887
|
+
cursor: "pointer",
|
|
3888
|
+
backgroundColor: "#fff",
|
|
3889
|
+
whiteSpace: "nowrap"
|
|
3890
|
+
},
|
|
3891
|
+
buttonDisabled: {
|
|
3892
|
+
opacity: 0.5,
|
|
3893
|
+
cursor: "not-allowed"
|
|
3894
|
+
},
|
|
3895
|
+
messageSuccess: {
|
|
3896
|
+
color: "#16a34a",
|
|
3897
|
+
fontSize: "13px",
|
|
3898
|
+
margin: 0
|
|
3899
|
+
},
|
|
3900
|
+
messageError: {
|
|
3901
|
+
color: "#dc2626",
|
|
3902
|
+
fontSize: "13px",
|
|
3903
|
+
margin: 0
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
function PromoCodeInput({
|
|
3907
|
+
promo,
|
|
3908
|
+
placeholder = "\u512A\u60E0\u78BC",
|
|
3909
|
+
applyText = "\u5957\u7528",
|
|
3910
|
+
clearText = "\u79FB\u9664",
|
|
3911
|
+
disabled = false,
|
|
3912
|
+
className,
|
|
3913
|
+
style: containerStyle,
|
|
3914
|
+
inputClassName,
|
|
3915
|
+
inputStyle,
|
|
3916
|
+
buttonClassName,
|
|
3917
|
+
buttonStyle,
|
|
3918
|
+
messageClassName,
|
|
3919
|
+
messageStyle,
|
|
3920
|
+
unstyled = false
|
|
3921
|
+
}) {
|
|
3922
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
3923
|
+
const handleApply = React.useCallback(() => {
|
|
3924
|
+
if (inputValue.trim()) {
|
|
3925
|
+
promo.apply(inputValue.trim());
|
|
3926
|
+
}
|
|
3927
|
+
}, [inputValue, promo]);
|
|
3928
|
+
const handleClear = React.useCallback(() => {
|
|
3929
|
+
setInputValue("");
|
|
3930
|
+
promo.clear();
|
|
3931
|
+
}, [promo]);
|
|
3932
|
+
const handleKeyDown = React.useCallback(
|
|
3933
|
+
(e) => {
|
|
3934
|
+
if (e.key === "Enter") {
|
|
3935
|
+
e.preventDefault();
|
|
3936
|
+
handleApply();
|
|
3937
|
+
}
|
|
3938
|
+
},
|
|
3939
|
+
[handleApply]
|
|
3940
|
+
);
|
|
3941
|
+
const isApplied = promo.isValid;
|
|
3942
|
+
const isDisabled = disabled || promo.isLoading;
|
|
3943
|
+
const s = unstyled ? null : defaultStyles;
|
|
3944
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3945
|
+
"div",
|
|
3946
|
+
{
|
|
3947
|
+
className,
|
|
3948
|
+
style: s ? { ...s.container, ...containerStyle } : containerStyle,
|
|
3949
|
+
children: [
|
|
3950
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: s?.row, children: [
|
|
3951
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3952
|
+
"input",
|
|
3953
|
+
{
|
|
3954
|
+
type: "text",
|
|
3955
|
+
value: isApplied ? promo.code || "" : inputValue,
|
|
3956
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
3957
|
+
onKeyDown: handleKeyDown,
|
|
3958
|
+
placeholder,
|
|
3959
|
+
disabled: isDisabled || isApplied,
|
|
3960
|
+
className: inputClassName,
|
|
3961
|
+
style: s ? { ...s.input, ...inputStyle } : inputStyle,
|
|
3962
|
+
"aria-label": placeholder
|
|
3963
|
+
}
|
|
3964
|
+
),
|
|
3965
|
+
isApplied ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3966
|
+
"button",
|
|
3967
|
+
{
|
|
3968
|
+
type: "button",
|
|
3969
|
+
onClick: handleClear,
|
|
3970
|
+
disabled,
|
|
3971
|
+
className: buttonClassName,
|
|
3972
|
+
style: s ? { ...s.button, ...buttonStyle } : buttonStyle,
|
|
3973
|
+
children: clearText
|
|
3974
|
+
}
|
|
3975
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3976
|
+
"button",
|
|
3977
|
+
{
|
|
3978
|
+
type: "button",
|
|
3979
|
+
onClick: handleApply,
|
|
3980
|
+
disabled: isDisabled || !inputValue.trim(),
|
|
3981
|
+
className: buttonClassName,
|
|
3982
|
+
style: s ? {
|
|
3983
|
+
...s.button,
|
|
3984
|
+
...isDisabled || !inputValue.trim() ? s.buttonDisabled : {},
|
|
3985
|
+
...buttonStyle
|
|
3986
|
+
} : buttonStyle,
|
|
3987
|
+
children: promo.isLoading ? "\u9A57\u8B49\u4E2D..." : applyText
|
|
3988
|
+
}
|
|
3989
|
+
)
|
|
3990
|
+
] }),
|
|
3991
|
+
promo.isValid && promo.discount && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3992
|
+
"p",
|
|
3993
|
+
{
|
|
3994
|
+
className: messageClassName,
|
|
3995
|
+
style: s ? { ...s.messageSuccess, ...messageStyle } : messageStyle,
|
|
3996
|
+
role: "status",
|
|
3997
|
+
children: [
|
|
3998
|
+
promo.discount.label,
|
|
3999
|
+
promo.discount.bonusTrialDays && promo.discount.bonusTrialDays > 0 ? ` + ${promo.discount.bonusTrialDays} \u5929\u514D\u8CBB\u8A66\u7528` : "",
|
|
4000
|
+
promo.discount.bonusMonths && promo.discount.bonusMonths > 0 ? ` + \u8D08\u9001 ${promo.discount.bonusMonths} \u500B\u6708` : ""
|
|
4001
|
+
]
|
|
4002
|
+
}
|
|
4003
|
+
),
|
|
4004
|
+
promo.error && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4005
|
+
"p",
|
|
4006
|
+
{
|
|
4007
|
+
className: messageClassName,
|
|
4008
|
+
style: s ? { ...s.messageError, ...messageStyle } : messageStyle,
|
|
4009
|
+
role: "alert",
|
|
4010
|
+
children: promo.error
|
|
4011
|
+
}
|
|
4012
|
+
)
|
|
4013
|
+
]
|
|
4014
|
+
}
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
3751
4017
|
function useCustomer() {
|
|
3752
4018
|
const context = React.useContext(CustomerContext);
|
|
3753
4019
|
if (!context) {
|
|
@@ -3824,9 +4090,11 @@ function useCustomer() {
|
|
|
3824
4090
|
};
|
|
3825
4091
|
}
|
|
3826
4092
|
|
|
4093
|
+
exports.PromoCodeInput = PromoCodeInput;
|
|
3827
4094
|
exports.RecurProvider = RecurProvider;
|
|
3828
4095
|
exports.useCustomer = useCustomer;
|
|
3829
4096
|
exports.usePlans = useProducts;
|
|
3830
4097
|
exports.useProducts = useProducts;
|
|
4098
|
+
exports.usePromoCode = usePromoCode;
|
|
3831
4099
|
exports.useRecur = useRecur;
|
|
3832
4100
|
exports.useSubscribe = useSubscribe;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { CSSProperties } from 'react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Recur Loading Spinner Web Component
|
|
@@ -517,7 +517,7 @@ interface Product {
|
|
|
517
517
|
displayOrder: number;
|
|
518
518
|
}
|
|
519
519
|
interface ProductsResult {
|
|
520
|
-
|
|
520
|
+
data: Product[];
|
|
521
521
|
}
|
|
522
522
|
type Plan = Product;
|
|
523
523
|
interface PlansResult {
|
|
@@ -1313,6 +1313,218 @@ interface UseSubscribeResult {
|
|
|
1313
1313
|
*/
|
|
1314
1314
|
declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult;
|
|
1315
1315
|
|
|
1316
|
+
interface UsePromoCodeOptions {
|
|
1317
|
+
/**
|
|
1318
|
+
* Customer identifier from your system (external ID).
|
|
1319
|
+
* Required for publishable key validation — the API rejects requests without it.
|
|
1320
|
+
*/
|
|
1321
|
+
customerId?: string;
|
|
1322
|
+
/**
|
|
1323
|
+
* Recur internal customer ID (alternative to customerId).
|
|
1324
|
+
* Use this if you already have the Recur customer ID.
|
|
1325
|
+
*/
|
|
1326
|
+
recurCustomerId?: string;
|
|
1327
|
+
/**
|
|
1328
|
+
* Publishable API key. If omitted, falls back to RecurProvider context.
|
|
1329
|
+
* This allows standalone usage without wrapping in RecurProvider.
|
|
1330
|
+
*
|
|
1331
|
+
* @example 'pk_test_abc123...'
|
|
1332
|
+
*/
|
|
1333
|
+
publishableKey?: string;
|
|
1334
|
+
/**
|
|
1335
|
+
* Base URL for API calls. Defaults to 'https://api.recur.tw'.
|
|
1336
|
+
*/
|
|
1337
|
+
baseUrl?: string;
|
|
1338
|
+
}
|
|
1339
|
+
type PromoCodeStatus = 'idle' | 'validating' | 'valid' | 'invalid' | 'already_used';
|
|
1340
|
+
interface PromoCodeDiscount {
|
|
1341
|
+
/** Discount type */
|
|
1342
|
+
type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FIRST_PERIOD_PRICE';
|
|
1343
|
+
/** Raw discount value (basis points for %, TWD for fixed) */
|
|
1344
|
+
amount: number;
|
|
1345
|
+
/** Final amount after discount (0 if no amount was provided) */
|
|
1346
|
+
discountedAmount: number;
|
|
1347
|
+
/** Human-readable label (e.g., "20% off", "NT$100 off") */
|
|
1348
|
+
label: string;
|
|
1349
|
+
/** Bonus trial days */
|
|
1350
|
+
bonusTrialDays?: number;
|
|
1351
|
+
/** Bonus months */
|
|
1352
|
+
bonusMonths?: number;
|
|
1353
|
+
}
|
|
1354
|
+
interface ApplicableProduct {
|
|
1355
|
+
id: string;
|
|
1356
|
+
name: string;
|
|
1357
|
+
price: number;
|
|
1358
|
+
priceAfterDiscount: number;
|
|
1359
|
+
interval?: string | null;
|
|
1360
|
+
intervalCount?: number | null;
|
|
1361
|
+
}
|
|
1362
|
+
interface UsePromoCodeReturn {
|
|
1363
|
+
/**
|
|
1364
|
+
* Validate and apply a promo code.
|
|
1365
|
+
* Call this on button click or input blur.
|
|
1366
|
+
*/
|
|
1367
|
+
apply: (code: string) => Promise<void>;
|
|
1368
|
+
/**
|
|
1369
|
+
* Clear the current promo code state.
|
|
1370
|
+
*/
|
|
1371
|
+
clear: () => void;
|
|
1372
|
+
/** Current status */
|
|
1373
|
+
status: PromoCodeStatus;
|
|
1374
|
+
/** Whether a validation request is in progress */
|
|
1375
|
+
isLoading: boolean;
|
|
1376
|
+
/** Whether the current code is valid */
|
|
1377
|
+
isValid: boolean;
|
|
1378
|
+
/**
|
|
1379
|
+
* The validated promo code string.
|
|
1380
|
+
* Pass this directly to subscribe() — null if no valid code.
|
|
1381
|
+
*/
|
|
1382
|
+
code: string | null;
|
|
1383
|
+
/** Discount details (when valid) */
|
|
1384
|
+
discount: PromoCodeDiscount | null;
|
|
1385
|
+
/** Whether the discount applies to all products */
|
|
1386
|
+
appliesToAllProducts: boolean;
|
|
1387
|
+
/** Applicable products with discounted prices (when restricted to specific products) */
|
|
1388
|
+
applicableProducts: ApplicableProduct[];
|
|
1389
|
+
/** Error message (when invalid) */
|
|
1390
|
+
error: string | null;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* usePromoCode Hook
|
|
1394
|
+
*
|
|
1395
|
+
* Validate promotion codes and get discount information.
|
|
1396
|
+
* Works with or without RecurProvider.
|
|
1397
|
+
*
|
|
1398
|
+
* @example With RecurProvider (publishableKey from context)
|
|
1399
|
+
* ```tsx
|
|
1400
|
+
* <RecurProvider config={{ publishableKey: 'pk_test_xxx' }}>
|
|
1401
|
+
* <PricingPage />
|
|
1402
|
+
* </RecurProvider>
|
|
1403
|
+
*
|
|
1404
|
+
* function PricingPage() {
|
|
1405
|
+
* const promo = usePromoCode({ customerId: 'user_123' })
|
|
1406
|
+
*
|
|
1407
|
+
* return (
|
|
1408
|
+
* <div>
|
|
1409
|
+
* <input onBlur={(e) => promo.apply(e.target.value)} />
|
|
1410
|
+
* {promo.isValid && <span>{promo.discount.label}</span>}
|
|
1411
|
+
* {promo.error && <span>{promo.error}</span>}
|
|
1412
|
+
* </div>
|
|
1413
|
+
* )
|
|
1414
|
+
* }
|
|
1415
|
+
* ```
|
|
1416
|
+
*
|
|
1417
|
+
* @example Standalone (no RecurProvider needed)
|
|
1418
|
+
* ```tsx
|
|
1419
|
+
* function PricingPage() {
|
|
1420
|
+
* const promo = usePromoCode({
|
|
1421
|
+
* publishableKey: 'pk_test_xxx',
|
|
1422
|
+
* customerId: 'user_123',
|
|
1423
|
+
* })
|
|
1424
|
+
* // ... same usage
|
|
1425
|
+
* }
|
|
1426
|
+
* ```
|
|
1427
|
+
*/
|
|
1428
|
+
declare function usePromoCode(options?: UsePromoCodeOptions): UsePromoCodeReturn;
|
|
1429
|
+
|
|
1430
|
+
interface PromoCodeInputProps {
|
|
1431
|
+
/**
|
|
1432
|
+
* Return value from usePromoCode().
|
|
1433
|
+
* The component reads state and calls apply/clear from this object.
|
|
1434
|
+
*/
|
|
1435
|
+
promo: UsePromoCodeReturn;
|
|
1436
|
+
/**
|
|
1437
|
+
* Input placeholder text
|
|
1438
|
+
* @default "優惠碼"
|
|
1439
|
+
*/
|
|
1440
|
+
placeholder?: string;
|
|
1441
|
+
/**
|
|
1442
|
+
* Apply button text
|
|
1443
|
+
* @default "套用"
|
|
1444
|
+
*/
|
|
1445
|
+
applyText?: string;
|
|
1446
|
+
/**
|
|
1447
|
+
* Clear button text (shown when a code is applied)
|
|
1448
|
+
* @default "移除"
|
|
1449
|
+
*/
|
|
1450
|
+
clearText?: string;
|
|
1451
|
+
/**
|
|
1452
|
+
* Disable the input and button
|
|
1453
|
+
*/
|
|
1454
|
+
disabled?: boolean;
|
|
1455
|
+
/**
|
|
1456
|
+
* CSS class for the outer container
|
|
1457
|
+
*/
|
|
1458
|
+
className?: string;
|
|
1459
|
+
/**
|
|
1460
|
+
* Inline styles for the outer container
|
|
1461
|
+
*/
|
|
1462
|
+
style?: CSSProperties;
|
|
1463
|
+
/**
|
|
1464
|
+
* CSS class for the input element
|
|
1465
|
+
*/
|
|
1466
|
+
inputClassName?: string;
|
|
1467
|
+
/**
|
|
1468
|
+
* Inline styles for the input element
|
|
1469
|
+
*/
|
|
1470
|
+
inputStyle?: CSSProperties;
|
|
1471
|
+
/**
|
|
1472
|
+
* CSS class for the apply/clear button
|
|
1473
|
+
*/
|
|
1474
|
+
buttonClassName?: string;
|
|
1475
|
+
/**
|
|
1476
|
+
* Inline styles for the apply/clear button
|
|
1477
|
+
*/
|
|
1478
|
+
buttonStyle?: CSSProperties;
|
|
1479
|
+
/**
|
|
1480
|
+
* CSS class for the status message (success/error)
|
|
1481
|
+
*/
|
|
1482
|
+
messageClassName?: string;
|
|
1483
|
+
/**
|
|
1484
|
+
* Inline styles for the status message
|
|
1485
|
+
*/
|
|
1486
|
+
messageStyle?: CSSProperties;
|
|
1487
|
+
/**
|
|
1488
|
+
* When true, renders without any default inline styles.
|
|
1489
|
+
* Use this when you want full control via className and style props.
|
|
1490
|
+
* @default false
|
|
1491
|
+
*/
|
|
1492
|
+
unstyled?: boolean;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* PromoCodeInput
|
|
1496
|
+
*
|
|
1497
|
+
* A drop-in promo code input component that works with usePromoCode().
|
|
1498
|
+
*
|
|
1499
|
+
* @example Minimal usage
|
|
1500
|
+
* ```tsx
|
|
1501
|
+
* const promo = usePromoCode({ customerId: 'user_123' });
|
|
1502
|
+
* <PromoCodeInput promo={promo} />
|
|
1503
|
+
* ```
|
|
1504
|
+
*
|
|
1505
|
+
* @example Custom styling with className (Tailwind)
|
|
1506
|
+
* ```tsx
|
|
1507
|
+
* <PromoCodeInput
|
|
1508
|
+
* promo={promo}
|
|
1509
|
+
* unstyled
|
|
1510
|
+
* className="flex flex-col gap-1"
|
|
1511
|
+
* inputClassName="border rounded px-3 py-2 uppercase"
|
|
1512
|
+
* buttonClassName="bg-blue-500 text-white px-4 py-2 rounded"
|
|
1513
|
+
* messageClassName="text-sm"
|
|
1514
|
+
* />
|
|
1515
|
+
* ```
|
|
1516
|
+
*
|
|
1517
|
+
* @example Custom styling with style props
|
|
1518
|
+
* ```tsx
|
|
1519
|
+
* <PromoCodeInput
|
|
1520
|
+
* promo={promo}
|
|
1521
|
+
* inputStyle={{ border: '2px solid #3b82f6', borderRadius: 8 }}
|
|
1522
|
+
* buttonStyle={{ backgroundColor: '#3b82f6', color: 'white' }}
|
|
1523
|
+
* />
|
|
1524
|
+
* ```
|
|
1525
|
+
*/
|
|
1526
|
+
declare function PromoCodeInput({ promo, placeholder, applyText, clearText, disabled, className, style: containerStyle, inputClassName, inputStyle, buttonClassName, buttonStyle, messageClassName, messageStyle, unstyled, }: PromoCodeInputProps): react_jsx_runtime.JSX.Element;
|
|
1527
|
+
|
|
1316
1528
|
/**
|
|
1317
1529
|
* useCustomer Hook
|
|
1318
1530
|
*
|
|
@@ -1460,4 +1672,4 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
|
|
|
1460
1672
|
*/
|
|
1461
1673
|
declare function useCustomer(): UseCustomerResult;
|
|
1462
1674
|
|
|
1463
|
-
export { type ApiErrorCode, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, useRecur, useSubscribe };
|
|
1675
|
+
export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { CSSProperties } from 'react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Recur Loading Spinner Web Component
|
|
@@ -517,7 +517,7 @@ interface Product {
|
|
|
517
517
|
displayOrder: number;
|
|
518
518
|
}
|
|
519
519
|
interface ProductsResult {
|
|
520
|
-
|
|
520
|
+
data: Product[];
|
|
521
521
|
}
|
|
522
522
|
type Plan = Product;
|
|
523
523
|
interface PlansResult {
|
|
@@ -1313,6 +1313,218 @@ interface UseSubscribeResult {
|
|
|
1313
1313
|
*/
|
|
1314
1314
|
declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult;
|
|
1315
1315
|
|
|
1316
|
+
interface UsePromoCodeOptions {
|
|
1317
|
+
/**
|
|
1318
|
+
* Customer identifier from your system (external ID).
|
|
1319
|
+
* Required for publishable key validation — the API rejects requests without it.
|
|
1320
|
+
*/
|
|
1321
|
+
customerId?: string;
|
|
1322
|
+
/**
|
|
1323
|
+
* Recur internal customer ID (alternative to customerId).
|
|
1324
|
+
* Use this if you already have the Recur customer ID.
|
|
1325
|
+
*/
|
|
1326
|
+
recurCustomerId?: string;
|
|
1327
|
+
/**
|
|
1328
|
+
* Publishable API key. If omitted, falls back to RecurProvider context.
|
|
1329
|
+
* This allows standalone usage without wrapping in RecurProvider.
|
|
1330
|
+
*
|
|
1331
|
+
* @example 'pk_test_abc123...'
|
|
1332
|
+
*/
|
|
1333
|
+
publishableKey?: string;
|
|
1334
|
+
/**
|
|
1335
|
+
* Base URL for API calls. Defaults to 'https://api.recur.tw'.
|
|
1336
|
+
*/
|
|
1337
|
+
baseUrl?: string;
|
|
1338
|
+
}
|
|
1339
|
+
type PromoCodeStatus = 'idle' | 'validating' | 'valid' | 'invalid' | 'already_used';
|
|
1340
|
+
interface PromoCodeDiscount {
|
|
1341
|
+
/** Discount type */
|
|
1342
|
+
type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FIRST_PERIOD_PRICE';
|
|
1343
|
+
/** Raw discount value (basis points for %, TWD for fixed) */
|
|
1344
|
+
amount: number;
|
|
1345
|
+
/** Final amount after discount (0 if no amount was provided) */
|
|
1346
|
+
discountedAmount: number;
|
|
1347
|
+
/** Human-readable label (e.g., "20% off", "NT$100 off") */
|
|
1348
|
+
label: string;
|
|
1349
|
+
/** Bonus trial days */
|
|
1350
|
+
bonusTrialDays?: number;
|
|
1351
|
+
/** Bonus months */
|
|
1352
|
+
bonusMonths?: number;
|
|
1353
|
+
}
|
|
1354
|
+
interface ApplicableProduct {
|
|
1355
|
+
id: string;
|
|
1356
|
+
name: string;
|
|
1357
|
+
price: number;
|
|
1358
|
+
priceAfterDiscount: number;
|
|
1359
|
+
interval?: string | null;
|
|
1360
|
+
intervalCount?: number | null;
|
|
1361
|
+
}
|
|
1362
|
+
interface UsePromoCodeReturn {
|
|
1363
|
+
/**
|
|
1364
|
+
* Validate and apply a promo code.
|
|
1365
|
+
* Call this on button click or input blur.
|
|
1366
|
+
*/
|
|
1367
|
+
apply: (code: string) => Promise<void>;
|
|
1368
|
+
/**
|
|
1369
|
+
* Clear the current promo code state.
|
|
1370
|
+
*/
|
|
1371
|
+
clear: () => void;
|
|
1372
|
+
/** Current status */
|
|
1373
|
+
status: PromoCodeStatus;
|
|
1374
|
+
/** Whether a validation request is in progress */
|
|
1375
|
+
isLoading: boolean;
|
|
1376
|
+
/** Whether the current code is valid */
|
|
1377
|
+
isValid: boolean;
|
|
1378
|
+
/**
|
|
1379
|
+
* The validated promo code string.
|
|
1380
|
+
* Pass this directly to subscribe() — null if no valid code.
|
|
1381
|
+
*/
|
|
1382
|
+
code: string | null;
|
|
1383
|
+
/** Discount details (when valid) */
|
|
1384
|
+
discount: PromoCodeDiscount | null;
|
|
1385
|
+
/** Whether the discount applies to all products */
|
|
1386
|
+
appliesToAllProducts: boolean;
|
|
1387
|
+
/** Applicable products with discounted prices (when restricted to specific products) */
|
|
1388
|
+
applicableProducts: ApplicableProduct[];
|
|
1389
|
+
/** Error message (when invalid) */
|
|
1390
|
+
error: string | null;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* usePromoCode Hook
|
|
1394
|
+
*
|
|
1395
|
+
* Validate promotion codes and get discount information.
|
|
1396
|
+
* Works with or without RecurProvider.
|
|
1397
|
+
*
|
|
1398
|
+
* @example With RecurProvider (publishableKey from context)
|
|
1399
|
+
* ```tsx
|
|
1400
|
+
* <RecurProvider config={{ publishableKey: 'pk_test_xxx' }}>
|
|
1401
|
+
* <PricingPage />
|
|
1402
|
+
* </RecurProvider>
|
|
1403
|
+
*
|
|
1404
|
+
* function PricingPage() {
|
|
1405
|
+
* const promo = usePromoCode({ customerId: 'user_123' })
|
|
1406
|
+
*
|
|
1407
|
+
* return (
|
|
1408
|
+
* <div>
|
|
1409
|
+
* <input onBlur={(e) => promo.apply(e.target.value)} />
|
|
1410
|
+
* {promo.isValid && <span>{promo.discount.label}</span>}
|
|
1411
|
+
* {promo.error && <span>{promo.error}</span>}
|
|
1412
|
+
* </div>
|
|
1413
|
+
* )
|
|
1414
|
+
* }
|
|
1415
|
+
* ```
|
|
1416
|
+
*
|
|
1417
|
+
* @example Standalone (no RecurProvider needed)
|
|
1418
|
+
* ```tsx
|
|
1419
|
+
* function PricingPage() {
|
|
1420
|
+
* const promo = usePromoCode({
|
|
1421
|
+
* publishableKey: 'pk_test_xxx',
|
|
1422
|
+
* customerId: 'user_123',
|
|
1423
|
+
* })
|
|
1424
|
+
* // ... same usage
|
|
1425
|
+
* }
|
|
1426
|
+
* ```
|
|
1427
|
+
*/
|
|
1428
|
+
declare function usePromoCode(options?: UsePromoCodeOptions): UsePromoCodeReturn;
|
|
1429
|
+
|
|
1430
|
+
interface PromoCodeInputProps {
|
|
1431
|
+
/**
|
|
1432
|
+
* Return value from usePromoCode().
|
|
1433
|
+
* The component reads state and calls apply/clear from this object.
|
|
1434
|
+
*/
|
|
1435
|
+
promo: UsePromoCodeReturn;
|
|
1436
|
+
/**
|
|
1437
|
+
* Input placeholder text
|
|
1438
|
+
* @default "優惠碼"
|
|
1439
|
+
*/
|
|
1440
|
+
placeholder?: string;
|
|
1441
|
+
/**
|
|
1442
|
+
* Apply button text
|
|
1443
|
+
* @default "套用"
|
|
1444
|
+
*/
|
|
1445
|
+
applyText?: string;
|
|
1446
|
+
/**
|
|
1447
|
+
* Clear button text (shown when a code is applied)
|
|
1448
|
+
* @default "移除"
|
|
1449
|
+
*/
|
|
1450
|
+
clearText?: string;
|
|
1451
|
+
/**
|
|
1452
|
+
* Disable the input and button
|
|
1453
|
+
*/
|
|
1454
|
+
disabled?: boolean;
|
|
1455
|
+
/**
|
|
1456
|
+
* CSS class for the outer container
|
|
1457
|
+
*/
|
|
1458
|
+
className?: string;
|
|
1459
|
+
/**
|
|
1460
|
+
* Inline styles for the outer container
|
|
1461
|
+
*/
|
|
1462
|
+
style?: CSSProperties;
|
|
1463
|
+
/**
|
|
1464
|
+
* CSS class for the input element
|
|
1465
|
+
*/
|
|
1466
|
+
inputClassName?: string;
|
|
1467
|
+
/**
|
|
1468
|
+
* Inline styles for the input element
|
|
1469
|
+
*/
|
|
1470
|
+
inputStyle?: CSSProperties;
|
|
1471
|
+
/**
|
|
1472
|
+
* CSS class for the apply/clear button
|
|
1473
|
+
*/
|
|
1474
|
+
buttonClassName?: string;
|
|
1475
|
+
/**
|
|
1476
|
+
* Inline styles for the apply/clear button
|
|
1477
|
+
*/
|
|
1478
|
+
buttonStyle?: CSSProperties;
|
|
1479
|
+
/**
|
|
1480
|
+
* CSS class for the status message (success/error)
|
|
1481
|
+
*/
|
|
1482
|
+
messageClassName?: string;
|
|
1483
|
+
/**
|
|
1484
|
+
* Inline styles for the status message
|
|
1485
|
+
*/
|
|
1486
|
+
messageStyle?: CSSProperties;
|
|
1487
|
+
/**
|
|
1488
|
+
* When true, renders without any default inline styles.
|
|
1489
|
+
* Use this when you want full control via className and style props.
|
|
1490
|
+
* @default false
|
|
1491
|
+
*/
|
|
1492
|
+
unstyled?: boolean;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* PromoCodeInput
|
|
1496
|
+
*
|
|
1497
|
+
* A drop-in promo code input component that works with usePromoCode().
|
|
1498
|
+
*
|
|
1499
|
+
* @example Minimal usage
|
|
1500
|
+
* ```tsx
|
|
1501
|
+
* const promo = usePromoCode({ customerId: 'user_123' });
|
|
1502
|
+
* <PromoCodeInput promo={promo} />
|
|
1503
|
+
* ```
|
|
1504
|
+
*
|
|
1505
|
+
* @example Custom styling with className (Tailwind)
|
|
1506
|
+
* ```tsx
|
|
1507
|
+
* <PromoCodeInput
|
|
1508
|
+
* promo={promo}
|
|
1509
|
+
* unstyled
|
|
1510
|
+
* className="flex flex-col gap-1"
|
|
1511
|
+
* inputClassName="border rounded px-3 py-2 uppercase"
|
|
1512
|
+
* buttonClassName="bg-blue-500 text-white px-4 py-2 rounded"
|
|
1513
|
+
* messageClassName="text-sm"
|
|
1514
|
+
* />
|
|
1515
|
+
* ```
|
|
1516
|
+
*
|
|
1517
|
+
* @example Custom styling with style props
|
|
1518
|
+
* ```tsx
|
|
1519
|
+
* <PromoCodeInput
|
|
1520
|
+
* promo={promo}
|
|
1521
|
+
* inputStyle={{ border: '2px solid #3b82f6', borderRadius: 8 }}
|
|
1522
|
+
* buttonStyle={{ backgroundColor: '#3b82f6', color: 'white' }}
|
|
1523
|
+
* />
|
|
1524
|
+
* ```
|
|
1525
|
+
*/
|
|
1526
|
+
declare function PromoCodeInput({ promo, placeholder, applyText, clearText, disabled, className, style: containerStyle, inputClassName, inputStyle, buttonClassName, buttonStyle, messageClassName, messageStyle, unstyled, }: PromoCodeInputProps): react_jsx_runtime.JSX.Element;
|
|
1527
|
+
|
|
1316
1528
|
/**
|
|
1317
1529
|
* useCustomer Hook
|
|
1318
1530
|
*
|
|
@@ -1460,4 +1672,4 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
|
|
|
1460
1672
|
*/
|
|
1461
1673
|
declare function useCustomer(): UseCustomerResult;
|
|
1462
1674
|
|
|
1463
|
-
export { type ApiErrorCode, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, useRecur, useSubscribe };
|
|
1675
|
+
export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { html, render, nothing } from 'lit-html';
|
|
2
|
-
import React, { createContext, useState, useCallback, useMemo, useContext, useEffect } from 'react';
|
|
3
|
-
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import React, { createContext, useState, useCallback, useMemo, useContext, useEffect, useRef } from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -2808,7 +2808,7 @@ function toCamelCase(obj) {
|
|
|
2808
2808
|
|
|
2809
2809
|
// package.json
|
|
2810
2810
|
var package_default = {
|
|
2811
|
-
version: "0.
|
|
2811
|
+
version: "0.15.0"};
|
|
2812
2812
|
var SDK_VERSION = package_default.version;
|
|
2813
2813
|
var SDK_TYPE = "react";
|
|
2814
2814
|
var RecurContext = createContext(null);
|
|
@@ -3605,7 +3605,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
|
|
|
3605
3605
|
const fetchPlans = useCallback(
|
|
3606
3606
|
async () => {
|
|
3607
3607
|
const result = await fetchProducts({ type: "SUBSCRIPTION" });
|
|
3608
|
-
return { plans: result.
|
|
3608
|
+
return { plans: result.data };
|
|
3609
3609
|
},
|
|
3610
3610
|
[fetchProducts]
|
|
3611
3611
|
);
|
|
@@ -3673,8 +3673,8 @@ function useProducts(options = {}) {
|
|
|
3673
3673
|
setError(null);
|
|
3674
3674
|
try {
|
|
3675
3675
|
const result = await fetchProducts({ type });
|
|
3676
|
-
setData(result.
|
|
3677
|
-
onSuccess?.(result.
|
|
3676
|
+
setData(result.data);
|
|
3677
|
+
onSuccess?.(result.data);
|
|
3678
3678
|
} catch (err) {
|
|
3679
3679
|
const error2 = err instanceof Error ? err : new Error("Failed to fetch products");
|
|
3680
3680
|
setError(error2);
|
|
@@ -3742,6 +3742,272 @@ function useSubscribe(options = {}) {
|
|
|
3742
3742
|
reset
|
|
3743
3743
|
};
|
|
3744
3744
|
}
|
|
3745
|
+
var DEFAULT_BASE_URL = "https://api.recur.tw";
|
|
3746
|
+
var SDK_TYPE2 = "react";
|
|
3747
|
+
var SDK_VERSION2 = package_default.version;
|
|
3748
|
+
function usePromoCode(options = {}) {
|
|
3749
|
+
const context = useContext(RecurContext);
|
|
3750
|
+
const publishableKey = options.publishableKey || context?.config?.publishableKey;
|
|
3751
|
+
const baseUrl = options.baseUrl || context?.config?.baseUrl || DEFAULT_BASE_URL;
|
|
3752
|
+
if (!publishableKey) {
|
|
3753
|
+
throw new Error(
|
|
3754
|
+
'usePromoCode requires a publishableKey. Either wrap your app in <RecurProvider> or pass publishableKey directly: usePromoCode({ publishableKey: "pk_..." })'
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
const [status, setStatus] = useState("idle");
|
|
3758
|
+
const [code, setCode] = useState(null);
|
|
3759
|
+
const [discount, setDiscount] = useState(null);
|
|
3760
|
+
const [appliesToAllProducts, setAppliesToAllProducts] = useState(false);
|
|
3761
|
+
const [applicableProducts, setApplicableProducts] = useState([]);
|
|
3762
|
+
const [error, setError] = useState(null);
|
|
3763
|
+
const abortControllerRef = useRef(null);
|
|
3764
|
+
const apply = useCallback(
|
|
3765
|
+
async (inputCode) => {
|
|
3766
|
+
const trimmed = inputCode.trim();
|
|
3767
|
+
if (!trimmed) {
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
abortControllerRef.current?.abort();
|
|
3771
|
+
const controller = new AbortController();
|
|
3772
|
+
abortControllerRef.current = controller;
|
|
3773
|
+
setStatus("validating");
|
|
3774
|
+
setError(null);
|
|
3775
|
+
setDiscount(null);
|
|
3776
|
+
setCode(null);
|
|
3777
|
+
setAppliesToAllProducts(false);
|
|
3778
|
+
setApplicableProducts([]);
|
|
3779
|
+
try {
|
|
3780
|
+
const body = { code: trimmed };
|
|
3781
|
+
if (options.customerId) {
|
|
3782
|
+
body.external_id = options.customerId;
|
|
3783
|
+
}
|
|
3784
|
+
if (options.recurCustomerId) {
|
|
3785
|
+
body.customer_id = options.recurCustomerId;
|
|
3786
|
+
}
|
|
3787
|
+
const response = await fetch(`${baseUrl}/v1/promotion-codes/validate`, {
|
|
3788
|
+
method: "POST",
|
|
3789
|
+
headers: {
|
|
3790
|
+
"Content-Type": "application/json",
|
|
3791
|
+
"X-Recur-Publishable-Key": publishableKey,
|
|
3792
|
+
"X-Recur-SDK-Type": SDK_TYPE2,
|
|
3793
|
+
"X-Recur-SDK-Version": SDK_VERSION2,
|
|
3794
|
+
"X-Recur-Source": typeof window !== "undefined" ? window.location.origin : "server"
|
|
3795
|
+
},
|
|
3796
|
+
body: JSON.stringify(body),
|
|
3797
|
+
signal: controller.signal
|
|
3798
|
+
});
|
|
3799
|
+
if (controller.signal.aborted) return;
|
|
3800
|
+
if (!response.ok) {
|
|
3801
|
+
const errorData = await response.json().catch(() => ({}));
|
|
3802
|
+
const message = errorData?.error?.message || "\u9A57\u8B49\u5931\u6557\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66";
|
|
3803
|
+
setStatus("invalid");
|
|
3804
|
+
setError(message);
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
const raw = await response.json();
|
|
3808
|
+
const result = toCamelCase(raw);
|
|
3809
|
+
if (controller.signal.aborted) return;
|
|
3810
|
+
if (result.valid) {
|
|
3811
|
+
setStatus("valid");
|
|
3812
|
+
setCode(trimmed.toUpperCase());
|
|
3813
|
+
setDiscount(result.discount || null);
|
|
3814
|
+
setAppliesToAllProducts(result.appliesToAllProducts ?? false);
|
|
3815
|
+
setApplicableProducts(result.applicableProducts ?? []);
|
|
3816
|
+
} else {
|
|
3817
|
+
const errorStatus = result.errorCode === "PROMO_CODE_ALREADY_USED" ? "already_used" : "invalid";
|
|
3818
|
+
setStatus(errorStatus);
|
|
3819
|
+
setError(result.errorMessage || "\u6B64\u512A\u60E0\u78BC\u7121\u6548");
|
|
3820
|
+
}
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
3823
|
+
setStatus("invalid");
|
|
3824
|
+
setError("\u7DB2\u8DEF\u932F\u8AA4\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66");
|
|
3825
|
+
}
|
|
3826
|
+
},
|
|
3827
|
+
[publishableKey, baseUrl, options.customerId, options.recurCustomerId]
|
|
3828
|
+
);
|
|
3829
|
+
const clear = useCallback(() => {
|
|
3830
|
+
abortControllerRef.current?.abort();
|
|
3831
|
+
setStatus("idle");
|
|
3832
|
+
setCode(null);
|
|
3833
|
+
setDiscount(null);
|
|
3834
|
+
setAppliesToAllProducts(false);
|
|
3835
|
+
setApplicableProducts([]);
|
|
3836
|
+
setError(null);
|
|
3837
|
+
}, []);
|
|
3838
|
+
return {
|
|
3839
|
+
apply,
|
|
3840
|
+
clear,
|
|
3841
|
+
status,
|
|
3842
|
+
isLoading: status === "validating",
|
|
3843
|
+
isValid: status === "valid",
|
|
3844
|
+
code,
|
|
3845
|
+
discount,
|
|
3846
|
+
appliesToAllProducts,
|
|
3847
|
+
applicableProducts,
|
|
3848
|
+
error
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
var defaultStyles = {
|
|
3852
|
+
container: {
|
|
3853
|
+
display: "flex",
|
|
3854
|
+
flexDirection: "column",
|
|
3855
|
+
gap: "6px",
|
|
3856
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3857
|
+
fontSize: "14px"
|
|
3858
|
+
},
|
|
3859
|
+
row: {
|
|
3860
|
+
display: "flex",
|
|
3861
|
+
gap: "8px",
|
|
3862
|
+
alignItems: "center"
|
|
3863
|
+
},
|
|
3864
|
+
input: {
|
|
3865
|
+
flex: 1,
|
|
3866
|
+
padding: "8px 12px",
|
|
3867
|
+
border: "1px solid #d1d5db",
|
|
3868
|
+
borderRadius: "6px",
|
|
3869
|
+
fontSize: "14px",
|
|
3870
|
+
fontFamily: "inherit",
|
|
3871
|
+
outline: "none",
|
|
3872
|
+
textTransform: "uppercase",
|
|
3873
|
+
letterSpacing: "0.05em"
|
|
3874
|
+
},
|
|
3875
|
+
button: {
|
|
3876
|
+
padding: "8px 16px",
|
|
3877
|
+
border: "1px solid #d1d5db",
|
|
3878
|
+
borderRadius: "6px",
|
|
3879
|
+
fontSize: "14px",
|
|
3880
|
+
fontFamily: "inherit",
|
|
3881
|
+
cursor: "pointer",
|
|
3882
|
+
backgroundColor: "#fff",
|
|
3883
|
+
whiteSpace: "nowrap"
|
|
3884
|
+
},
|
|
3885
|
+
buttonDisabled: {
|
|
3886
|
+
opacity: 0.5,
|
|
3887
|
+
cursor: "not-allowed"
|
|
3888
|
+
},
|
|
3889
|
+
messageSuccess: {
|
|
3890
|
+
color: "#16a34a",
|
|
3891
|
+
fontSize: "13px",
|
|
3892
|
+
margin: 0
|
|
3893
|
+
},
|
|
3894
|
+
messageError: {
|
|
3895
|
+
color: "#dc2626",
|
|
3896
|
+
fontSize: "13px",
|
|
3897
|
+
margin: 0
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
function PromoCodeInput({
|
|
3901
|
+
promo,
|
|
3902
|
+
placeholder = "\u512A\u60E0\u78BC",
|
|
3903
|
+
applyText = "\u5957\u7528",
|
|
3904
|
+
clearText = "\u79FB\u9664",
|
|
3905
|
+
disabled = false,
|
|
3906
|
+
className,
|
|
3907
|
+
style: containerStyle,
|
|
3908
|
+
inputClassName,
|
|
3909
|
+
inputStyle,
|
|
3910
|
+
buttonClassName,
|
|
3911
|
+
buttonStyle,
|
|
3912
|
+
messageClassName,
|
|
3913
|
+
messageStyle,
|
|
3914
|
+
unstyled = false
|
|
3915
|
+
}) {
|
|
3916
|
+
const [inputValue, setInputValue] = useState("");
|
|
3917
|
+
const handleApply = useCallback(() => {
|
|
3918
|
+
if (inputValue.trim()) {
|
|
3919
|
+
promo.apply(inputValue.trim());
|
|
3920
|
+
}
|
|
3921
|
+
}, [inputValue, promo]);
|
|
3922
|
+
const handleClear = useCallback(() => {
|
|
3923
|
+
setInputValue("");
|
|
3924
|
+
promo.clear();
|
|
3925
|
+
}, [promo]);
|
|
3926
|
+
const handleKeyDown = useCallback(
|
|
3927
|
+
(e) => {
|
|
3928
|
+
if (e.key === "Enter") {
|
|
3929
|
+
e.preventDefault();
|
|
3930
|
+
handleApply();
|
|
3931
|
+
}
|
|
3932
|
+
},
|
|
3933
|
+
[handleApply]
|
|
3934
|
+
);
|
|
3935
|
+
const isApplied = promo.isValid;
|
|
3936
|
+
const isDisabled = disabled || promo.isLoading;
|
|
3937
|
+
const s = unstyled ? null : defaultStyles;
|
|
3938
|
+
return /* @__PURE__ */ jsxs(
|
|
3939
|
+
"div",
|
|
3940
|
+
{
|
|
3941
|
+
className,
|
|
3942
|
+
style: s ? { ...s.container, ...containerStyle } : containerStyle,
|
|
3943
|
+
children: [
|
|
3944
|
+
/* @__PURE__ */ jsxs("div", { style: s?.row, children: [
|
|
3945
|
+
/* @__PURE__ */ jsx(
|
|
3946
|
+
"input",
|
|
3947
|
+
{
|
|
3948
|
+
type: "text",
|
|
3949
|
+
value: isApplied ? promo.code || "" : inputValue,
|
|
3950
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
3951
|
+
onKeyDown: handleKeyDown,
|
|
3952
|
+
placeholder,
|
|
3953
|
+
disabled: isDisabled || isApplied,
|
|
3954
|
+
className: inputClassName,
|
|
3955
|
+
style: s ? { ...s.input, ...inputStyle } : inputStyle,
|
|
3956
|
+
"aria-label": placeholder
|
|
3957
|
+
}
|
|
3958
|
+
),
|
|
3959
|
+
isApplied ? /* @__PURE__ */ jsx(
|
|
3960
|
+
"button",
|
|
3961
|
+
{
|
|
3962
|
+
type: "button",
|
|
3963
|
+
onClick: handleClear,
|
|
3964
|
+
disabled,
|
|
3965
|
+
className: buttonClassName,
|
|
3966
|
+
style: s ? { ...s.button, ...buttonStyle } : buttonStyle,
|
|
3967
|
+
children: clearText
|
|
3968
|
+
}
|
|
3969
|
+
) : /* @__PURE__ */ jsx(
|
|
3970
|
+
"button",
|
|
3971
|
+
{
|
|
3972
|
+
type: "button",
|
|
3973
|
+
onClick: handleApply,
|
|
3974
|
+
disabled: isDisabled || !inputValue.trim(),
|
|
3975
|
+
className: buttonClassName,
|
|
3976
|
+
style: s ? {
|
|
3977
|
+
...s.button,
|
|
3978
|
+
...isDisabled || !inputValue.trim() ? s.buttonDisabled : {},
|
|
3979
|
+
...buttonStyle
|
|
3980
|
+
} : buttonStyle,
|
|
3981
|
+
children: promo.isLoading ? "\u9A57\u8B49\u4E2D..." : applyText
|
|
3982
|
+
}
|
|
3983
|
+
)
|
|
3984
|
+
] }),
|
|
3985
|
+
promo.isValid && promo.discount && /* @__PURE__ */ jsxs(
|
|
3986
|
+
"p",
|
|
3987
|
+
{
|
|
3988
|
+
className: messageClassName,
|
|
3989
|
+
style: s ? { ...s.messageSuccess, ...messageStyle } : messageStyle,
|
|
3990
|
+
role: "status",
|
|
3991
|
+
children: [
|
|
3992
|
+
promo.discount.label,
|
|
3993
|
+
promo.discount.bonusTrialDays && promo.discount.bonusTrialDays > 0 ? ` + ${promo.discount.bonusTrialDays} \u5929\u514D\u8CBB\u8A66\u7528` : "",
|
|
3994
|
+
promo.discount.bonusMonths && promo.discount.bonusMonths > 0 ? ` + \u8D08\u9001 ${promo.discount.bonusMonths} \u500B\u6708` : ""
|
|
3995
|
+
]
|
|
3996
|
+
}
|
|
3997
|
+
),
|
|
3998
|
+
promo.error && /* @__PURE__ */ jsx(
|
|
3999
|
+
"p",
|
|
4000
|
+
{
|
|
4001
|
+
className: messageClassName,
|
|
4002
|
+
style: s ? { ...s.messageError, ...messageStyle } : messageStyle,
|
|
4003
|
+
role: "alert",
|
|
4004
|
+
children: promo.error
|
|
4005
|
+
}
|
|
4006
|
+
)
|
|
4007
|
+
]
|
|
4008
|
+
}
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
3745
4011
|
function useCustomer() {
|
|
3746
4012
|
const context = useContext(CustomerContext);
|
|
3747
4013
|
if (!context) {
|
|
@@ -3818,4 +4084,4 @@ function useCustomer() {
|
|
|
3818
4084
|
};
|
|
3819
4085
|
}
|
|
3820
4086
|
|
|
3821
|
-
export { RecurProvider, useCustomer, useProducts as usePlans, useProducts, useRecur, useSubscribe };
|
|
4087
|
+
export { PromoCodeInput, RecurProvider, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe };
|
package/dist/recur.umd.js
CHANGED
|
@@ -2101,7 +2101,7 @@
|
|
|
2101
2101
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
|
|
2102
2102
|
<circle cx="12" cy="7" r="4"/>
|
|
2103
2103
|
</svg>
|
|
2104
|
-
`}setupEventListeners(){let t=this.shadowRoot?.querySelector("button");t&&t.addEventListener("click",this.handleClick)}redirectToPortal(t){let r=this.getAttribute("target");this.dispatchEvent(new CustomEvent("portal-redirect",{detail:{url:t},bubbles:!0,composed:!0})),r==="_blank"?window.open(t,"_blank","noopener,noreferrer"):window.location.href=t}async fetchAndRedirect(t){let r=this.getAttribute("customer-id"),o=this.getAttribute("return-url");this._isLoading=!0,this.render(),this.setupEventListeners();try{let i={};r&&(i.customerId=r),o&&(i.returnUrl=o);let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!n.ok){let u=await n.json().catch(()=>({}));throw new Error(u.error?.message||u.message||`HTTP ${n.status}: Failed to create portal session`)}let a=await n.json(),c=a.url||a.portalUrl;if(!c)throw new Error("Portal URL not found in response");this._isLoading=!1,this.render(),this.setupEventListeners(),this.redirectToPortal(c)}catch(i){this._isLoading=!1,this.render(),this.setupEventListeners(),this.dispatchError(i instanceof Error?i.message:"Failed to create portal session")}}dispatchError(t){console.error("[recur-portal]",t),this.dispatchEvent(new CustomEvent("portal-error",{detail:{message:t},bubbles:!0,composed:!0}))}};typeof window<"u"&&!customElements.get("recur-portal")&&customElements.define("recur-portal",pe)});var Qt={};S(Qt,{RecurCheckout:()=>O,RecurElements:()=>z,create:()=>kt,createElements:()=>$e,default:()=>Zt,init:()=>vt});async function jt(){if(typeof window>"u"){console.warn("[Recur SDK] Cannot register Web Components: window is not defined");return}await Promise.all([Promise.resolve().then(()=>(Le(),Me)),Promise.resolve().then(()=>(He(),Ue)),Promise.resolve().then(()=>(Be(),Ne)),Promise.resolve().then(()=>(Oe(),ze)),Promise.resolve().then(()=>(tt(),et)),Promise.resolve().then(()=>(ot(),rt)),Promise.resolve().then(()=>(pt(),dt)),Promise.resolve().then(()=>(ht(),mt)),Promise.resolve().then(()=>(ft(),gt))]);let e=["recur-loading-spinner","recur-success-message","recur-error-display","recur-skeleton-loader","recur-payment-form-skeleton","recur-toast","recur-toast-container","recur-payment-form","recur-checkout","recur-portal"].filter(t=>!customElements.get(t));e.length>0&&console.warn("[Recur SDK] The following components are not registered:",e)}typeof window<"u"&&jt();function Ft(s){return s.replace(/_([a-z])/g,(e,t)=>t.toUpperCase())}function w(s){if(s==null)return s;if(Array.isArray(s))return s.map(e=>w(e));if(s instanceof Date)return s;if(typeof s=="object"){let e={};for(let[t,r]of Object.entries(s)){let o=Ft(t);e[o]=w(r)}return e}return s}var me={name:"recur-tw",version:"0.
|
|
2104
|
+
`}setupEventListeners(){let t=this.shadowRoot?.querySelector("button");t&&t.addEventListener("click",this.handleClick)}redirectToPortal(t){let r=this.getAttribute("target");this.dispatchEvent(new CustomEvent("portal-redirect",{detail:{url:t},bubbles:!0,composed:!0})),r==="_blank"?window.open(t,"_blank","noopener,noreferrer"):window.location.href=t}async fetchAndRedirect(t){let r=this.getAttribute("customer-id"),o=this.getAttribute("return-url");this._isLoading=!0,this.render(),this.setupEventListeners();try{let i={};r&&(i.customerId=r),o&&(i.returnUrl=o);let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!n.ok){let u=await n.json().catch(()=>({}));throw new Error(u.error?.message||u.message||`HTTP ${n.status}: Failed to create portal session`)}let a=await n.json(),c=a.url||a.portalUrl;if(!c)throw new Error("Portal URL not found in response");this._isLoading=!1,this.render(),this.setupEventListeners(),this.redirectToPortal(c)}catch(i){this._isLoading=!1,this.render(),this.setupEventListeners(),this.dispatchError(i instanceof Error?i.message:"Failed to create portal session")}}dispatchError(t){console.error("[recur-portal]",t),this.dispatchEvent(new CustomEvent("portal-error",{detail:{message:t},bubbles:!0,composed:!0}))}};typeof window<"u"&&!customElements.get("recur-portal")&&customElements.define("recur-portal",pe)});var Qt={};S(Qt,{RecurCheckout:()=>O,RecurElements:()=>z,create:()=>kt,createElements:()=>$e,default:()=>Zt,init:()=>vt});async function jt(){if(typeof window>"u"){console.warn("[Recur SDK] Cannot register Web Components: window is not defined");return}await Promise.all([Promise.resolve().then(()=>(Le(),Me)),Promise.resolve().then(()=>(He(),Ue)),Promise.resolve().then(()=>(Be(),Ne)),Promise.resolve().then(()=>(Oe(),ze)),Promise.resolve().then(()=>(tt(),et)),Promise.resolve().then(()=>(ot(),rt)),Promise.resolve().then(()=>(pt(),dt)),Promise.resolve().then(()=>(ht(),mt)),Promise.resolve().then(()=>(ft(),gt))]);let e=["recur-loading-spinner","recur-success-message","recur-error-display","recur-skeleton-loader","recur-payment-form-skeleton","recur-toast","recur-toast-container","recur-payment-form","recur-checkout","recur-portal"].filter(t=>!customElements.get(t));e.length>0&&console.warn("[Recur SDK] The following components are not registered:",e)}typeof window<"u"&&jt();function Ft(s){return s.replace(/_([a-z])/g,(e,t)=>t.toUpperCase())}function w(s){if(s==null)return s;if(Array.isArray(s))return s.map(e=>w(e));if(s instanceof Date)return s;if(typeof s=="object"){let e={};for(let[t,r]of Object.entries(s)){let o=Ft(t);e[o]=w(r)}return e}return s}var me={name:"recur-tw",version:"0.15.0",description:"React & Vanilla JS SDK for embedding subscription checkout flows (Taiwan / PAYUNi)",type:"module",private:!1,author:"Recur",license:"Elastic-2.0",keywords:["react","vanilla-js","subscription","checkout","payment","payuni","recurring-billing","taiwan","\u7E41\u9AD4\u4E2D\u6587","sdk","embedded-checkout"],homepage:"https://recur.tw/",bugs:{email:"support@recur.tw"},scripts:{build:"tsup",dev:"tsup --watch",test:"vitest run","test:watch":"vitest",lint:"eslint .","lint:fix":"eslint . --fix","type-check":"tsc --noEmit",examples:"npx http-server . -p 8080 -o /examples/"},main:"./dist/index.js",module:"./dist/index.js",types:"./dist/index.d.ts",exports:{".":{types:"./dist/index.d.ts",import:"./dist/index.js",require:"./dist/index.cjs"},"./server":{types:"./dist/server.d.ts",import:"./dist/server.js",require:"./dist/server.cjs"},"./vanilla":"./dist/recur.umd.js","./checkout":"./dist/checkout.js","./widget":"./dist/widget.js","./package.json":"./package.json"},unpkg:"./dist/recur.umd.js",jsdelivr:"./dist/recur.umd.js",files:["dist","README.md","AGENTS.md","LICENSE"],sideEffects:["src/components/**/*.ts","src/components/**/*.js"],peerDependencies:{react:">=18.0.0","react-dom":">=18.0.0"},peerDependenciesMeta:{react:{optional:!0},"react-dom":{optional:!0}},devDependencies:{"@eslint/js":"^9.39.1","@testing-library/dom":"^10.4.1","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.3.0","@types/node":"^20.19.9","@types/react":"^19.1.9","@types/react-dom":"^19.1.7","@workspace/typescript-config":"workspace:*",autoprefixer:"^10.4.22",eslint:"^9","eslint-plugin-react":"^7.37.5","eslint-plugin-react-hooks":"^7.0.1",jsdom:"^27.2.0",postcss:"^8.5.6",react:"^19.2.1","react-dom":"^19.2.1",tailwindcss:"^4.1.11",tsup:"^8.3.5",typescript:"^5.9.2","typescript-eslint":"^8.47.0",vitest:"^4.1.0"},dependencies:{"lit-html":"^3.3.1"}};var qt=me.version,Yt="vanilla",he=class{constructor(e){l(this,"config");if(!e.publishableKey)throw new Error("publishableKey is required");this.config={baseUrl:"https://api.recur.tw",...e}}getHeaders(){return{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey,"X-Recur-SDK-Type":Yt,"X-Recur-SDK-Version":qt,"X-Recur-Source":typeof window<"u"?window.location.origin:"server"}}async createSubscription(e){let{customerName:t,customerEmail:r}=e,o=e.productId||e.planId,i=e.productSlug;if(!o&&!i)throw new Error("Either productId or productSlug is required");let n={customerName:t,customerEmail:r};o&&(n.productId=o),i&&(n.productSlug=i);let a=await fetch(`${this.config.baseUrl}/v1/subscriptions`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify(n)});if(!a.ok){let u=await a.json().catch(()=>({}));throw{code:u.error||"CHECKOUT_FAILED",message:u.message||"Failed to initiate checkout",details:u}}let c=await a.json();return w(c)}async fetchProducts(e){let t=new URL(`${this.config.baseUrl}/v1/products`);e?.type&&t.searchParams.set("type",e.type);let r=await fetch(t.toString(),{method:"GET",headers:this.getHeaders()});if(!r.ok){let i=await r.json().catch(()=>({}));throw{code:i.error||"FETCH_PRODUCTS_FAILED",message:i.message||"Failed to fetch products",details:i}}let o=await r.json();return w(o)}async fetchPlans(){return{plans:(await this.fetchProducts({type:"SUBSCRIPTION"})).data}}getConfig(){return{...this.config}}};var ge=class{constructor(e,t){l(this,"config");l(this,"options");l(this,"container");l(this,"checkoutId",null);l(this,"sdkToken",null);l(this,"sdkTimestamp",null);l(this,"creditToken",null);l(this,"sdkEnv","S");l(this,"payuniSDK",null);l(this,"isFormValid",!1);if(this.config=e,this.options=t,typeof t.container=="string"){let r=document.querySelector(t.container);if(!r)throw new Error(`Container not found: ${t.container}`);this.container=r}else this.container=t.container}async render(){try{await this.initCheckout(),this.renderHTML(),await this.loadPayUniSDK(),await this.initPayUniSDK(),this.setupFormSubmit()}catch(e){this.handleError(e)}}getBaseUrl(){if(this.config.baseUrl)return this.config.baseUrl;if(typeof window<"u"){let e=window.location.hostname;if(e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}async initCheckout(){let e=this.getBaseUrl(),t=await fetch(`${e}/v1/checkouts`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({productId:this.options.productId||this.options.planId,customerEmail:this.options.customerEmail,customerName:this.options.customerName})});if(!t.ok){let i=await t.json().catch(()=>({}));throw new Error(i.error||"Failed to initialize checkout")}let r=await t.json(),o=w(r);this.checkoutId=o.checkout.id,this.sdkToken=o.sdkToken,this.sdkTimestamp=o.sdkTimestamp||null,this.creditToken=o.creditToken||null,this.sdkEnv=o.livemode?"P":"S"}renderHTML(){this.container.innerHTML=`
|
|
2105
2105
|
<div class="recur-embedded-checkout" style="max-width: 400px; margin: 0 auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif;">
|
|
2106
2106
|
<div class="recur-checkout-header" style="margin-bottom: 24px;">
|
|
2107
2107
|
<h2 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600; color: #111;">Subscribe</h2>
|
package/dist/server.cjs
CHANGED
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recur-tw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "React & Vanilla JS SDK for embedding subscription checkout flows (Taiwan / PAYUNi)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup",
|
|
28
28
|
"dev": "tsup --watch",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
29
31
|
"lint": "eslint .",
|
|
30
32
|
"lint:fix": "eslint . --fix",
|
|
31
33
|
"type-check": "tsc --noEmit",
|
|
@@ -76,6 +78,9 @@
|
|
|
76
78
|
},
|
|
77
79
|
"devDependencies": {
|
|
78
80
|
"@eslint/js": "^9.39.1",
|
|
81
|
+
"@testing-library/dom": "^10.4.1",
|
|
82
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
83
|
+
"@testing-library/react": "^16.3.0",
|
|
79
84
|
"@types/node": "^20.19.9",
|
|
80
85
|
"@types/react": "^19.1.9",
|
|
81
86
|
"@types/react-dom": "^19.1.7",
|
|
@@ -84,13 +89,15 @@
|
|
|
84
89
|
"eslint": "^9",
|
|
85
90
|
"eslint-plugin-react": "^7.37.5",
|
|
86
91
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
92
|
+
"jsdom": "^27.2.0",
|
|
87
93
|
"postcss": "^8.5.6",
|
|
88
94
|
"react": "^19.2.1",
|
|
89
95
|
"react-dom": "^19.2.1",
|
|
90
96
|
"tailwindcss": "^4.1.11",
|
|
91
97
|
"tsup": "^8.3.5",
|
|
92
98
|
"typescript": "^5.9.2",
|
|
93
|
-
"typescript-eslint": "^8.47.0"
|
|
99
|
+
"typescript-eslint": "^8.47.0",
|
|
100
|
+
"vitest": "^4.1.0"
|
|
94
101
|
},
|
|
95
102
|
"dependencies": {
|
|
96
103
|
"lit-html": "^3.3.1"
|