mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
web: fix checkout (and make it load faster)
This commit is contained in:
@@ -123,6 +123,7 @@ test("applying coupon should change discount & total price", async () => {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
await pricing.waitForPaddleFrame();
|
||||
await pricing.applyCoupon("INTRO50");
|
||||
|
||||
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
|
||||
@@ -141,6 +142,7 @@ test("apply coupon through url", async () => {
|
||||
for (const plan of ["monthly", "yearly"] as const) {
|
||||
await app.checkout.goto(plan, "INTRO50");
|
||||
const pricing = await app.checkout.getPricing();
|
||||
await pricing.waitForPaddleFrame();
|
||||
await pricing.waitForCoupon();
|
||||
|
||||
planPrices[plan] = roundOffPrices(await pricing.getPrices());
|
||||
@@ -164,6 +166,7 @@ test("apply coupon after changing country", async () => {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
await pricing.waitForPaddleFrame();
|
||||
await pricing.changeCountry("IN", 110001);
|
||||
await pricing.applyCoupon("INTRO50");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200
|
||||
Total: $200/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200
|
||||
Total: $200/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200
|
||||
Total: $200/yr
|
||||
@@ -101,6 +101,10 @@ class PricingModel {
|
||||
});
|
||||
}
|
||||
|
||||
async waitForPaddleFrame() {
|
||||
await getPaddleFrame(this.page);
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.title.textContent();
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ function Field(props: FieldProps) {
|
||||
m: "2px",
|
||||
mr: "2px",
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
pointerEvents: disabled ? "none" : "all",
|
||||
...sx,
|
||||
flexDirection: "column"
|
||||
}}
|
||||
@@ -109,6 +108,7 @@ function Field(props: FieldProps) {
|
||||
sx={{
|
||||
flex: 1,
|
||||
...styles?.input,
|
||||
pointerEvents: disabled ? "none" : "all",
|
||||
":disabled": {
|
||||
bg: "background-disabled"
|
||||
},
|
||||
|
||||
@@ -481,10 +481,16 @@ function SelectedPlan(props: SelectedPlanProps) {
|
||||
)}
|
||||
{pricingInfo ? (
|
||||
<>
|
||||
<Text data-test-id={`checkout-plan-country-${pricingInfo.country}`} />
|
||||
{pricingInfo.coupon && (
|
||||
<Text data-test-id={`checkout-plan-coupon-applied`} />
|
||||
)}
|
||||
{IS_TESTING ? (
|
||||
<>
|
||||
<span
|
||||
data-test-id={`checkout-plan-country-${pricingInfo.country}`}
|
||||
/>
|
||||
{pricingInfo.coupon && (
|
||||
<span data-test-id={`checkout-plan-coupon-applied`} />
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Field
|
||||
inputRef={couponInputRef}
|
||||
|
||||
@@ -69,9 +69,15 @@ type PaddleCheckoutProps = {
|
||||
coupon?: string;
|
||||
};
|
||||
export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
const { plan, onPriceUpdated, coupon, onCouponApplied, onCompleted, user } =
|
||||
props;
|
||||
const [sourceUrl, setSourceUrl] = useState<string>();
|
||||
const {
|
||||
plan,
|
||||
onPriceUpdated,
|
||||
coupon,
|
||||
theme,
|
||||
onCouponApplied,
|
||||
onCompleted,
|
||||
user
|
||||
} = props;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const appliedCouponCode = useRef<string>();
|
||||
const checkoutId = useRef<string>();
|
||||
@@ -81,7 +87,11 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
const reloadCheckout = useCallback(() => {
|
||||
if (!checkoutRef.current) return;
|
||||
setIsLoading(true);
|
||||
checkoutRef.current.src = `${PADDLE_ORIGIN}/checkout/?checkout_id=${checkoutId.current}&display_mode=inline&apple_pay_enabled=false`;
|
||||
checkoutRef.current.src = `${PADDLE_ORIGIN}/checkout/?checkout_id=${
|
||||
checkoutId.current
|
||||
}&display_mode=inline&apple_pay_enabled=${isFeatureSupported(
|
||||
"applePaySupported"
|
||||
)}`;
|
||||
}, []);
|
||||
|
||||
const updatePrice = useCallback(
|
||||
@@ -96,12 +106,37 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
[onPriceUpdated, plan]
|
||||
);
|
||||
|
||||
const updateCoupon = useCallback(
|
||||
async (checkoutId: string) => {
|
||||
if (
|
||||
appliedCouponCode.current === coupon ||
|
||||
!appliedCouponCode.current === !coupon
|
||||
)
|
||||
return false;
|
||||
if (onCouponApplied) onCouponApplied();
|
||||
const checkoutData = coupon
|
||||
? await applyCoupon(checkoutId, coupon).catch(() => false)
|
||||
: await removeCoupon(checkoutId).catch(() => false);
|
||||
if (!checkoutData) {
|
||||
await updatePrice(checkoutId, true).catch(() => false);
|
||||
return false;
|
||||
}
|
||||
appliedCouponCode.current = coupon;
|
||||
return true;
|
||||
},
|
||||
[coupon, updatePrice, onCouponApplied]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const url = await getCheckoutURL(props);
|
||||
setSourceUrl(url);
|
||||
})();
|
||||
}, [props]);
|
||||
createCheckout({ plan, theme, user, coupon }).then((checkoutData) => {
|
||||
if (!checkoutData) return;
|
||||
const pricingInfo = getPricingInfo(plan, checkoutData);
|
||||
if (onPriceUpdated) onPriceUpdated(pricingInfo);
|
||||
appliedCouponCode.current = pricingInfo.coupon;
|
||||
checkoutId.current = checkoutData.public_checkout_id;
|
||||
reloadCheckout();
|
||||
});
|
||||
}, [plan, theme, user]);
|
||||
|
||||
useEffect(() => {
|
||||
async function onMessage(ev: MessageEvent<PaddleEvent>) {
|
||||
@@ -110,10 +145,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
const { event, event_name, callback_data } = ev.data;
|
||||
const { checkout } = callback_data;
|
||||
|
||||
if (event === PaddleEvents["Checkout.RemoveSpinner"]) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (event === PaddleEvents["Checkout.RemoveSpinner"]) setIsLoading(false);
|
||||
|
||||
if (
|
||||
!checkout ||
|
||||
@@ -125,32 +157,30 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event_name === PaddleEvents["Checkout.Customer.Details"] &&
|
||||
!checkoutId.current
|
||||
) {
|
||||
submitCustomerInfo(
|
||||
checkout.id,
|
||||
user.email,
|
||||
callback_data.user?.country || "US"
|
||||
).finally(() => {
|
||||
checkoutId.current = checkout.id;
|
||||
reloadCheckout();
|
||||
});
|
||||
}
|
||||
|
||||
if (event_name === PaddleEvents["Checkout.Complete"]) {
|
||||
onCompleted && onCompleted();
|
||||
return;
|
||||
}
|
||||
if (event_name === PaddleEvents["Checkout.Loaded"] && checkoutId.current)
|
||||
|
||||
if (event_name === PaddleEvents["Checkout.Loaded"]) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const pricingInfo = await updatePrice(checkout.id);
|
||||
if (!pricingInfo) return;
|
||||
|
||||
const pricingInfo = getPricingInfo(plan, {
|
||||
public_checkout_id: checkout.id,
|
||||
ip_geo_country_code: callback_data.user?.country || "US",
|
||||
items: [
|
||||
{
|
||||
prices: checkout.prices.customer.items,
|
||||
recurring: { prices: checkout.recurring_prices.customer.items }
|
||||
}
|
||||
]
|
||||
});
|
||||
if (onPriceUpdated) onPriceUpdated(pricingInfo);
|
||||
appliedCouponCode.current = pricingInfo.coupon;
|
||||
checkoutId.current = checkout.id;
|
||||
|
||||
await updateCoupon(checkout.id);
|
||||
}
|
||||
window.addEventListener("message", onMessage, false);
|
||||
return () => {
|
||||
@@ -162,38 +192,16 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
plan,
|
||||
onCompleted,
|
||||
user.email,
|
||||
reloadCheckout
|
||||
reloadCheckout,
|
||||
updateCoupon
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkout_id = checkoutId.current;
|
||||
if (
|
||||
!checkout_id ||
|
||||
appliedCouponCode.current === coupon ||
|
||||
!appliedCouponCode.current === !coupon
|
||||
)
|
||||
return;
|
||||
if (onCouponApplied) onCouponApplied();
|
||||
|
||||
(async function () {
|
||||
const checkoutData = coupon
|
||||
? await applyCoupon(checkout_id, coupon).catch(() => false)
|
||||
: await removeCoupon(checkout_id).catch(() => false);
|
||||
if (!checkoutData) {
|
||||
await updatePrice(checkout_id, true);
|
||||
return;
|
||||
}
|
||||
appliedCouponCode.current = coupon;
|
||||
reloadCheckout();
|
||||
})();
|
||||
}, [
|
||||
coupon,
|
||||
onCouponApplied,
|
||||
onPriceUpdated,
|
||||
plan,
|
||||
reloadCheckout,
|
||||
updatePrice
|
||||
]);
|
||||
if (!checkoutId.current) return;
|
||||
updateCoupon(checkoutId.current).then((result) => {
|
||||
if (result) reloadCheckout();
|
||||
});
|
||||
}, [coupon]);
|
||||
|
||||
return (
|
||||
<ScrollContainer
|
||||
@@ -222,7 +230,6 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
scrolling="no"
|
||||
frameBorder={"0"}
|
||||
ref={checkoutRef}
|
||||
src={sourceUrl}
|
||||
allow={`payment ${PADDLE_ORIGIN} ${SUBSCRIPTION_MANAGEMENT_URL};`}
|
||||
style={{
|
||||
// padding: "0px 30px",
|
||||
@@ -240,9 +247,13 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
);
|
||||
}
|
||||
|
||||
async function getCheckoutURL(params: PaddleCheckoutProps) {
|
||||
function getCheckoutURL(params: {
|
||||
plan: PaddleCheckoutProps["plan"];
|
||||
theme: PaddleCheckoutProps["theme"];
|
||||
user: PaddleCheckoutProps["user"];
|
||||
}) {
|
||||
const { plan, theme, user } = params;
|
||||
const BASE_URL = `${PADDLE_ORIGIN}/paddlejs?ccsURL=${CHECKOUT_SERVICE_ORIGIN}/create/checkout/product/${plan.id}`;
|
||||
const BASE_URL = `${CHECKOUT_SERVICE_ORIGIN}/create/v2/checkout/product/${plan.id}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.set("product", plan.id);
|
||||
queryParams.set("vendor", VENDOR_ID.toString());
|
||||
@@ -264,6 +275,7 @@ async function getCheckoutURL(params: PaddleCheckoutProps) {
|
||||
queryParams.set("parent_url", window.location.origin);
|
||||
queryParams.set("parentURL", window.location.origin);
|
||||
queryParams.set("referring_domain", window.location.hostname);
|
||||
queryParams.set("locale", "en");
|
||||
const fullURL = `${BASE_URL}?${queryParams.toString()}`;
|
||||
return fullURL;
|
||||
}
|
||||
@@ -276,7 +288,7 @@ function getPricingInfo(plan: Plan, checkoutData: CheckoutData): PricingInfo {
|
||||
const isRecurringDiscount = recurringPrice.discounts.length > 0;
|
||||
|
||||
return {
|
||||
country: checkoutData.customer.country_code,
|
||||
country: checkoutData.ip_geo_country_code,
|
||||
currency: price.currency,
|
||||
discount: {
|
||||
amount: discount?.gross_discount || 0,
|
||||
@@ -317,9 +329,6 @@ async function applyCoupon(
|
||||
|
||||
if (!response.ok) return false;
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
|
||||
await sendCheckoutEvent(checkoutId, PaddleEvents["Checkout.Coupon.Applied"]);
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
@@ -348,8 +357,8 @@ async function submitCustomerInfo(
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
return json.data;
|
||||
}
|
||||
|
||||
@@ -361,25 +370,9 @@ async function removeCoupon(checkoutId: string): Promise<CheckoutData | false> {
|
||||
});
|
||||
if (!response.ok) return false;
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
|
||||
await sendCheckoutEvent(checkoutId, PaddleEvents["Checkout.Coupon.Remove"]);
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function sendCheckoutEvent(checkoutId: string, eventName: PaddleEvents) {
|
||||
const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/event`;
|
||||
const body = { data: { event_name: eventName } };
|
||||
const headers = new Headers();
|
||||
headers.set("content-type", "application/json");
|
||||
const response = await fetch(url, {
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
method: "POST"
|
||||
});
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async function getCheckoutData(
|
||||
checkoutId: string
|
||||
): Promise<CheckoutData | undefined> {
|
||||
@@ -389,3 +382,35 @@ async function getCheckoutData(
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function createCheckout(props: {
|
||||
plan: PaddleCheckoutProps["plan"];
|
||||
user: PaddleCheckoutProps["user"];
|
||||
theme: PaddleCheckoutProps["theme"];
|
||||
coupon?: PaddleCheckoutProps["coupon"];
|
||||
}) {
|
||||
const { plan, user, theme, coupon } = props;
|
||||
const url = getCheckoutURL({ plan, user, theme });
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return false;
|
||||
|
||||
const json = (await response.json()) as CheckoutDataResponse;
|
||||
|
||||
let checkoutData = json.data;
|
||||
const checkoutId = checkoutData.public_checkout_id;
|
||||
|
||||
checkoutData = await submitCustomerInfo(
|
||||
checkoutId,
|
||||
user.email,
|
||||
json.data.ip_geo_country_code
|
||||
)
|
||||
.then((res) => (res ? res : checkoutData))
|
||||
.catch(() => checkoutData);
|
||||
|
||||
if (coupon)
|
||||
checkoutData = await applyCoupon(checkoutId, coupon)
|
||||
.then((res) => (res ? res : checkoutData))
|
||||
.catch(() => checkoutData);
|
||||
|
||||
return checkoutData;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ export enum PaddleEvents {
|
||||
|
||||
export interface CallbackData {
|
||||
checkout?: Checkout;
|
||||
coupon?: { coupon_code: string };
|
||||
user?: {
|
||||
email: string;
|
||||
id: string;
|
||||
@@ -86,6 +87,16 @@ export interface CallbackData {
|
||||
|
||||
export interface Checkout {
|
||||
id?: string;
|
||||
prices: {
|
||||
customer: {
|
||||
items: CheckoutPrices[];
|
||||
};
|
||||
};
|
||||
recurring_prices: {
|
||||
customer: {
|
||||
items: CheckoutPrices[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type PaddleEvent = {
|
||||
@@ -126,8 +137,11 @@ export type Discount = {
|
||||
|
||||
export interface Price {
|
||||
gross: number;
|
||||
gross_after_discount?: number;
|
||||
net: number;
|
||||
net_after_discount?: number;
|
||||
tax: number;
|
||||
tax_after_discount?: number;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
@@ -137,67 +151,67 @@ export interface CheckoutDataResponse {
|
||||
|
||||
export interface CheckoutData {
|
||||
public_checkout_id: string;
|
||||
type: string;
|
||||
uuid: string;
|
||||
vendor: Vendor;
|
||||
display_currency: string;
|
||||
charge_currency: string;
|
||||
customer: Customer;
|
||||
// type: string;
|
||||
// uuid: string;
|
||||
// vendor: Vendor;
|
||||
// display_currency: string;
|
||||
// charge_currency: string;
|
||||
// customer: Customer;
|
||||
items: Item[];
|
||||
available_payment_methods: unknown[];
|
||||
total: TotalPrice[];
|
||||
pending_payment: boolean;
|
||||
completed: boolean;
|
||||
payment_method_type: null;
|
||||
flagged_for_review: boolean;
|
||||
// available_payment_methods: unknown[];
|
||||
// total: TotalPrice[];
|
||||
// pending_payment: boolean;
|
||||
// completed: boolean;
|
||||
// payment_method_type: null;
|
||||
// flagged_for_review: boolean;
|
||||
ip_geo_country_code: string;
|
||||
tax: null;
|
||||
name: null;
|
||||
image_url: null;
|
||||
message: null;
|
||||
passthrough: string;
|
||||
redirect_url: null;
|
||||
created_at: Date;
|
||||
// tax: null;
|
||||
// name: null;
|
||||
// image_url: null;
|
||||
// message: null;
|
||||
// passthrough: string;
|
||||
// redirect_url: null;
|
||||
// created_at: Date;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: number;
|
||||
email: string;
|
||||
// id: number;
|
||||
// email: string;
|
||||
country_code: string;
|
||||
postcode: null;
|
||||
audience_opt_in: boolean;
|
||||
// postcode: null;
|
||||
// audience_opt_in: boolean;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
checkout_product_id: number;
|
||||
product_id: number;
|
||||
name: string;
|
||||
custom_message: string;
|
||||
quantity: number;
|
||||
allow_quantity: boolean;
|
||||
min_quantity: number;
|
||||
max_quantity: number;
|
||||
icon_url: string;
|
||||
// checkout_product_id: number;
|
||||
// product_id: number;
|
||||
// name: string;
|
||||
// custom_message: string;
|
||||
// quantity: number;
|
||||
// allow_quantity: boolean;
|
||||
// min_quantity: number;
|
||||
// max_quantity: number;
|
||||
// icon_url: string;
|
||||
prices: CheckoutPrices[];
|
||||
recurring: Recurring;
|
||||
webhook_url: null;
|
||||
// webhook_url: null;
|
||||
}
|
||||
|
||||
export interface CheckoutPrices {
|
||||
currency: string;
|
||||
unit_price: CheckoutPrice;
|
||||
line_price: CheckoutPrice;
|
||||
// line_price: CheckoutPrice;
|
||||
discounts: CheckoutDiscount[];
|
||||
tax_rate: number;
|
||||
// tax_rate: number;
|
||||
}
|
||||
|
||||
export interface CheckoutDiscount {
|
||||
rank: number;
|
||||
type: string;
|
||||
net_discount: number;
|
||||
// rank: number;
|
||||
// type: string;
|
||||
// net_discount: number;
|
||||
gross_discount: number;
|
||||
code: string;
|
||||
description: string;
|
||||
// description: string;
|
||||
}
|
||||
|
||||
export interface CheckoutPrice {
|
||||
@@ -212,9 +226,9 @@ export interface CheckoutPrice {
|
||||
}
|
||||
|
||||
export interface Recurring {
|
||||
period: string;
|
||||
interval: number;
|
||||
trial_days: number;
|
||||
// period: string;
|
||||
// interval: number;
|
||||
// trial_days: number;
|
||||
prices: CheckoutPrices[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user