mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 22:49:45 +01:00
feat: finalize ne checkout/upgrade dialog ui
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user