feat: finalize ne checkout/upgrade dialog ui

This commit is contained in:
thecodrr
2021-11-18 09:09:20 +05:00
parent 71d699b4e6
commit 84983f2d3e
10 changed files with 527 additions and 796 deletions

View File

@@ -22,6 +22,7 @@
"emotion-theming": "^10.0.19", "emotion-theming": "^10.0.19",
"event-source-polyfill": "^1.0.25", "event-source-polyfill": "^1.0.25",
"fast-sort": "^2.1.1", "fast-sort": "^2.1.1",
"fetch-jsonp": "^1.2.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^4.1.17", "framer-motion": "^4.1.17",
"hash-wasm": "^4.9.0", "hash-wasm": "^4.9.0",

View File

@@ -6,7 +6,7 @@
/security#csp-meta-tag --> /security#csp-meta-tag -->
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="script-src 'self' https://analytics.streetwriters.co https://cdn.paddle.com https://cdnjs.cloudflare.com 'unsafe-inline' 'unsafe-eval';" content="script-src 'self' https://checkout.paddle.com https://analytics.streetwriters.co https://cdn.paddle.com https://cdnjs.cloudflare.com 'unsafe-inline' 'unsafe-eval';"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"

View File

@@ -1,10 +1,16 @@
import { ANALYTICS_EVENTS, trackEvent } from "../utils/analytics"; import { ANALYTICS_EVENTS, trackEvent } from "../utils/analytics";
import DEFAULT_DATA_MONTHLY from "./monthly.json"; import fetchJsonp from "fetch-jsonp";
import DEFAULT_DATA_YEARLY from "./yearly.json"; import Config from "../utils/config";
const isDev = false; // process.env.NODE_ENV === "development"; const isDev = false; // process.env.NODE_ENV === "development";
const VENDOR_ID = isDev ? 1506 : 128190; const VENDOR_ID = isDev ? 1506 : 128190;
/**
*
* @param {"yearly"|"monthly"} plan
* @returns
*/
const PRODUCT_ID = (plan) => { const PRODUCT_ID = (plan) => {
if (isDev) return plan === "monthly" ? 9822 : 0; if (isDev) return plan === "monthly" ? 9822 : 0;
else return plan === "monthly" ? 648884 : 658759; else return plan === "monthly" ? 648884 : 658759;
@@ -12,6 +18,15 @@ const PRODUCT_ID = (plan) => {
function loadPaddle(eventCallback) { function loadPaddle(eventCallback) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (window.Paddle) {
window.Paddle.Options({
vendor: VENDOR_ID,
eventCallback,
});
resolve();
return;
}
var script = document.createElement("script"); var script = document.createElement("script");
script.src = "https://cdn.paddle.com/paddle/paddle.js"; script.src = "https://cdn.paddle.com/paddle/paddle.js";
script.async = true; script.async = true;
@@ -28,6 +43,53 @@ function loadPaddle(eventCallback) {
}); });
} }
function inlineCheckout({ user, plan, coupon, country, onCheckoutLoaded }) {
return new Promise(async (resolve) => {
await loadPaddle((e) => {
console.log("E", e);
const data = e.eventData;
switch (e.event) {
case "Checkout.Loaded":
onCheckoutLoaded && onCheckoutLoaded(data);
resolve();
break;
case "Checkout.Coupon.Applied":
onCheckoutLoaded && onCheckoutLoaded(data);
resolve();
break;
default:
break;
}
});
const { Paddle } = window;
if (!Paddle) return;
// if (coupon) {
// trackEvent(ANALYTICS_EVENTS.offerClaimed, `[${coupon}] redeemed!`);
// } else {
// trackEvent(ANALYTICS_EVENTS.checkoutStarted, `Checkout requested`);
// }
Paddle.Checkout.open({
frameTarget: "checkout-container",
frameStyle: "position: relative; width: 100%; border: 0;",
frameInitialHeight: 416,
disableLogout: true,
allowQuantity: false,
method: "inline",
displayModeTheme: Config.get("theme", "light"),
product: PRODUCT_ID(plan),
country,
email: user.email,
coupon,
passthrough: JSON.stringify({
userId: user.id,
}),
});
});
}
async function upgrade(user, coupon, plan) { async function upgrade(user, coupon, plan) {
if (!window.Paddle) { if (!window.Paddle) {
await loadPaddle(); await loadPaddle();
@@ -66,35 +128,18 @@ async function openPaddleDialog(overrideUrl) {
}); });
} }
async function getCouponData(coupon, plan) { async function getPlans() {
let url = const monthlyProductId = PRODUCT_ID("monthly");
plan === "monthly" const yearlyProductId = PRODUCT_ID("yearly");
? "https://checkout-service.paddle.com/checkout/1122-chree0325aa1705-38b9ffa5ce/coupon" const url = `https://checkout.paddle.com/api/2.0/prices?product_ids=${yearlyProductId},${monthlyProductId}&callback=getPrices`;
: "https://checkout-service.paddle.com/checkout/1122-chre94eb195cbde-12dcba6761/coupon"; const response = await fetchJsonp(url, {
jsonpCallback: "callback",
try { jsonpCallbackFunction: "getPrices",
const response = await fetch(url, {
headers: {
accept: "application/json, text/plain, */*",
"content-type": "application/json;charset=UTF-8",
},
body: coupon
? JSON.stringify({ data: { coupon_code: coupon } })
: undefined,
method: coupon ? "POST" : "DELETE",
}); });
const json = await response.json(); const json = await response.json();
if (response.ok) { if (!json || !json.success || !json.response?.products?.length)
return json.data; throw new Error("Failed to get prices.");
} else { return json.response.products;
throw new Error(json.errors[0].details);
}
} catch (e) {
console.error("Error: ", e);
return plan === "monthly"
? DEFAULT_DATA_MONTHLY.data
: DEFAULT_DATA_YEARLY.data;
}
} }
export { upgrade, openPaddleDialog, getCouponData }; export { upgrade, openPaddleDialog, getPlans, inlineCheckout };

View File

@@ -18,22 +18,22 @@ async function initializeDatabase() {
db = new Database(Storage, EventSource, FS); db = new Database(Storage, EventSource, FS);
// if (isTesting()) { // if (isTesting()) {
db.host({ // db.host({
API_HOST: "https://api.notesnook.com", // API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co", // AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co", // SSE_HOST: "https://events.streetwriters.co",
}); // });
// } else { // } else {
// db.host({ // db.host({
// API_HOST: "http://localhost:5264", // API_HOST: "http://localhost:5264",
// AUTH_HOST: "http://localhost:8264", // AUTH_HOST: "http://localhost:8264",
// SSE_HOST: "http://localhost:7264", // SSE_HOST: "http://localhost:7264",
// }); // });
// db.host({ db.host({
// API_HOST: "http://192.168.10.29:5264", API_HOST: "http://192.168.10.29:5264",
// AUTH_HOST: "http://192.168.10.29:8264", AUTH_HOST: "http://192.168.10.29:8264",
// SSE_HOST: "http://192.168.10.29:7264", SSE_HOST: "http://192.168.10.29:7264",
// }); });
// } // }
await db.init(); await db.init();

View File

@@ -1,172 +0,0 @@
{
"data": {
"public_checkout_id": "112231879-chree0325aa1705-38b9ffa5ce",
"type": "default",
"vendor": { "id": 128190, "name": "Streetwriters (Private) Limited" },
"display_currency": "USD",
"charge_currency": "USD",
"settings": {
"feature_flags": {
"show_save_payment_details_UI_without_taking_payment": false,
"show_save_payment_details_UI_and_take_payment": false,
"marketing_consent": true,
"customer_name": true,
"tax_code": false,
"tax_code_link": false,
"coupon": true,
"coupon_link": true,
"quantity_enabled": false,
"quantity_selection": false,
"hide_compliance_bar": false
},
"variant": "multipage",
"expires": "2021-12-07 23:59:59",
"marketing_consent_message": "<var vendorname>Streetwriters (Private) Limited</var> may send me product updates and offers via email. It is possible to opt-out at any time.",
"language_code": "en",
"disable_logout": false,
"styles": { "primary_colour": null, "theme": "light" },
"inline_styles": null
},
"customer": {
"id": 11363557,
"email": "enkaboot@gmail.com",
"country_code": null,
"postcode": null,
"remember_me": false,
"audience_opt_in": false
},
"items": [
{
"checkout_product_id": 116991843,
"product_id": 648884,
"name": "Notesnook Pro (Monthly)",
"custom_message": "",
"quantity": 1,
"allow_quantity": false,
"icon_url": "https://paddle.s3.amazonaws.com/user/128190/UgQbUiBTuWY7j9PDMnvY_android-chrome-512x512.png",
"prices": [
{
"currency": "USD",
"unit_price": {
"net": 4.49,
"gross": 4.49,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 4.49,
"gross_after_discount": 4.49,
"tax": 0.0,
"tax_after_discount": 0.0
},
"line_price": {
"net": 4.49,
"gross": 4.49,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 4.49,
"gross_after_discount": 4.49,
"tax": 0.0,
"tax_after_discount": 0.0
},
"discounts": [],
"tax_rate": 0.0
}
],
"recurring": {
"period": "month",
"interval": 1,
"trial_days": 0,
"prices": [
{
"currency": "USD",
"unit_price": {
"net": 4.49,
"gross": 4.49,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 4.49,
"gross_after_discount": 4.49,
"tax": 0.0,
"tax_after_discount": 0.0
},
"line_price": {
"net": 4.49,
"gross": 4.49,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 4.49,
"gross_after_discount": 4.49,
"tax": 0.0,
"tax_after_discount": 0.0
},
"discounts": [],
"tax_rate": 0.0
}
]
},
"webhook_url": null
}
],
"available_payment_methods": [],
"total": [
{
"net": 4.49,
"gross": 4.49,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 4.49,
"gross_after_discount": 4.49,
"tax": 0.0,
"tax_after_discount": 0.0,
"currency": "USD",
"is_free": false,
"includes_tax": false,
"tax_rate": 0.0
}
],
"pending_payment": false,
"completed": false,
"payment_method_type": null,
"flagged_for_review": false,
"ip_geo_country_code": "PK",
"tax": null,
"passthrough": "{}",
"redirect_url": null,
"tracking": {
"source_page": "http://localhost:3000/settings",
"test_name": null,
"test_variant": null,
"initial_request": {
"data": {
"type": "default",
"items": [{ "product_id": 648884 }],
"customer": { "email": "redacted" },
"settings": { "passthrough": "{}" },
"tracking": { "referrer": "localhost:3000 / localhost:3000" },
"parent_url": "http://localhost:3000/settings",
"geo_country": "PK"
}
}
},
"created_at": "2021-10-08 12:03:13",
"paddlejs": {
"vendor": {
"currency": "USD",
"prices": {
"unit": 4.49,
"unit_tax": 0.0,
"total": 4.49,
"total_tax": 0.0
},
"recurring_prices": {
"unit": 4.49,
"unit_tax": 0.0,
"total": 4.49,
"total_tax": 0.0
}
}
},
"payment_details": null,
"environment": "production",
"messages": []
}
}

View File

@@ -1,188 +0,0 @@
{
"data": {
"public_checkout_id": "112231646-chre94eb195cbde-12dcba6761",
"type": "default",
"vendor": { "id": 128190, "name": "Streetwriters (Private) Limited" },
"display_currency": "USD",
"charge_currency": "USD",
"settings": {
"feature_flags": {
"show_save_payment_details_UI_without_taking_payment": false,
"show_save_payment_details_UI_and_take_payment": false,
"marketing_consent": true,
"customer_name": true,
"tax_code": false,
"tax_code_link": false,
"coupon": true,
"coupon_link": true,
"quantity_enabled": false,
"quantity_selection": false,
"hide_compliance_bar": false
},
"variant": "multipage",
"expires": "2021-12-07 23:59:59",
"marketing_consent_message": "<var vendorname>Streetwriters (Private) Limited</var> may send me product updates and offers via email. It is possible to opt-out at any time.",
"language_code": "en",
"disable_logout": false,
"styles": { "primary_colour": null, "theme": "light" },
"inline_styles": null
},
"customer": {
"id": 11363557,
"email": "enkaboot@gmail.com",
"country_code": "PK",
"postcode": null,
"remember_me": false,
"audience_opt_in": false
},
"items": [
{
"checkout_product_id": 116991610,
"product_id": 658759,
"name": "Notesnook Pro (Yearly)",
"custom_message": "",
"quantity": 1,
"allow_quantity": false,
"icon_url": "https://paddle.s3.amazonaws.com/user/128190/9v0fhULnQUCZzo3DMkJ8_android-chrome-512x512.png",
"prices": [
{
"currency": "USD",
"unit_price": {
"net": 49.99,
"gross": 49.99,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 49.99,
"gross_after_discount": 49.99,
"tax": 0.0,
"tax_after_discount": 0.0
},
"line_price": {
"net": 49.99,
"gross": 49.99,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 49.99,
"gross_after_discount": 49.99,
"tax": 0.0,
"tax_after_discount": 0.0
},
"discounts": [],
"tax_rate": 0.0
}
],
"recurring": {
"period": "year",
"interval": 1,
"trial_days": 0,
"prices": [
{
"currency": "USD",
"unit_price": {
"net": 49.99,
"gross": 49.99,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 49.99,
"gross_after_discount": 49.99,
"tax": 0.0,
"tax_after_discount": 0.0
},
"line_price": {
"net": 49.99,
"gross": 49.99,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 49.99,
"gross_after_discount": 49.99,
"tax": 0.0,
"tax_after_discount": 0.0
},
"discounts": [],
"tax_rate": 0.0
}
]
},
"webhook_url": null
}
],
"available_payment_methods": [
{
"type": "SPREEDLY_CARD",
"weight": 1,
"options": {
"api_key": "LIyzQnqLfedZ3oo6aWJFxSiqgfW",
"spreedly_environment_key": "LIyzQnqLfedZ3oo6aWJFxSiqgfW"
},
"seller_friendly_name": "card"
},
{
"type": "PAYPAL",
"weight": 2,
"options": [],
"seller_friendly_name": "paypal"
}
],
"total": [
{
"net": 49.99,
"gross": 49.99,
"net_discount": 0.0,
"gross_discount": 0.0,
"net_after_discount": 49.99,
"gross_after_discount": 49.99,
"tax": 0.0,
"tax_after_discount": 0.0,
"currency": "USD",
"is_free": false,
"includes_tax": false,
"tax_rate": 0.0
}
],
"pending_payment": false,
"completed": false,
"payment_method_type": null,
"flagged_for_review": false,
"ip_geo_country_code": "PK",
"tax": null,
"passthrough": "{}",
"redirect_url": null,
"tracking": {
"source_page": "http://localhost:3000/settings",
"test_name": null,
"test_variant": null,
"initial_request": {
"data": {
"type": "default",
"items": [{ "product_id": 658759 }],
"customer": { "email": "redacted" },
"settings": { "passthrough": "{}" },
"tracking": { "referrer": "localhost:3000 / localhost:3000" },
"parent_url": "http://localhost:3000/settings",
"geo_country": "PK"
}
}
},
"created_at": "2021-10-08 12:00:58",
"paddlejs": {
"vendor": {
"currency": "USD",
"prices": {
"unit": 49.99,
"unit_tax": 0.0,
"total": 49.99,
"total_tax": 0.0
},
"recurring_prices": {
"unit": 49.99,
"unit_tax": 0.0,
"total": 49.99,
"total_tax": 0.0
}
}
},
"payment_details": null,
"environment": "production",
"messages": []
}
}

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Text, Flex, Button, Box } from "rebass"; import { Text, Flex, Button, Box } from "rebass";
import Dialog from "./dialog"; import Dialog from "./dialog";
import * as Icon from "../icons"; import * as Icon from "../icons";
import { useStore as useUserStore } from "../../stores/user-store"; import { useStore as useUserStore } from "../../stores/user-store";
import { getCouponData, upgrade } from "../../common/checkout"; import { getPlans, inlineCheckout, upgrade } from "../../common/checkout";
import getSymbolFromCurrency from "currency-symbol-map"; import getSymbolFromCurrency from "currency-symbol-map";
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics"; import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
@@ -11,6 +11,9 @@ import Switch from "../switch";
import Modal from "react-modal"; import Modal from "react-modal";
import { useTheme } from "emotion-theming"; import { useTheme } from "emotion-theming";
import { ReactComponent as Rocket } from "../../assets/rocket.svg"; import { ReactComponent as Rocket } from "../../assets/rocket.svg";
import Loader from "../loader";
import Field from "../field";
import { useSessionState } from "../../utils/hooks";
const sections = [ const sections = [
{ {
@@ -251,51 +254,13 @@ const sections = [
}, },
]; ];
const plans = [
{
title: "Monthly",
subtitle: "Pay once a month",
price: 4.99,
currency: "USD",
},
{
title: "Yearly",
subtitle: "Pay once a year",
price: 49.99,
currency: "USD",
},
];
function BuyDialog(props) { function BuyDialog(props) {
const { couponCode } = props;
const [coupon, setCoupon] = useState(couponCode);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [prices, setPrices] = useState();
const [plan, setPlan] = useState(props.plan || "monthly");
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const user = useUserStore((store) => store.user); const user = useUserStore((store) => store.user);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const [selectedPlan, setSelectedPlan] = useState();
const [discount, setDiscount] = useState({ isApplyingCoupon: false });
const theme = useTheme(); const theme = useTheme();
useEffect(() => {
(async function () {
try {
setIsLoading(true);
setError();
const data = await getCouponData(coupon, plan);
setPrices({
...data.paddlejs.vendor,
withoutDiscount: data.total[0],
});
} catch (e) {
console.error(e);
setError(e.message);
} finally {
setIsLoading(false);
}
})();
}, [coupon, plan]);
useEffect(() => { useEffect(() => {
trackEvent(ANALYTICS_EVENTS.purchaseInitiated, "Buy dialog opened."); trackEvent(ANALYTICS_EVENTS.purchaseInitiated, "Buy dialog opened.");
}, []); }, []);
@@ -346,9 +311,8 @@ function BuyDialog(props) {
> >
<Flex <Flex
flexDirection="row" flexDirection="row"
width={"60%"} maxWidth={["100%", "80%", "60%"]}
// maxHeight={["100%", "80%", "70%"]} maxHeight={["100%", "80%", "80%"]}
height={["100%", "80%", "70%"]}
bg="transparent" bg="transparent"
alignSelf={"center"} alignSelf={"center"}
overflowY={props.scrollable ? "auto" : "hidden"} overflowY={props.scrollable ? "auto" : "hidden"}
@@ -363,8 +327,10 @@ function BuyDialog(props) {
flexDirection="column" flexDirection="column"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
flexShrink={0}
sx={{ sx={{
borderRadius: "dialog", borderTopLeftRadius: "dialog",
borderBottomLeftRadius: "dialog",
overflow: "hidden", overflow: "hidden",
bg: "bgTransparent", bg: "bgTransparent",
backdropFilter: "blur(8px)", backdropFilter: "blur(8px)",
@@ -373,63 +339,73 @@ function BuyDialog(props) {
p={4} p={4}
py={50} py={50}
> >
<Rocket width={200} /> {isLoggedIn ? (
<Text variant="heading" textAlign="center" mt={4}> selectedPlan ? (
Choose a plan <SelectedPlan
</Text> plan={{ ...selectedPlan, ...discount }}
<Text variant="body" textAlign="center" mt={1}> onPlanChangeRequest={() => setSelectedPlan()}
Every day we spend hours improving Notesnook. You are what makes onCouponApplied={(coupon) => {
that possible. setDiscount({ isApplyingCoupon: true });
</Text> setSelectedPlan((plan) => ({ ...plan, coupon }));
</Flex>
<Flex
flex={1}
flexDirection="column"
overflowY="auto"
sx={{ position: "relative" }}
pt={6}
bg="background"
justifyContent="center"
>
{plans.map((plan) => (
<Flex justifyContent="space-between" alignItems="center" p={6}>
<Text variant="heading" fontWeight="normal">
{plan.title}
<Text variant="subtitle" fontWeight="normal">
{plan.subtitle}
</Text>
</Text>
<Text variant="body" fontSize="subheading">
{plan.price} {plan.currency}
</Text>
</Flex>
))}
</Flex>
{/* <Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
sx={{
borderRadius: "dialog",
overflow: "hidden",
bg: "bgTransparent",
backdropFilter: "blur(8px)",
}} }}
width={350} />
p={4} ) : (
py={50} <ChooseAPlan
> selectedPlan={selectedPlan}
<Rocket width={200} /> onPlanChanged={(plan) => setSelectedPlan(plan)}
<Text variant="heading" textAlign="center" mt={4}> />
Notesnook Pro )
</Text> ) : (
<Text variant="body" textAlign="center" mt={1}> <TryForFree />
Ready to take the next step in your private note taking journey? )}
</Text>
<Button variant="primary" mt={4}>
Try free for 14 days
</Button>
</Flex> </Flex>
{selectedPlan ? (
<Checkout
user={user}
plan={selectedPlan}
onCheckoutLoaded={(data) => {
const pricingInfo = getPricingInfoFromCheckout(
selectedPlan,
data
);
console.log(pricingInfo, selectedPlan.coupon);
setDiscount(pricingInfo);
}}
/>
) : (
<FeaturesList />
)}
</Flex>
</Modal>
);
}
export default BuyDialog;
function RecurringPricing(props) {
const { plan } = props;
// if (product.prices.total === prices.recurring_prices.total) return null;
return (
<Text variant="body" fontSize="subBody">
<Text as="span" fontSize="subtitle">
<Price currency={plan.currency} price={plan.price} />
</Text>
{formatPeriod(plan.key)}
</Text>
);
}
function Price(props) {
const { currency, price } = props;
return (
<>
{getSymbolFromCurrency(currency)}
{price}
</>
);
}
function FeaturesList() {
return (
<Flex <Flex
flex={1} flex={1}
flexDirection="column" flexDirection="column"
@@ -499,241 +475,307 @@ function BuyDialog(props) {
)} )}
</Flex> </Flex>
))} ))}
</Flex> */}
</Flex> </Flex>
</Modal>
); );
}
/* <Flex flexDirection="column" flex={1} overflowY="hidden"> function Checkout({ user, plan, onCheckoutLoaded }) {
<Flex bg="primary" p={5} sx={{ position: "relative" }}> const [isLoading, setIsLoading] = useState(true);
<Text variant="heading" fontSize="38px" color="static"> useEffect(() => {
Notesnook Pro (async () => {
<Text setIsLoading(true);
variant="subBody" await inlineCheckout({
color="static" user,
opacity={1} plan: plan.key,
fontWeight="normal" coupon: plan.coupon,
fontSize="title" country: plan.country,
> onCheckoutLoaded,
Ready to take the next step on your private note taking journey? });
</Text> setIsLoading(false);
</Text> })();
<Text // eslint-disable-next-line react-hooks/exhaustive-deps
sx={{ position: "absolute", top: 0, right: 0 }} }, [plan, user]);
variant="heading"
color="static" return (
opacity={0.2}
fontSize={90}
>
PRO
</Text>
</Flex>
<Flex <Flex
flexDirection="column" bg="background"
px={5} width="500px"
pb={2} padding={40}
overflowY="auto" overflowY="auto"
sx={{ position: "relative" }} alignItems={"center"}
> >
{premiumDetails.map((item) => ( {isLoading ? (
<Flex mt={2}> <Loader
<Icon.Checkmark color="primary" size={16} /> title={
<Text variant="body" fontSize="title" ml={1}> plan.isApplyingCoupon
{item.title} ? "Applying coupon code. Please wait..."
: "Loading checkout. Please wait..."
}
/>
) : null}
<Box
flex={1}
className="checkout-container"
display={isLoading ? "none" : "block"}
/>
</Flex>
);
}
const CACHED_PLANS = [
{
key: "monthly",
title: "Monthly",
subtitle: `Pay once a month.`,
},
{
key: "yearly",
title: "Yearly",
subtitle: `Pay once a year.`,
},
];
function PlansList({ selectedPlan, onPlanChanged }) {
const [isLoading, setIsLoading] = useState(false);
const [plans, setPlans] = useSessionState("PlansList:plans", CACHED_PLANS);
useEffect(() => {
if (plans && plans !== CACHED_PLANS) return;
(async function () {
try {
setIsLoading(true);
let plans = await getPlans();
plans = plans.map((product) => {
return {
key: `${product.subscription.interval}ly`,
country: product.customer_country,
currency: product.currency,
price: product.price.net,
id: product.product_id,
title:
product.subscription.interval === "month" ? "Monthly" : "Yearly",
subtitle: `Pay once a ${product.subscription.interval}.`,
};
});
setPlans(plans);
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
})();
}, []);
return plans.map((plan) => (
<Button
disabled={isLoading}
variant="tool"
display="flex"
textAlign="start"
justifyContent="space-between"
alignItems="center"
flex={1}
mt={1}
sx={{
bg: selectedPlan?.key === plan.key ? "border" : "transparent",
border:
selectedPlan?.key === plan.key ? "1px solid var(--primary)" : "none",
}}
onClick={() => onPlanChanged(plan)}
>
<Text variant="subtitle" fontWeight="normal">
{plan.title}
<Text variant="body" fontWeight="normal" color="fontTertiary">
{plan.subtitle}
</Text>
</Text>
{isLoading ? <Icon.Loading /> : <RecurringPricing plan={plan} />}
</Button>
));
}
function ChooseAPlan({ selectedPlan, onPlanChanged }) {
return (
<>
<Rocket width={200} />
<Text variant="heading" textAlign="center" mt={4}>
Choose a plan
</Text>
<Text variant="body" textAlign="center" mt={1}>
Every day we spend hours improving Notesnook. You are what makes that
possible.
</Text>
<Flex flexDirection="column" alignSelf="stretch" mt={2}>
<PlansList selectedPlan={selectedPlan} onPlanChanged={onPlanChanged} />
</Flex>
</>
);
}
function TryForFree() {
return (
<>
<Rocket width={200} />
<Text variant="heading" textAlign="center" mt={4}>
Notesnook Pro
</Text>
<Text variant="body" textAlign="center" mt={1}>
Ready to take the next step in your private note taking journey?
</Text>
<Button variant="primary" mt={4}>
Try free for 14 days
</Button>
</>
);
}
function SelectedPlan({ plan, onPlanChangeRequest, onCouponApplied }) {
console.log("SELECTED", plan);
useEffect(() => {
const couponInput = document.getElementById("coupon");
couponInput.value = plan.coupon ? plan.coupon : couponInput.value;
}, [plan.coupon]);
return (
<>
<Rocket width={200} />
<Text variant="heading" textAlign="center" mt={4}>
Notesnook Pro
</Text>
<Text variant="body" fontSize="subheading" textAlign="center" mt={1}>
{plan.title}
</Text>
<Field
variant={plan.isInvalidCoupon ? "error" : "input"}
sx={{ alignSelf: "stretch", my: 2 }}
styles={{ input: { fontSize: "body" } }}
id="coupon"
name="coupon"
placeholder="Coupon code"
autoFocus={plan.isInvalidCoupon}
disabled={!!plan.coupon}
onKeyUp={(e) => {
if (e.code === "Enter") {
const couponInput = document.getElementById("coupon");
if (!!plan.coupon) couponInput.value = "";
onCouponApplied(couponInput.value);
}
}}
action={{
icon: plan.isApplyingCoupon
? Icon.Loading
: plan.coupon
? Icon.Cross
: Icon.Check,
onClick: () => {
const couponInput = document.getElementById("coupon");
if (!!plan.coupon) couponInput.value = "";
onCouponApplied(couponInput.value);
},
}}
/>
<CheckoutPricing
currency={plan.currency}
period={plan.key}
subtotal={plan.price}
discountedPrice={plan.discount || 0.0}
isRecurringDiscount={plan.isRecurringDiscount}
/>
<Button variant="secondary" mt={4} px={4} onClick={onPlanChangeRequest}>
Change plan
</Button>
</>
);
}
function CheckoutPricing({
currency,
period,
subtotal,
discountedPrice,
isRecurringDiscount,
}) {
const fields = [
{
key: "subtotal",
label: "Subtotal",
value: formatPrice(currency, subtotal.toFixed(2)),
},
{
key: "discount",
label: "Discount",
color: "primary",
value: formatPrice(
currency,
discountedPrice.toFixed(2),
null,
discountedPrice > 0
),
},
];
return (
<>
{fields.map((field) => (
<Flex
key={field.key}
justifyContent="space-between"
alignSelf="stretch"
mt={1}
>
<Text variant="body" fontSize="subtitle">
{field.label}
</Text>
<Text
variant="body"
fontSize="subtitle"
color={field.color || "text"}
>
{field.value}
</Text> </Text>
</Flex> </Flex>
))} ))}
</Flex> <Flex justifyContent="space-between" alignSelf="stretch" mt={1}>
<Text variant="body" fontSize="heading">
<Flex Total
flexDirection="column"
bg={error ? "errorBg" : "shade"}
p={5}
pt={2}
>
{isLoading ? (
<Icon.Loading size={32} />
) : error ? (
<>
<Text variant="title" color="error" fontWeight="normal">
{error}
</Text>
<Button
variant="primary"
bg="error"
color="static"
mt={2}
onClick={() => setCoupon()}
>
Try again
</Button>
</>
) : (
<>
<Flex justifyContent="center" alignItems="center">
<Text variant="body" mr={1}>
Monthly
</Text>
<Switch
checked={plan !== "monthly"}
onClick={() => {
setPlan((s) => (s === "monthly" ? "yearly" : "monthly"));
}}
/>
<Text variant="body" ml={1}>
Yearly
</Text>
</Flex>
{coupon ? (
<Flex mb={1} alignItems="center" justifyContent="space-between">
<Flex alignItems="center" justifyContent="center">
<Text variant="subBody" mr={1}>
Coupon:
</Text>
<Text
variant="subBody"
fontSize={10}
bg="primary"
color="static"
px={1}
py="3px"
sx={{ borderRadius: "default" }}
>
{coupon}
</Text>
</Flex>
<Flex>
<Text
sx={{ cursor: "pointer", ":hover": { opacity: 0.8 } }}
variant="subBody"
color="primary"
mr={1}
onClick={() => {
const code = window.prompt("Enter new coupon code:");
setCoupon(code);
}}
>
Change
</Text>
<Text
sx={{ cursor: "pointer", ":hover": { opacity: 0.8 } }}
variant="subBody"
color="error"
onClick={() => {
if (
window.confirm(
"Are you sure you want to remove this coupon?"
)
) {
setCoupon();
}
}}
>
Remove
</Text>
</Flex>
</Flex>
) : (
<Text
sx={{ cursor: "pointer", ":hover": { opacity: 0.8 } }}
variant="subBody"
color="primary"
mr={1}
onClick={() => {
const code = window.prompt("Enter new coupon code:");
setCoupon(code);
}}
>
Add coupon code
</Text> </Text>
<Text variant="body" fontSize="heading" color={"text"} textAlign="end">
{formatPrice(
currency,
subtotal - discountedPrice,
isRecurringDiscount || discountedPrice <= 0 ? period : "null"
)} )}
<Text variant="heading" fontSize={24} color="primary"> <Text fontSize="body">
Only <MainPricing prices={prices} plan={plan} /> {isRecurringDiscount || discountedPrice <= 0
? ""
: `then ${formatPrice(currency, subtotal, period)}`}
</Text> </Text>
<RecurringPricing prices={prices} plan={plan} />
<Text display="flex" variant="subBody" color="text" mt={1} mb={2}>
Cancel anytime. No questions asked.
</Text> </Text>
<Button
fontSize="title"
fontWeight="bold"
onClick={async () => {
if (isLoggedIn) {
await upgrade(user, coupon, plan);
} else {
navigate(`/login`, { redirect: `/#/buy/${coupon}` });
}
props.onCancel();
}}
>
Subscribe to Notesnook Pro
</Button>
</>
)}
</Flex> </Flex>
</Flex> */
// </Dialog>
// );
}
export default BuyDialog;
function MainPricing(props) {
const { prices, plan } = props;
if (prices.withoutDiscount.net !== prices.prices.total)
return (
<>
<del>
<Price
currency={prices.withoutDiscount.currency}
price={prices.withoutDiscount.net}
/>
</del>{" "}
<Price currency={prices.currency} price={prices.prices.total} />
{prices.recurring_prices.total === prices.prices.total
? ` per ${planToPeriod(plan)}`
: ` your first ${planToPeriod(plan)}`}
</>
);
else
return (
<>
<Price currency={prices.currency} price={prices.prices.total} />
{` per ${planToPeriod(plan)}`}
</> </>
); );
} }
function RecurringPricing(props) { function formatPrice(currency, price, period, negative = false) {
const { prices, plan } = props; return `${negative ? "-" : ""}${getSymbolFromCurrency(
currency
if (prices.prices.total === prices.recurring_prices.total) return null; )}${price}${formatPeriod(period)}`;
return (
<Text
display="flex"
variant="body"
mt={1}
fontWeight="bold"
color="primary"
>
And then{" "}
<Price currency={prices.currency} price={prices.recurring_prices.total} />{" "}
every {planToPeriod(plan)} afterwards.
</Text>
);
} }
function Price(props) { function formatPeriod(period) {
const { currency, price } = props; return period === "monthly" ? "/mo" : period === "yearly" ? "/yr" : "";
return (
<>
{getSymbolFromCurrency(currency)}
{price}
</>
);
} }
function planToPeriod(plan) { function getPricingInfoFromCheckout(plan, eventData) {
return plan === "monthly" ? "month" : "year"; const { checkout } = eventData;
const { prices, recurring_prices, coupon } = checkout;
const price = parseFloat(prices.customer.total);
const recurringPrice = parseFloat(recurring_prices.customer.total);
return {
price: plan.price,
discount: plan.price - price,
coupon: coupon.coupon_code,
isRecurringDiscount: coupon.coupon_code && price === recurringPrice,
isInvalidCoupon: !!plan.coupon !== !!coupon.coupon_code,
};
} }

View File

@@ -48,6 +48,7 @@ function Field(props) {
placeholder, placeholder,
validatePassword, validatePassword,
onError, onError,
variant,
} = props; } = props;
const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [rules, setRules] = useState(passwordValidationRules); const [rules, setRules] = useState(passwordValidationRules);
@@ -83,6 +84,7 @@ function Field(props) {
<Flex mt={1} sx={{ position: "relative" }}> <Flex mt={1} sx={{ position: "relative" }}>
<Input <Input
data-test-id={props["data-test-id"]} data-test-id={props["data-test-id"]}
variant={variant}
defaultValue={defaultValue} defaultValue={defaultValue}
ref={inputRef} ref={inputRef}
autoFocus={autoFocus} autoFocus={autoFocus}

View File

@@ -7,6 +7,7 @@ class DarkColorSchemeFactory {
primary: accent, primary: accent,
placeholder: hexToRGB("#ffffff", 0.6), placeholder: hexToRGB("#ffffff", 0.6),
background: "#1f1f1f", background: "#1f1f1f",
bgTransparent: "#1f1f1f99",
accent: "#000", accent: "#000",
bgSecondary: "#2b2b2b", bgSecondary: "#2b2b2b",
bgSecondaryText: "#A1A1A1", bgSecondaryText: "#A1A1A1",

View File

@@ -22,7 +22,7 @@ class Default {
color: "text", color: "text",
outline: "none", outline: "none",
":focus": { ":focus": {
boxShadow: "0px 0px 0px 2px var(--primary) inset", boxShadow: "0px 0px 0px 1.5px var(--primary) inset",
}, },
":hover:not(:focus)": { ":hover:not(:focus)": {
boxShadow: "0px 0px 0px 1px var(--dimPrimary) inset", boxShadow: "0px 0px 0px 1px var(--dimPrimary) inset",
@@ -51,13 +51,13 @@ class Error {
constructor() { constructor() {
return { return {
variant: "forms.input", variant: "forms.input",
borderColor: "red", boxShadow: "0px 0px 0px 1px var(--error) inset",
":focus": {
outline: "none", outline: "none",
borderColor: "red", ":focus": {
boxShadow: "0px 0px 0px 1.5px var(--error) inset",
}, },
":hover": { ":hover:not(:focus)": {
borderColor: "darkred", boxShadow: "0px 0px 0px 1px var(--error) inset",
}, },
}; };
} }