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",
"event-source-polyfill": "^1.0.25",
"fast-sort": "^2.1.1",
"fetch-jsonp": "^1.2.1",
"file-saver": "^2.0.5",
"framer-motion": "^4.1.17",
"hash-wasm": "^4.9.0",

View File

@@ -6,7 +6,7 @@
/security#csp-meta-tag -->
<meta
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
rel="apple-touch-icon"

View File

@@ -1,10 +1,16 @@
import { ANALYTICS_EVENTS, trackEvent } from "../utils/analytics";
import DEFAULT_DATA_MONTHLY from "./monthly.json";
import DEFAULT_DATA_YEARLY from "./yearly.json";
import fetchJsonp from "fetch-jsonp";
import Config from "../utils/config";
const isDev = false; // process.env.NODE_ENV === "development";
const VENDOR_ID = isDev ? 1506 : 128190;
/**
*
* @param {"yearly"|"monthly"} plan
* @returns
*/
const PRODUCT_ID = (plan) => {
if (isDev) return plan === "monthly" ? 9822 : 0;
else return plan === "monthly" ? 648884 : 658759;
@@ -12,6 +18,15 @@ const PRODUCT_ID = (plan) => {
function loadPaddle(eventCallback) {
return new Promise((resolve) => {
if (window.Paddle) {
window.Paddle.Options({
vendor: VENDOR_ID,
eventCallback,
});
resolve();
return;
}
var script = document.createElement("script");
script.src = "https://cdn.paddle.com/paddle/paddle.js";
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) {
if (!window.Paddle) {
await loadPaddle();
@@ -66,35 +128,18 @@ async function openPaddleDialog(overrideUrl) {
});
}
async function getCouponData(coupon, plan) {
let url =
plan === "monthly"
? "https://checkout-service.paddle.com/checkout/1122-chree0325aa1705-38b9ffa5ce/coupon"
: "https://checkout-service.paddle.com/checkout/1122-chre94eb195cbde-12dcba6761/coupon";
try {
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",
async function getPlans() {
const monthlyProductId = PRODUCT_ID("monthly");
const yearlyProductId = PRODUCT_ID("yearly");
const url = `https://checkout.paddle.com/api/2.0/prices?product_ids=${yearlyProductId},${monthlyProductId}&callback=getPrices`;
const response = await fetchJsonp(url, {
jsonpCallback: "callback",
jsonpCallbackFunction: "getPrices",
});
const json = await response.json();
if (response.ok) {
return json.data;
} else {
throw new Error(json.errors[0].details);
}
} catch (e) {
console.error("Error: ", e);
return plan === "monthly"
? DEFAULT_DATA_MONTHLY.data
: DEFAULT_DATA_YEARLY.data;
}
if (!json || !json.success || !json.response?.products?.length)
throw new Error("Failed to get prices.");
return json.response.products;
}
export { upgrade, openPaddleDialog, getCouponData };
export { upgrade, openPaddleDialog, getPlans, inlineCheckout };

View File

@@ -18,22 +18,22 @@ async function initializeDatabase() {
db = new Database(Storage, EventSource, FS);
// if (isTesting()) {
db.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
});
// db.host({
// API_HOST: "https://api.notesnook.com",
// AUTH_HOST: "https://auth.streetwriters.co",
// SSE_HOST: "https://events.streetwriters.co",
// });
// } else {
// db.host({
// API_HOST: "http://localhost:5264",
// AUTH_HOST: "http://localhost:8264",
// SSE_HOST: "http://localhost:7264",
// });
// db.host({
// API_HOST: "http://192.168.10.29:5264",
// AUTH_HOST: "http://192.168.10.29:8264",
// SSE_HOST: "http://192.168.10.29:7264",
// });
db.host({
API_HOST: "http://192.168.10.29:5264",
AUTH_HOST: "http://192.168.10.29:8264",
SSE_HOST: "http://192.168.10.29:7264",
});
// }
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 Dialog from "./dialog";
import * as Icon from "../icons";
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 { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
import { navigate } from "../../navigation";
@@ -11,6 +11,9 @@ import Switch from "../switch";
import Modal from "react-modal";
import { useTheme } from "emotion-theming";
import { ReactComponent as Rocket } from "../../assets/rocket.svg";
import Loader from "../loader";
import Field from "../field";
import { useSessionState } from "../../utils/hooks";
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) {
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 isLoggedIn = useUserStore((store) => store.isLoggedIn);
const [selectedPlan, setSelectedPlan] = useState();
const [discount, setDiscount] = useState({ isApplyingCoupon: false });
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(() => {
trackEvent(ANALYTICS_EVENTS.purchaseInitiated, "Buy dialog opened.");
}, []);
@@ -346,9 +311,8 @@ function BuyDialog(props) {
>
<Flex
flexDirection="row"
width={"60%"}
// maxHeight={["100%", "80%", "70%"]}
height={["100%", "80%", "70%"]}
maxWidth={["100%", "80%", "60%"]}
maxHeight={["100%", "80%", "80%"]}
bg="transparent"
alignSelf={"center"}
overflowY={props.scrollable ? "auto" : "hidden"}
@@ -363,8 +327,10 @@ function BuyDialog(props) {
flexDirection="column"
justifyContent="center"
alignItems="center"
flexShrink={0}
sx={{
borderRadius: "dialog",
borderTopLeftRadius: "dialog",
borderBottomLeftRadius: "dialog",
overflow: "hidden",
bg: "bgTransparent",
backdropFilter: "blur(8px)",
@@ -373,63 +339,73 @@ function BuyDialog(props) {
p={4}
py={50}
>
<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>
<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)",
{isLoggedIn ? (
selectedPlan ? (
<SelectedPlan
plan={{ ...selectedPlan, ...discount }}
onPlanChangeRequest={() => setSelectedPlan()}
onCouponApplied={(coupon) => {
setDiscount({ isApplyingCoupon: true });
setSelectedPlan((plan) => ({ ...plan, coupon }));
}}
width={350}
p={4}
py={50}
>
<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>
/>
) : (
<ChooseAPlan
selectedPlan={selectedPlan}
onPlanChanged={(plan) => setSelectedPlan(plan)}
/>
)
) : (
<TryForFree />
)}
</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={1}
flexDirection="column"
@@ -499,241 +475,307 @@ function BuyDialog(props) {
)}
</Flex>
))}
</Flex> */}
</Flex>
</Modal>
);
}
/* <Flex flexDirection="column" flex={1} overflowY="hidden">
<Flex bg="primary" p={5} sx={{ position: "relative" }}>
<Text variant="heading" fontSize="38px" color="static">
Notesnook Pro
<Text
variant="subBody"
color="static"
opacity={1}
fontWeight="normal"
fontSize="title"
>
Ready to take the next step on your private note taking journey?
</Text>
</Text>
<Text
sx={{ position: "absolute", top: 0, right: 0 }}
variant="heading"
color="static"
opacity={0.2}
fontSize={90}
>
PRO
</Text>
</Flex>
function Checkout({ user, plan, onCheckoutLoaded }) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
(async () => {
setIsLoading(true);
await inlineCheckout({
user,
plan: plan.key,
coupon: plan.coupon,
country: plan.country,
onCheckoutLoaded,
});
setIsLoading(false);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [plan, user]);
return (
<Flex
flexDirection="column"
px={5}
pb={2}
bg="background"
width="500px"
padding={40}
overflowY="auto"
sx={{ position: "relative" }}
alignItems={"center"}
>
{premiumDetails.map((item) => (
<Flex mt={2}>
<Icon.Checkmark color="primary" size={16} />
<Text variant="body" fontSize="title" ml={1}>
{item.title}
{isLoading ? (
<Loader
title={
plan.isApplyingCoupon
? "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>
</Flex>
))}
</Flex>
<Flex
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
<Flex justifyContent="space-between" alignSelf="stretch" mt={1}>
<Text variant="body" fontSize="heading">
Total
</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">
Only <MainPricing prices={prices} plan={plan} />
<Text fontSize="body">
{isRecurringDiscount || discountedPrice <= 0
? ""
: `then ${formatPrice(currency, subtotal, period)}`}
</Text>
<RecurringPricing prices={prices} plan={plan} />
<Text display="flex" variant="subBody" color="text" mt={1} mb={2}>
Cancel anytime. No questions asked.
</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> */
// </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) {
const { prices, plan } = props;
if (prices.prices.total === prices.recurring_prices.total) return null;
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 formatPrice(currency, price, period, negative = false) {
return `${negative ? "-" : ""}${getSymbolFromCurrency(
currency
)}${price}${formatPeriod(period)}`;
}
function Price(props) {
const { currency, price } = props;
return (
<>
{getSymbolFromCurrency(currency)}
{price}
</>
);
function formatPeriod(period) {
return period === "monthly" ? "/mo" : period === "yearly" ? "/yr" : "";
}
function planToPeriod(plan) {
return plan === "monthly" ? "month" : "year";
function getPricingInfoFromCheckout(plan, eventData) {
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,
validatePassword,
onError,
variant,
} = props;
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [rules, setRules] = useState(passwordValidationRules);
@@ -83,6 +84,7 @@ function Field(props) {
<Flex mt={1} sx={{ position: "relative" }}>
<Input
data-test-id={props["data-test-id"]}
variant={variant}
defaultValue={defaultValue}
ref={inputRef}
autoFocus={autoFocus}

View File

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

View File

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