From c51da9be0fff2f71631467979471aacbf24c8c24 Mon Sep 17 00:00:00 2001 From: EpokK Date: Sun, 24 Nov 2019 16:43:36 +0100 Subject: [PATCH 001/242] Starting --- .../subscription/SubscriptionTable.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 containers/payments/subscription/SubscriptionTable.js diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js new file mode 100644 index 000000000..9052e070d --- /dev/null +++ b/containers/payments/subscription/SubscriptionTable.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useToggle, Button, classnames, LinkButton } from 'react-components'; +import { c } from 'ttag'; + +const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularIndex = 0 }) => { + const { state: showAllFeatures, toggle } = useToggle(); + + return ( +
+
+ {plans.map(({ planName, price, imageSrc, description, features = [] }, index) => { + return ( +
+ {index === mostPopularIndex ? ( +
{c('Title for subscription plan').t`Most popular`}
+ ) : null} +
{planName}
+
{price}
+
+ {planName} +
+

{description}

+
    + {features.map((feature, index) => { + return
  • {feature}
  • ; + })} +
+
+ +
+
+ ); + })} +
+ {showAllFeatures ? ( +
+ {c('Action').t`Compare all features`} +
+ ) : null} +
+ ); +}; + +SubscriptionTable.propTypes = { + plans: PropTypes.arrayOf( + PropTypes.shape({ + planName: PropTypes.string, + price: PropTypes.node, + imageSrc: PropTypes.string, + description: PropTypes.node, + features: PropTypes.arrayOf(PropTypes.node) + }) + ), + onSelect: PropTypes.func.isRequired, + currentPlanIndex: PropTypes.number, + mostPopularIndex: PropTypes.number +}; + +export default SubscriptionTable; From 486cc334f4dc096c7cf78f3bea7f49bf03744360 Mon Sep 17 00:00:00 2001 From: EpokK Date: Sun, 24 Nov 2019 16:52:57 +0100 Subject: [PATCH 002/242] Continue --- .../subscription/SubscriptionTable.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 9052e070d..30b92d587 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -4,7 +4,7 @@ import { useToggle, Button, classnames, LinkButton } from 'react-components'; import { c } from 'ttag'; const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularIndex = 0 }) => { - const { state: showAllFeatures, toggle } = useToggle(); + const { state: showAllFeatures, toggle: toggleFeatures } = useToggle(false); return (
@@ -27,9 +27,16 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI

{description}

@@ -55,11 +62,16 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI SubscriptionTable.propTypes = { plans: PropTypes.arrayOf( PropTypes.shape({ - planName: PropTypes.string, - price: PropTypes.node, - imageSrc: PropTypes.string, - description: PropTypes.node, - features: PropTypes.arrayOf(PropTypes.node) + planName: PropTypes.string.isRequired, + price: PropTypes.node.isRequired, + imageSrc: PropTypes.string.isRequired, + description: PropTypes.node.isRequired, + features: PropTypes.arrayOf( + PropTypes.shape({ + feature: PropTypes.node.isRequired, + advanced: PropTypes.bool + }) + ) }) ), onSelect: PropTypes.func.isRequired, From e483293602cded82a0adfc9752721e9296ecc641 Mon Sep 17 00:00:00 2001 From: EpokK Date: Mon, 25 Nov 2019 14:26:58 +0100 Subject: [PATCH 003/242] Continue --- containers/payments/PlansSection.js | 18 +- containers/payments/PlansTable.js | 403 ------------------ .../subscription/MailSubscriptionTable.js | 82 ++++ .../subscription/SubscriptionPrices.js | 32 ++ .../subscription/VpnSubscriptionTable.js | 82 ++++ index.ts | 1 + 6 files changed, 206 insertions(+), 412 deletions(-) delete mode 100644 containers/payments/PlansTable.js create mode 100644 containers/payments/subscription/MailSubscriptionTable.js create mode 100644 containers/payments/subscription/SubscriptionPrices.js create mode 100644 containers/payments/subscription/VpnSubscriptionTable.js diff --git a/containers/payments/PlansSection.js b/containers/payments/PlansSection.js index 29507a4c4..291b8d332 100644 --- a/containers/payments/PlansSection.js +++ b/containers/payments/PlansSection.js @@ -23,8 +23,8 @@ import { getPlans, isBundleEligible } from 'proton-shared/lib/helpers/subscripti import { isLoyal } from 'proton-shared/lib/helpers/organization'; import SubscriptionModal from './subscription/SubscriptionModal'; +import MailSubscriptionTable from './subscription/MailSubscriptionTable'; import { mergePlansMap, getCheckParams } from './subscription/helpers'; -import PlansTable from './PlansTable'; const PlansSection = () => { const { call } = useEventManager(); @@ -128,15 +128,15 @@ const PlansSection = () => { ) : null} {Plans.length ?
{c('Info').t`You are currently subscribed to ${names}.`}
: null} - { + const { Name } = plans[planIndex]; + handleModal({ [Name]: 1 }); + }} />

* {c('Info concerning plan features').t`denotes customizable features`}

diff --git a/containers/payments/PlansTable.js b/containers/payments/PlansTable.js deleted file mode 100644 index 19b2a0bc3..000000000 --- a/containers/payments/PlansTable.js +++ /dev/null @@ -1,403 +0,0 @@ -import React from 'react'; -import { c } from 'ttag'; -import PropTypes from 'prop-types'; -import { SmallButton, Price, Icon, Info, Tooltip, useToggle, classnames } from 'react-components'; -import { getPlanName } from 'proton-shared/lib/helpers/subscription'; -import { CYCLE, DEFAULT_CURRENCY, DEFAULT_CYCLE, PLANS } from 'proton-shared/lib/constants'; - -import CurrencySelector from './CurrencySelector'; -import CycleSelector from './CycleSelector'; - -const { MONTHLY, YEARLY, TWO_YEARS } = CYCLE; -const { PLUS, PROFESSIONAL, VISIONARY } = PLANS; -const FREE = 'free'; -const PLAN_NUMBERS = { - [FREE]: 1, - [PLUS]: 2, - [PROFESSIONAL]: 3, - [VISIONARY]: 4 -}; - -const PlansTable = ({ - user = {}, - subscription = {}, - plans = [], - currency = DEFAULT_CURRENCY, - cycle = DEFAULT_CYCLE, - updateCurrency, - updateCycle, - onSelect -}) => { - const planName = getPlanName(subscription) || FREE; - const { hasPaidVpn } = user; - const { state, toggle } = useToggle(); - const mySubscription = c('Title').t`My subscription`; - - const getPrice = (planName) => { - const plan = plans.find(({ Name }) => Name === planName); - const monthlyPrice = ( - - {plan.Pricing[cycle] / cycle} - - ); - - if (cycle === MONTHLY) { - return monthlyPrice; - } - - const billedPrice = ( - - {plan.Pricing[cycle]} - - ); - - return ( - <> -
{monthlyPrice}
- {c('Info').jt`billed as ${billedPrice}`} - - ); - }; - - const addCycleTooltip = (comp) => { - if (cycle === TWO_YEARS) { - return comp; - } - - return {comp}; - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - - - - - - - - - - - - - - - {state ? null : ( - - - - - - - - )} - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - {state ? ( - - - - - - - - ) : null} - - - - - - - - - - - - - - - -
- - FREE - - PLUS - - PROFESSIONAL - - VISIONARY -
- {addCycleTooltip( -
-
- -
-
- -
-
- )} -
FREE{getPrice(PLUS)}{getPrice(PROFESSIONAL)}{getPrice(VISIONARY)}
{c( - 'Header' - ).t`Users`}111-5000*6
{c( - 'Header' - ).t`Email addresses`}15*5 / {c('X / user').t`user`}50
{c( - 'Header' - ).t`Storage capacity (GB)`}0.55*5 / {c('X / user').t`user`}20
- {c('Header').t`Messages per day`} - - 1501000{c('Plan option').t`Unlimited`}{c('Plan option').t`Unlimited`}
{c( - 'Header' - ).t`Folders`}3200{c('Plan option').t`Unlimited`}{c('Plan option').t`Unlimited`}
{c( - 'Header' - ).t`Labels`}3200{c('Plan option').t`Unlimited`}{c('Plan option').t`Unlimited`}
- {c('Header').t`Custom domains`} - - - - 1*2*10
- {c('Header').t`IMAP / SMTP support`} - - - - - - - - - -
{c( - 'Header' - ).t`Additional features`}{c('Plan option').t`Only basic email features`}{c('Plan option') - .t`Folders, Labels, Filters, Encrypted contacts, Auto-responder and more`}{c('Plan option') - .t`All Plus features, and catch-all email, multi-user support and more`}{c('Plan option') - .t`All Professional features, limited to 6 users, includes ProtonVPN`}
{c( - 'Header' - ).t`Encrypted contact details`} - - - - - - - -
{c( - 'Header' - ).t`Short address (@pm.me)`} - - - - - - - -
{c( - 'Header' - ).t`Auto-reply`} - - - - - - - -
{c( - 'Header' - ).t`Catch-all email`} - - - - - - - -
{c( - 'Header' - ).t`Multi-user support`} - - - - - - - -
{c( - 'Header' - ).t`Priority customer support`} - - - - - - - -
- ProtonVPN - - - - {hasPaidVpn ? c('Action').t`Edit VPN` : c('Action').t`Add VPN`} - - - - {hasPaidVpn ? c('Action').t`Edit VPN` : c('Action').t`Add VPN`} - - - - {hasPaidVpn ? c('Action').t`Edit VPN` : c('Action').t`Add VPN`} - - {c('Plan option').t`Included`}
- - {state ? c('Action').t`Hide additional features` : c('Action').t`Compare all features`} - - - - {planName === FREE ? c('Action').t`Update` : c('Action').t`Select`} - - - - {planName === PLUS ? c('Action').t`Update` : c('Action').t`Select`} - - - - {planName === PROFESSIONAL ? c('Action').t`Update` : c('Action').t`Select`} - - - - {planName === VISIONARY ? c('Action').t`Update` : c('Action').t`Select`} - -
- ); -}; - -PlansTable.propTypes = { - subscription: PropTypes.object, - currency: PropTypes.string, - cycle: PropTypes.number, - plans: PropTypes.array, - user: PropTypes.object, - updateCurrency: PropTypes.func, - updateCycle: PropTypes.func, - onSelect: PropTypes.func -}; - -export default PlansTable; diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js new file mode 100644 index 000000000..0221a0b6c --- /dev/null +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { SubscriptionTable } from 'react-components'; +import PropTypes from 'prop-types'; +import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; +import { getPlan } from 'proton-shared/lib/subscription'; +import { c } from 'ttag'; +import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; +import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; +import professionalPlanSvg from 'design-system/assets/img/pm-images/professional-plan.svg'; +import visionaryPlanSvg from 'design-system/assets/img/pm-images/visionary-plan.svg'; + +import SubscriptionPrices from './SubscriptionPrices'; + +const INDEXES = { + [PLANS.PLUS]: 1, + [PLANS.PROFESSIONAL]: 2, + [PLANS.VISIONARY]: 3 +}; + +const FREE_PLAN = { + Pricing: { + [CYCLE.MONTHLY]: 0, + [CYCLE.YEARLY]: 0, + [CYCLE.TWO_YEARS]: 0 + } +}; + +const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, currency, onSelect }) => { + const plusPlan = apiPlans.find(({ Name }) => Name === PLANS.PLUS); + const professionalPlan = apiPlans.find(({ Name }) => Name === PLANS.PROFESSIONAL); + const visionaryPlan = apiPlans.find(({ Name }) => Name === PLANS.VISIONARY); + const plans = [ + { + planName: 'Free', + price: , + imageSrc: freePlanSvg, + description: c('Description').t`The basics for private and secure communications`, + features: [] + }, + plusPlan && { + planName: PLAN_NAMES.PLUS, + price: , + imageSrc: plusPlanSvg, + description: c('Description').t`Full-featured mailbox with advanced protection`, + features: [] + }, + professionalPlan && { + planName: PLAN_NAMES.PROFESSIONAL, + price: , + imageSrc: professionalPlanSvg, + description: c('Description').t`ProtonMail for professionals and businesses`, + features: [] + }, + visionaryPlan && { + planName: PLAN_NAMES.VISIONARY, + price: , + imageSrc: visionaryPlanSvg, + description: c('Description').t`ProtonMail for families and small businesses`, + features: [] + } + ]; + const { Name } = getPlan(subscription); + + return ( + + ); +}; + +MailSubscriptionTable.propTypes = { + subscription: PropTypes.object, + plans: PropTypes.arrayOf(PropTypes.object), + onSelect: PropTypes.func.isRequired, + cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, + currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired +}; + +export default MailSubscriptionTable; diff --git a/containers/payments/subscription/SubscriptionPrices.js b/containers/payments/subscription/SubscriptionPrices.js new file mode 100644 index 000000000..dc6ed3b0c --- /dev/null +++ b/containers/payments/subscription/SubscriptionPrices.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Price } from 'react-components'; +import { CYCLE } from 'proton-shared/lib/constants'; +import { c } from 'ttag'; + +const SubscriptionPrices = ({ cycle, currency, plan }) => { + const billiedAmount = ( + + {plan.Pricing[cycle]} + + ); + return ( + <> + + {plan.Pricing[cycle] / cycle} + + {cycle === CYCLE.YEARLY && {c('Details').jt`Billed as ${billiedAmount} per year`}} + {cycle === CYCLE.TWO_YEARS && {c('Details').jt`Billed as ${billiedAmount} every 2 year`}} + + ); +}; + +SubscriptionPrices.propTypes = { + cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, + currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired, + plan: PropTypes.shape({ + Pricing: PropTypes.object + }).isRequired +}; + +export default SubscriptionPrices; diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js new file mode 100644 index 000000000..56743796b --- /dev/null +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { SubscriptionTable } from 'react-components'; +import PropTypes from 'prop-types'; +import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; +import { getPlan } from 'proton-shared/lib/subscription'; +import { c } from 'ttag'; +import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; +import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; +import professionalPlanSvg from 'design-system/assets/img/pm-images/professional-plan.svg'; +import visionaryPlanSvg from 'design-system/assets/img/pm-images/visionary-plan.svg'; + +import SubscriptionPrices from './SubscriptionPrices'; + +const INDEXES = { + [PLANS.VPNBASIC]: 1, + [PLANS.VPNPLUS]: 2, + [PLANS.VISIONARY]: 3 +}; + +const FREE_PLAN = { + Pricing: { + [CYCLE.MONTHLY]: 0, + [CYCLE.YEARLY]: 0, + [CYCLE.TWO_YEARS]: 0 + } +}; + +const VpnSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, currency, onSelect }) => { + const vpnBasicPlan = apiPlans.find(({ Name }) => Name === PLANS.VPNBASIC); + const vpnPlusPlan = apiPlans.find(({ Name }) => Name === PLANS.PROFESSIONAL); + const visionaryPlan = apiPlans.find(({ Name }) => Name === PLANS.VISIONARY); + const plans = [ + { + planName: 'Free', + price: , + imageSrc: freePlanSvg, + description: c('Description').t`The basics for private and secure communications`, + features: [] + }, + vpnBasicPlan && { + planName: PLAN_NAMES.PLUS, + price: , + imageSrc: plusPlanSvg, + description: c('Description').t`Full-featured mailbox with advanced protection`, + features: [] + }, + vpnPlusPlan && { + planName: PLAN_NAMES.PROFESSIONAL, + price: , + imageSrc: professionalPlanSvg, + description: c('Description').t`ProtonMail for professionals and businesses`, + features: [] + }, + visionaryPlan && { + planName: PLAN_NAMES.VISIONARY, + price: , + imageSrc: visionaryPlanSvg, + description: c('Description').t`ProtonMail for families and small businesses`, + features: [] + } + ]; + const { Name } = getPlan(subscription); + + return ( + + ); +}; + +VpnSubscriptionTable.propTypes = { + subscription: PropTypes.object, + plans: PropTypes.arrayOf(PropTypes.object), + onSelect: PropTypes.func.isRequired, + cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, + currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired +}; + +export default VpnSubscriptionTable; diff --git a/index.ts b/index.ts index 0b49ad45c..0794b7231 100644 --- a/index.ts +++ b/index.ts @@ -181,6 +181,7 @@ export { default as MailBlackFridayModal } from './containers/payments/subscript export { default as VPNBlackFridayModal } from './containers/payments/subscription/VPNBlackFridayModal'; export { default as BlackFridayNavbarLink } from './containers/payments/subscription/BlackFridayNavbarLink'; export { default as UpgradeModal } from './containers/payments/subscription/UpgradeModal'; +export { default as SubscriptionTable } from './containers/payments/subscription/SubscriptionTable'; export { default as AmountButton } from './containers/payments/AmountButton'; export { default as Bitcoin } from './containers/payments/Bitcoin'; export { default as CurrencySelector } from './containers/payments/CurrencySelector'; From 6e0270f5a2d95852309af5c87689e6ca71864c88 Mon Sep 17 00:00:00 2001 From: EpokK Date: Mon, 25 Nov 2019 14:32:25 +0100 Subject: [PATCH 004/242] Export components --- index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.ts b/index.ts index 0794b7231..aae390951 100644 --- a/index.ts +++ b/index.ts @@ -182,6 +182,8 @@ export { default as VPNBlackFridayModal } from './containers/payments/subscripti export { default as BlackFridayNavbarLink } from './containers/payments/subscription/BlackFridayNavbarLink'; export { default as UpgradeModal } from './containers/payments/subscription/UpgradeModal'; export { default as SubscriptionTable } from './containers/payments/subscription/SubscriptionTable'; +export { default as MailSubscriptionTable } from './containers/payments/subscription/MailSubscriptionTable'; +export { default as VpnSubscriptionTable } from './containers/payments/subscription/VpnSubscriptionTable'; export { default as AmountButton } from './containers/payments/AmountButton'; export { default as Bitcoin } from './containers/payments/Bitcoin'; export { default as CurrencySelector } from './containers/payments/CurrencySelector'; From 29133a8f538e75ee096da4f3dea44942a036f81b Mon Sep 17 00:00:00 2001 From: EpokK Date: Mon, 25 Nov 2019 14:49:57 +0100 Subject: [PATCH 005/242] Review --- .../subscription/MailSubscriptionTable.js | 8 ++++---- .../subscription/SubscriptionTable.js | 19 ++++++++++--------- .../subscription/VpnSubscriptionTable.js | 8 ++++---- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 0221a0b6c..319eb7aa8 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -2,7 +2,7 @@ import React from 'react'; import { SubscriptionTable } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; -import { getPlan } from 'proton-shared/lib/subscription'; +import { getPlan } from 'proton-shared/lib/helpers/subscription'; import { c } from 'ttag'; import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; @@ -38,21 +38,21 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, features: [] }, plusPlan && { - planName: PLAN_NAMES.PLUS, + planName: PLAN_NAMES[PLANS.PLUS], price: , imageSrc: plusPlanSvg, description: c('Description').t`Full-featured mailbox with advanced protection`, features: [] }, professionalPlan && { - planName: PLAN_NAMES.PROFESSIONAL, + planName: PLAN_NAMES[PLANS.PROFESSIONAL], price: , imageSrc: professionalPlanSvg, description: c('Description').t`ProtonMail for professionals and businesses`, features: [] }, visionaryPlan && { - planName: PLAN_NAMES.VISIONARY, + planName: PLAN_NAMES[PLANS.VISIONARY], price: , imageSrc: visionaryPlanSvg, description: c('Description').t`ProtonMail for families and small businesses`, diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 30b92d587..6f2685a98 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -8,19 +8,20 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI return (
-
+
{plans.map(({ planName, price, imageSrc, description, features = [] }, index) => { return (
{index === mostPopularIndex ? ( -
{c('Title for subscription plan').t`Most popular`}
+
{c('Title for subscription plan') + .t`Most popular`}
) : null} -
{planName}
+
{planName}
{price}
{planName} @@ -50,11 +51,11 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI ); })}
- {showAllFeatures ? ( -
- {c('Action').t`Compare all features`} -
- ) : null} +
+ + {showAllFeatures ? c('Action').t`Display less features` : c('Action').t`Compare all features`} + +
); }; diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index 56743796b..44f860856 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -2,7 +2,7 @@ import React from 'react'; import { SubscriptionTable } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; -import { getPlan } from 'proton-shared/lib/subscription'; +import { getPlan } from 'proton-shared/lib/helpers/subscription'; import { c } from 'ttag'; import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; @@ -38,21 +38,21 @@ const VpnSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, features: [] }, vpnBasicPlan && { - planName: PLAN_NAMES.PLUS, + planName: PLAN_NAMES[PLANS.VPNBASIC], price: , imageSrc: plusPlanSvg, description: c('Description').t`Full-featured mailbox with advanced protection`, features: [] }, vpnPlusPlan && { - planName: PLAN_NAMES.PROFESSIONAL, + planName: PLAN_NAMES[PLANS.VPNPLUS], price: , imageSrc: professionalPlanSvg, description: c('Description').t`ProtonMail for professionals and businesses`, features: [] }, visionaryPlan && { - planName: PLAN_NAMES.VISIONARY, + planName: PLAN_NAMES[PLANS.VISIONARY], price: , imageSrc: visionaryPlanSvg, description: c('Description').t`ProtonMail for families and small businesses`, From 814dadee20aee5066a761b710198922b6ffcdb2f Mon Sep 17 00:00:00 2001 From: EpokK Date: Mon, 25 Nov 2019 15:03:00 +0100 Subject: [PATCH 006/242] Review --- containers/payments/subscription/SubscriptionTable.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 6f2685a98..bbaacb0ee 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -8,7 +8,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI return (
-
+
{plans.map(({ planName, price, imageSrc, description, features = [] }, index) => { return (
{index === mostPopularIndex ? ( -
{c('Title for subscription plan') +
{c('Title for subscription plan') .t`Most popular`}
) : null} -
{planName}
+
{planName}
{price}
{planName} From 49f6247bd7c4aae83d9eb6ca1fd162c171f2ff0c Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 27 Nov 2019 06:35:40 +0100 Subject: [PATCH 007/242] Continue --- containers/payments/PlansSection.js | 22 ++++-- .../subscription/MailSubscriptionTable.js | 39 +++++++++- .../subscription/SubscriptionPrices.js | 10 ++- .../subscription/SubscriptionSection.js | 4 +- .../subscription/SubscriptionTable.js | 73 ++++++++++++------- .../subscription/SubscriptionTable.scss | 41 +++++++++++ 6 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 containers/payments/subscription/SubscriptionTable.scss diff --git a/containers/payments/PlansSection.js b/containers/payments/PlansSection.js index 291b8d332..e75853b3c 100644 --- a/containers/payments/PlansSection.js +++ b/containers/payments/PlansSection.js @@ -3,6 +3,8 @@ import { c } from 'ttag'; import { SubTitle, Alert, + CurrencySelector, + CycleSelector, DowngradeModal, LossLoyaltyModal, MozillaInfoPanel, @@ -121,13 +123,19 @@ const PlansSection = () => { return ( <> {c('Title').t`Plans`} - - {bundleEligible ? ( -
{c('Info') - .t`Get 20% bundle discount when you purchase ProtonMail and ProtonVPN together.`}
- ) : null} - {Plans.length ?
{c('Info').t`You are currently subscribed to ${names}.`}
: null} -
+
+ + {bundleEligible ? ( +
{c('Info') + .t`Get 20% bundle discount when you purchase ProtonMail and ProtonVPN together.`}
+ ) : null} + {Plans.length ?
{c('Info').t`You are currently subscribed to ${names}.`}
: null} +
+
+ + +
+
, imageSrc: freePlanSvg, description: c('Description').t`The basics for private and secure communications`, - features: [] + features: [ + { feature: c('Feature').t`1 user` }, + { feature: c('Feature').t`500 MB storage` }, + { feature: c('Feature').t`1 address` }, + { feature: c('Feature').t`No domain support` }, + { feature: c('Feature').t`ProtonVPN (optional) *` } + ] }, plusPlan && { planName: PLAN_NAMES[PLANS.PLUS], + canCustomize: true, price: , imageSrc: plusPlanSvg, description: c('Description').t`Full-featured mailbox with advanced protection`, - features: [] + features: [ + { feature: c('Feature').t`1 user` }, + { feature: c('Feature').t`5 GB storage *` }, + { feature: c('Feature').t`5 addresses *` }, + { feature: c('Feature').t`Supports 1 domain *` }, + { feature: c('Feature').t`Supports folder, labels, filters, auto-reply, IMAP/SMTP and more` }, + { feature: c('Feature').t`ProtonVPN (optional) *` } + ] }, professionalPlan && { planName: PLAN_NAMES[PLANS.PROFESSIONAL], + canCustomize: true, price: , imageSrc: professionalPlanSvg, description: c('Description').t`ProtonMail for professionals and businesses`, - features: [] + features: [ + { feature: c('Feature').t`1 - 5000 user *` }, + { feature: c('Feature').t`5 GB storage per user *` }, + { feature: c('Feature').t`5 addresses per user *` }, + { feature: c('Feature').t`Supports 2 domains *` }, + { feature: c('Feature').t`Catch all email, multi user management, priority support and more` }, + { feature: c('Feature').t`ProtonVPN (optional) *` } + ] }, visionaryPlan && { planName: PLAN_NAMES[PLANS.VISIONARY], + canCustomize: false, price: , imageSrc: visionaryPlanSvg, description: c('Description').t`ProtonMail for families and small businesses`, - features: [] + features: [ + { feature: c('Feature').t`6 users` }, + { feature: c('Feature').t`20 GB storage` }, + { feature: c('Feature').t`Supports 10 domains` }, + { feature: c('Feature').t`Includes all features` }, + { feature: c('Feature').t`Priority support` }, + { feature: c('Feature').t`Includes ProtonVPN` } + ] } ]; const { Name } = getPlan(subscription); diff --git a/containers/payments/subscription/SubscriptionPrices.js b/containers/payments/subscription/SubscriptionPrices.js index dc6ed3b0c..0a0d27a3f 100644 --- a/containers/payments/subscription/SubscriptionPrices.js +++ b/containers/payments/subscription/SubscriptionPrices.js @@ -12,11 +12,15 @@ const SubscriptionPrices = ({ cycle, currency, plan }) => { ); return ( <> - + {plan.Pricing[cycle] / cycle} - {cycle === CYCLE.YEARLY && {c('Details').jt`Billed as ${billiedAmount} per year`}} - {cycle === CYCLE.TWO_YEARS && {c('Details').jt`Billed as ${billiedAmount} every 2 year`}} + {cycle === CYCLE.YEARLY && ( +
{c('Details').jt`Billed as ${billiedAmount} per year`}
+ )} + {cycle === CYCLE.TWO_YEARS && ( +
{c('Details').jt`Billed as ${billiedAmount} every 2 year`}
+ )} ); }; diff --git a/containers/payments/subscription/SubscriptionSection.js b/containers/payments/subscription/SubscriptionSection.js index 60e595c25..80a5560de 100644 --- a/containers/payments/subscription/SubscriptionSection.js +++ b/containers/payments/subscription/SubscriptionSection.js @@ -130,7 +130,7 @@ const SubscriptionSection = ({ permission }) => { <> {subTitle} {c('Info') - .t`To manage your subscription, customize your current plan or select another one from the plan's table.`} + .t`To manage your subscription, update your current plan or select another one from the plan's table.`}
{hasPaidMail && mailPlanName !== 'visionary' ? ( @@ -139,7 +139,7 @@ const SubscriptionSection = ({ permission }) => { style={{ right: '2em', top: '1em' }} onClick={handleModal} > - {c('Action').t`Customize`} + {c('Action').t`Update`} ) : null}
diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index bbaacb0ee..8b6088655 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -7,27 +7,38 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI const { state: showAllFeatures, toggle: toggleFeatures } = useToggle(false); return ( -
+
- {plans.map(({ planName, price, imageSrc, description, features = [] }, index) => { + {plans.map(({ planName, price, imageSrc, description, features = [], canCustomize }, index) => { return (
- {index === mostPopularIndex ? ( -
{c('Title for subscription plan') - .t`Most popular`}
- ) : null} -
{planName}
-
{price}
-
- {planName} -
-

{description}

-
    +
    + {index === currentPlanIndex ? ( +
    {c( + 'Title for subscription plan' + ).t`Current plan`}
    + ) : null} + {index === mostPopularIndex ? ( +
    {c( + 'Title for subscription plan' + ).t`Most popular`}
    + ) : null} +
    {planName}
    +
    {price}
    +
    + {planName} +
    +
    +

    {description}

    +
      {features .filter(({ advanced = false }) => { if (!advanced) { @@ -36,26 +47,38 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI return showAllFeatures; }) .map(({ feature }, index) => { - return
    • {feature}
    • ; + return ( +
    • + {feature} +
    • + ); })}
    -
    - -
    +
    + {index === currentPlanIndex && !canCustomize ? ( + c('Label').t`Current plan` + ) : ( + + )} + {canCustomize ? ( + onSelect(index)}>{c('Action').t`Customize`} + ) : null} +
); })}
-
+
- {showAllFeatures ? c('Action').t`Display less features` : c('Action').t`Compare all features`} + {showAllFeatures ? c('Action').t`Close feature comparison` : c('Action').t`Compare all features`}
+ {showAllFeatures ?
: null}
); }; diff --git a/containers/payments/subscription/SubscriptionTable.scss b/containers/payments/subscription/SubscriptionTable.scss new file mode 100644 index 000000000..f7f0a70f7 --- /dev/null +++ b/containers/payments/subscription/SubscriptionTable.scss @@ -0,0 +1,41 @@ +.subsctiptionTable-plan[data-current-plan="true"] { + border-left: 2px solid $pm-primary; + border-right: 2px solid $pm-primary; + border-bottom: 2px solid $pm-primary; + background-color: $pm-global-light; +} + +.subsctiptionTable-currentPlan-container { + background-color: $pm-primary; + color: $white; + position: absolute; + top: -21px; + left: -2px; + right: -2px; + border-radius: $global-border-radius $global-border-radius 0 0; +} + +.subsctiptionTable-header { + height: 220px; +} + +.subsctiptionTable-image-container { + height: 100px; +} + +.subscriptionPrices-monthly .amount { + font-size: larger; +} + +.subscriptionPrices-monthly .suffix, +.subscriptionPrices-monthly .currency { + font-size: smaller; +} + +.subsctiptionTable-features { + background-image: url('#{$path-images}sprite-for-css-only.svg#css-arrow-right'); + background-size: 10px; + padding-left: 20px; + background-repeat: no-repeat; + background-position: 5px 5px; +} \ No newline at end of file From 9b472502970d5f4cdb2d984d6a1f13c2e98821f1 Mon Sep 17 00:00:00 2001 From: EpokK Date: Wed, 27 Nov 2019 16:09:22 +0100 Subject: [PATCH 008/242] Continue --- .../subscription/MailSubscriptionTable.js | 117 ++++++++++++++---- .../subscription/SubscriptionTable.js | 83 +++++++++---- .../subscription/SubscriptionTable.scss | 14 ++- .../subscription/VpnSubscriptionTable.js | 57 +++++++-- 4 files changed, 203 insertions(+), 68 deletions(-) diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 0f10eb319..73b3394c0 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -35,13 +35,31 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, canCustomize: true, price: , imageSrc: freePlanSvg, - description: c('Description').t`The basics for private and secure communications`, + description: c('Description').t`Basic private and secure comminications`, features: [ - { feature: c('Feature').t`1 user` }, - { feature: c('Feature').t`500 MB storage` }, - { feature: c('Feature').t`1 address` }, - { feature: c('Feature').t`No domain support` }, - { feature: c('Feature').t`ProtonVPN (optional) *` } + c('Feature').t`1 user`, + c('Feature').t`500 MB storage`, + c('Feature').t`1 address`, + c('Feature').t`No domain support`, + c('Feature').t`150 messages/day`, + c('Feature').t`ProtonVPN (optional)` + ], + allFeatures: [ + c('Feature').t`1 user`, + c('Feature').t`500 MB storage`, + c('Feature').t`1 address`, + c('Feature').t`No domain support`, + c('Feature').t`150 messages per day`, + c('Feature').t`3 folders/labels`, + {c('Feature').t`Encrypted contacts`}, + {c('Feature').t`Address verification`}, + {c('Feature').t`Filters`}, + {c('Feature').t`IMAP/SMTP support`}, + {c('Feature').t`Auto-responder`}, + {c('Feature').t`@pm.me short email`}, + {c('Feature').t`Catch-all email`}, + {c('Feature').t`Multi-user support`}, + c('Feature').t`Limited support` ] }, plusPlan && { @@ -51,12 +69,29 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, imageSrc: plusPlanSvg, description: c('Description').t`Full-featured mailbox with advanced protection`, features: [ - { feature: c('Feature').t`1 user` }, - { feature: c('Feature').t`5 GB storage *` }, - { feature: c('Feature').t`5 addresses *` }, - { feature: c('Feature').t`Supports 1 domain *` }, - { feature: c('Feature').t`Supports folder, labels, filters, auto-reply, IMAP/SMTP and more` }, - { feature: c('Feature').t`ProtonVPN (optional) *` } + c('Feature').t`1 user`, + c('Feature').t`5 GB storage *`, + c('Feature').t`5 addresses *`, + c('Feature').t`Supports 1 domain *`, + c('Feature').t`Folder, labels, filters, auto-reply, IMAP/SMTP and more`, + c('Feature').t`ProtonVPN (optional)` + ], + allFeatures: [ + c('Feature').t`1 user`, + c('Feature').t`5 GB storage *`, + c('Feature').t`5 addresses *`, + c('Feature').t`1 custom domain *`, + c('Feature').t`Unlimited messages **`, + c('Feature').t`Unlimited folders/labels`, + c('Feature').t`Encrypted contacts`, + c('Feature').t`Address verification`, + c('Feature').t`Filters`, + c('Feature').t`IMAP/SMTP support`, + c('Feature').t`Auto-responder`, + c('Feature').t`@pm.me short email`, + {c('Feature').t`Catch-all email`}, + {c('Feature').t`Multi-user support`}, + c('Feature').t`Normal support` ] }, professionalPlan && { @@ -66,12 +101,29 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, imageSrc: professionalPlanSvg, description: c('Description').t`ProtonMail for professionals and businesses`, features: [ - { feature: c('Feature').t`1 - 5000 user *` }, - { feature: c('Feature').t`5 GB storage per user *` }, - { feature: c('Feature').t`5 addresses per user *` }, - { feature: c('Feature').t`Supports 2 domains *` }, - { feature: c('Feature').t`Catch all email, multi user management, priority support and more` }, - { feature: c('Feature').t`ProtonVPN (optional) *` } + c('Feature').t`1 - 5000 user *`, + c('Feature').t`5 GB storage per user *`, + c('Feature').t`5 addresses per user *`, + c('Feature').t`Supports 2 domains *`, + c('Feature').t`Catch-all email, multi-user management`, + c('Feature').t`Priority support` + ], + allFeatures: [ + c('Feature').t`1-5000 user`, + c('Feature').t`5 GB per user *`, + c('Feature').t`5 addresses per user *`, + c('Feature').t`2 custom domains *`, + c('Feature').t`Unlimited messages **`, + c('Feature').t`Unlimited folders/labels`, + c('Feature').t`Encrypted contacts`, + c('Feature').t`Address verification`, + c('Feature').t`Filters`, + c('Feature').t`IMAP/SMTP support`, + c('Feature').t`Auto-responder`, + c('Feature').t`@pm.me short email`, + c('Feature').t`Catch-all email`, + c('Feature').t`Multi-user support`, + c('Feature').t`Priority support` ] }, visionaryPlan && { @@ -81,12 +133,29 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, imageSrc: visionaryPlanSvg, description: c('Description').t`ProtonMail for families and small businesses`, features: [ - { feature: c('Feature').t`6 users` }, - { feature: c('Feature').t`20 GB storage` }, - { feature: c('Feature').t`Supports 10 domains` }, - { feature: c('Feature').t`Includes all features` }, - { feature: c('Feature').t`Priority support` }, - { feature: c('Feature').t`Includes ProtonVPN` } + c('Feature').t`6 users`, + c('Feature').t`20 GB storage`, + c('Feature').t`50 addresses`, + c('Feature').t`Supports 10 domains`, + c('Feature').t`Includes all features`, + c('Feature').t`Includes ProtonVPN` + ], + allFeatures: [ + c('Feature').t`6 users`, + c('Feature').t`20 GB storage`, + c('Feature').t`50 addresses`, + c('Feature').t`10 custom domains *`, + c('Feature').t`Unlimited messages **`, + c('Feature').t`Unlimited folders/labels`, + c('Feature').t`Encrypted contacts`, + c('Feature').t`Address verification`, + c('Feature').t`Filters`, + c('Feature').t`IMAP/SMTP support`, + c('Feature').t`Auto-responder`, + c('Feature').t`@pm.me short email`, + c('Feature').t`Catch-all email`, + c('Feature').t`Multi-user support`, + c('Feature').t`Priority support` ] } ]; diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 8b6088655..8dcaf434a 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -14,15 +14,15 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI
-
+
{index === currentPlanIndex ? ( -
{c( +
{c( 'Title for subscription plan' ).t`Current plan`}
) : null} @@ -33,26 +33,19 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI ) : null}
{planName}
{price}
-
+
{planName}

{description}

-
    - {features - .filter(({ advanced = false }) => { - if (!advanced) { - return true; - } - return showAllFeatures; - }) - .map(({ feature }, index) => { - return ( -
  • - {feature} -
  • - ); - })} +
      + {features.map((feature, index) => { + return ( +
    • + {feature} +
    • + ); + })}
    {index === currentPlanIndex && !canCustomize ? ( @@ -78,7 +71,50 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI {showAllFeatures ? c('Action').t`Close feature comparison` : c('Action').t`Compare all features`}
- {showAllFeatures ?
: null} + {showAllFeatures ? ( +
+ {plans.map(({ planName, allFeatures = [], canCustomize }, index) => { + return ( +
+
    + {allFeatures.map((feature, index) => { + return ( +
  • + {feature} +
  • + ); + })} +
+
+ {index === currentPlanIndex && !canCustomize ? ( + c('Label').t`Current plan` + ) : ( + + )} + {canCustomize ? ( + onSelect(index)}>{c('Action') + .t`Customize`} + ) : null} +
+
+ ); + })} +
+ ) : null}
); }; @@ -90,12 +126,7 @@ SubscriptionTable.propTypes = { price: PropTypes.node.isRequired, imageSrc: PropTypes.string.isRequired, description: PropTypes.node.isRequired, - features: PropTypes.arrayOf( - PropTypes.shape({ - feature: PropTypes.node.isRequired, - advanced: PropTypes.bool - }) - ) + features: PropTypes.arrayOf(PropTypes.node).isRequired }) ), onSelect: PropTypes.func.isRequired, diff --git a/containers/payments/subscription/SubscriptionTable.scss b/containers/payments/subscription/SubscriptionTable.scss index f7f0a70f7..317bfa7c3 100644 --- a/containers/payments/subscription/SubscriptionTable.scss +++ b/containers/payments/subscription/SubscriptionTable.scss @@ -1,11 +1,11 @@ -.subsctiptionTable-plan[data-current-plan="true"] { +.subscriptionTable-plan[data-current-plan="true"] { border-left: 2px solid $pm-primary; border-right: 2px solid $pm-primary; border-bottom: 2px solid $pm-primary; background-color: $pm-global-light; } -.subsctiptionTable-currentPlan-container { +.subscriptionTable-currentPlan-container { background-color: $pm-primary; color: $white; position: absolute; @@ -15,11 +15,11 @@ border-radius: $global-border-radius $global-border-radius 0 0; } -.subsctiptionTable-header { +.subscriptionTable-header { height: 220px; } -.subsctiptionTable-image-container { +.subscriptionTable-image-container { height: 100px; } @@ -32,10 +32,14 @@ font-size: smaller; } -.subsctiptionTable-features { +.subscriptionTable-feature { background-image: url('#{$path-images}sprite-for-css-only.svg#css-arrow-right'); background-size: 10px; padding-left: 20px; background-repeat: no-repeat; background-position: 5px 5px; +} + +.subscriptionTable-feature { + } \ No newline at end of file diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index 44f860856..da705cd1c 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -4,10 +4,10 @@ import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; import { getPlan } from 'proton-shared/lib/helpers/subscription'; import { c } from 'ttag'; -import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; -import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; -import professionalPlanSvg from 'design-system/assets/img/pm-images/professional-plan.svg'; -import visionaryPlanSvg from 'design-system/assets/img/pm-images/visionary-plan.svg'; +import freePlanSvg from 'design-system/assets/img/pv-images/plans/free-plan.svg'; +import plusPlanSvg from 'design-system/assets/img/pv-images/plans/vpnbasic-plan.svg'; +import professionalPlanSvg from 'design-system/assets/img/pv-images/plans/vpnplus-plan.svg'; +import visionaryPlanSvg from 'design-system/assets/img/pv-images/plans/visionary-plan.svg'; import SubscriptionPrices from './SubscriptionPrices'; @@ -34,29 +34,60 @@ const VpnSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, planName: 'Free', price: , imageSrc: freePlanSvg, - description: c('Description').t`The basics for private and secure communications`, - features: [] + description: c('Description').t`Privacy and security for everyone`, + features: [ + c('Feature').t`1 VPN connection`, + c('Feature').t`Servers in 3 countries`, + c('Feature').t`Medium speed`, + c('Feature').t`No logs/No ads`, + {c('Feature').t`Filesharing/bitorrent support`}, + {c('Feature').t`Secure Core and Tor VPN`}, + {c('Feature').t`Advanced privacy features`}, + {c('Feature').t`Access blocked content`} + ] }, vpnBasicPlan && { planName: PLAN_NAMES[PLANS.VPNBASIC], price: , imageSrc: plusPlanSvg, - description: c('Description').t`Full-featured mailbox with advanced protection`, - features: [] + description: c('Description').t`Basic privacy features`, + features: [ + c('Feature').t`2 VPN connection`, + c('Feature').t`Servers in XX countries`, // TODO + c('Feature').t`High speed`, + c('Feature').t`No logs/No ads`, + {c('Feature').t`Filesharing/bitorrent support`}, + {c('Feature').t`Secure Core and Tor VPN`}, + {c('Feature').t`Advanced privacy features`}, + {c('Feature').t`Access blocked content`} + ] }, vpnPlusPlan && { planName: PLAN_NAMES[PLANS.VPNPLUS], price: , imageSrc: professionalPlanSvg, - description: c('Description').t`ProtonMail for professionals and businesses`, - features: [] + description: c('Description').t`Advanced security features`, + features: [ + c('Feature').t`5 VPN connection`, + c('Feature').t`Servers in XX countries`, // TODO + c('Feature').t`Highest speed (10 Gbps)`, + c('Feature').t`No logs/No ads`, + c('Feature').t`Filesharing/bitorrent support`, + c('Feature').t`Secure Core and Tor VPN`, + c('Feature').t`Advanced privacy features`, + c('Feature').t`Access blocked content` + ] }, visionaryPlan && { planName: PLAN_NAMES[PLANS.VISIONARY], price: , imageSrc: visionaryPlanSvg, - description: c('Description').t`ProtonMail for families and small businesses`, - features: [] + description: c('Description').t`The complete privacy suite`, + features: [ + c('Feature').t`All Plus plan features`, + c('Feature').t`10 simultaneous VPN connections`, + c('Feature').t`ProtonMail Visionary account` + ] } ]; const { Name } = getPlan(subscription); @@ -64,7 +95,7 @@ const VpnSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, return ( From b7dfc027c7d664b86f9dbc159773ede61ba8d3c6 Mon Sep 17 00:00:00 2001 From: EpokK Date: Fri, 29 Nov 2019 13:03:55 +0100 Subject: [PATCH 009/242] Continue --- .../subscription/SubscriptionTable.js | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 8dcaf434a..32e41db5c 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useToggle, Button, classnames, LinkButton } from 'react-components'; +import { range } from 'proton-shared/lib/helpers/array'; import { c } from 'ttag'; const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularIndex = 0 }) => { @@ -47,7 +48,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI ); })} -
+
{index === currentPlanIndex && !canCustomize ? ( c('Label').t`Current plan` ) : ( @@ -72,47 +73,29 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI
{showAllFeatures ? ( -
- {plans.map(({ planName, allFeatures = [], canCustomize }, index) => { - return ( -
-
    - {allFeatures.map((feature, index) => { - return ( -
  • - {feature} -
  • - ); - })} -
-
- {index === currentPlanIndex && !canCustomize ? ( - c('Label').t`Current plan` - ) : ( -
@@ -126,7 +109,8 @@ SubscriptionTable.propTypes = { price: PropTypes.node.isRequired, imageSrc: PropTypes.string.isRequired, description: PropTypes.node.isRequired, - features: PropTypes.arrayOf(PropTypes.node).isRequired + features: PropTypes.arrayOf(PropTypes.node).isRequired, + allFeatures: PropTypes.arrayOf(PropTypes.node).isRequired }) ), onSelect: PropTypes.func.isRequired, From 709007fca4722e3c8022d7ff5f1d35eeca6d6f7a Mon Sep 17 00:00:00 2001 From: EpokK Date: Fri, 29 Nov 2019 13:09:05 +0100 Subject: [PATCH 010/242] Continue --- containers/payments/subscription/SubscriptionTable.js | 1 - 1 file changed, 1 deletion(-) diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 32e41db5c..a8ba65f2f 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useToggle, Button, classnames, LinkButton } from 'react-components'; -import { range } from 'proton-shared/lib/helpers/array'; import { c } from 'ttag'; const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularIndex = 0 }) => { From 10b914e786e0eff87f3f3f6ed0ffe0d152a372e2 Mon Sep 17 00:00:00 2001 From: EpokK Date: Fri, 29 Nov 2019 15:02:49 +0100 Subject: [PATCH 011/242] Continue --- containers/payments/PlansSection.js | 1 - .../subscription/MailSubscriptionTable.js | 22 +++++++++++-------- .../subscription/SubscriptionTable.js | 6 ++--- .../subscription/SubscriptionTable.scss | 4 ++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/containers/payments/PlansSection.js b/containers/payments/PlansSection.js index e75853b3c..4f42469e1 100644 --- a/containers/payments/PlansSection.js +++ b/containers/payments/PlansSection.js @@ -146,7 +146,6 @@ const PlansSection = () => { handleModal({ [Name]: 1 }); }} /> -

* {c('Info concerning plan features').t`denotes customizable features`}

); }; diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 73b3394c0..b45fe6b5f 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -67,7 +67,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, canCustomize: true, price: , imageSrc: plusPlanSvg, - description: c('Description').t`Full-featured mailbox with advanced protection`, + description: c('Description').t`Full-featured individual mailbox`, features: [ c('Feature').t`1 user`, c('Feature').t`5 GB storage *`, @@ -99,7 +99,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, canCustomize: true, price: , imageSrc: professionalPlanSvg, - description: c('Description').t`ProtonMail for professionals and businesses`, + description: c('Description').t`For large organizations and businesses`, features: [ c('Feature').t`1 - 5000 user *`, c('Feature').t`5 GB storage per user *`, @@ -131,7 +131,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, canCustomize: false, price: , imageSrc: visionaryPlanSvg, - description: c('Description').t`ProtonMail for families and small businesses`, + description: c('Description').t`For families and small businesses`, features: [ c('Feature').t`6 users`, c('Feature').t`20 GB storage`, @@ -162,12 +162,16 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, const { Name } = getPlan(subscription); return ( - + <> + +

* {c('Info concerning plan features').t`Customizable features`}

+

** {c('Info concerning plan features').t`ProtonMail cannot be used for mass emailing or spamming. Legitimate emails are unlimited.`}

+ ); }; diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index a8ba65f2f..39855198f 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -47,7 +47,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI ); })} -
+
{index === currentPlanIndex && !canCustomize ? ( c('Label').t`Current plan` ) : ( @@ -73,7 +73,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI
{showAllFeatures ? (
- +
{plans[0].allFeatures .map((f, i) => { @@ -86,7 +86,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI key={`tr${index}`} > {features.map((feature, index) => ( - ))} diff --git a/containers/payments/subscription/SubscriptionTable.scss b/containers/payments/subscription/SubscriptionTable.scss index 317bfa7c3..8d8e64573 100644 --- a/containers/payments/subscription/SubscriptionTable.scss +++ b/containers/payments/subscription/SubscriptionTable.scss @@ -19,6 +19,10 @@ height: 220px; } +.subscriptionTable-footer { + height: 70px; +} + .subscriptionTable-image-container { height: 100px; } From ecbf9fe111b7f08080e4e1758e5dc6f198a29af1 Mon Sep 17 00:00:00 2001 From: EpokK Date: Fri, 29 Nov 2019 21:42:47 +0100 Subject: [PATCH 012/242] Continue --- containers/payments/CurrencySelector.js | 3 +- .../subscription/MailSubscriptionTable.js | 2 +- .../subscription/NewSubscriptionModal.js | 84 +++++++++++++++++++ .../subscription/SubscriptionCheckout.js | 25 ++++++ .../subscription/SubscriptionCustomization.js | 5 ++ .../subscription/SubscriptionPayment.js | 3 + .../subscription/SubscriptionPrices.js | 5 +- .../subscription/SubscriptionThanks.js | 3 + .../subscription/SubscriptionUpgrade.js | 3 + index.ts | 1 + 10 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 containers/payments/subscription/NewSubscriptionModal.js create mode 100644 containers/payments/subscription/SubscriptionCheckout.js create mode 100644 containers/payments/subscription/SubscriptionCustomization.js create mode 100644 containers/payments/subscription/SubscriptionPayment.js create mode 100644 containers/payments/subscription/SubscriptionThanks.js create mode 100644 containers/payments/subscription/SubscriptionUpgrade.js diff --git a/containers/payments/CurrencySelector.js b/containers/payments/CurrencySelector.js index 6dba53170..186ce5ca3 100644 --- a/containers/payments/CurrencySelector.js +++ b/containers/payments/CurrencySelector.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Select, Group, ButtonGroup } from 'react-components'; +import { Select, Group, ButtonGroup, classnames } from 'react-components'; import { CURRENCIES, DEFAULT_CURRENCY } from 'proton-shared/lib/constants'; -import { classnames } from '../../helpers/component'; const CurrencySelector = ({ currency = DEFAULT_CURRENCY, onSelect, mode = 'select', ...rest }) => { const handleChange = ({ target }) => onSelect(target.value); diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index b45fe6b5f..634bb314f 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -97,7 +97,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, professionalPlan && { planName: PLAN_NAMES[PLANS.PROFESSIONAL], canCustomize: true, - price: , + price: , imageSrc: professionalPlanSvg, description: c('Description').t`For large organizations and businesses`, features: [ diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js new file mode 100644 index 000000000..5f329df78 --- /dev/null +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { c } from 'ttag'; +import { FormModal, CurrencySelector, CycleSelector, usePlans, useNotifications, useApi, useLoading, useSubscription } from 'react-components'; +import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; +import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; +import { toMap } from 'proton-shared/lib/helpers/object'; + +import SubscriptionCustomization from './SubscriptionCustomization'; +import SubscriptionPayment from './SubscriptionPayment'; +import SubscriptionUpgrade from './SubscriptionUpgrade'; +import SubscriptionThanks from './SubscriptionThanks'; +import SubscriptionCheckout from './SubscriptionCheckout'; + +const STEPS = { + CUSTOMIZATION: 0, + PAYMENT: 1, + UPGRADE: 2, + THANKS: 3 +}; + +const NewSubscriptionModal = ({ step: initialStep = STEPS.CUSTOMIZATION, cycle = DEFAULT_CYCLE, currency = DEFAULT_CURRENCY, coupon, planIDs = [], ...rest }) => { + const api = useApi(); + const [plans, loadingPlans] = usePlans(); + const [subscription, loadingSubscription] = useSubscription(); + const [loading, withLoading] = useLoading(); + const { createNotification } = useNotifications(); + const [model, setModel] = useState({ cycle, currency, coupon, planIDs }); + const [step, setStep] = useState(initialStep); + const plansMap = toMap(plans || []); + + const check = async (newModel = model) => { + try { + const { Coupon, Gift } = await api(checkSubscription({ + PlanIDs: newModel.planIDs, + CouponCode: newModel.coupon, + Currency: newModel.currency, + Cycle: newModel.cycle + })); + + if (newModel.coupon && newModel.coupon !== Coupon) { + const text = c('Error').t`Your coupon is invalid or cannot be applied to your plan`; + createNotification({ text, type: 'error' }); + throw new Error(text); + } + + if (newModel.gift && !Gift) { + const text = c('Error').t`Invalid gift code`; + createNotification({ text, type: 'error' }); + throw new Error(text); + } + + setModel(newModel); + } catch (error) { + throw error; + } + }; + + const handleSubscribe = async () => { + setStep(STEPS.UPGRADE); + }; + + return ( + + + {step === STEPS.CUSTOMIZATION && } + {step === STEPS.PAYMENT && } + {step === STEPS.UPGRADE && } + {step === STEPS.THANKS && } + + ); +}; + +NewSubscriptionModal.propTypes = { + step: PropTypes.oneOf([STEPS.CUSTOMIZATION, STEPS.PAYMENT, STEPS.UPGRADE, STEPS.THANKS]), + cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.TWO_YEARS, CYCLE.YEARLY]), + currency: PropTypes.oneOf(CURRENCIES), + coupon: PropTypes.string, + planIDs: PropTypes.arrayOf(PropTypes.string) +}; + +export default NewSubscriptionModal; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js new file mode 100644 index 000000000..5d12235bb --- /dev/null +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { c } from 'ttag'; +import { CurrencySelector, CycleSelector } from 'react-components'; + +const SubscriptionCheckout = ({ model, setModel }) => { + return ( + <> +
+ setModel({ ...model, currency: newCurrency })} /> + setModel({ ...model, cycle: newCycle })} /> +
+
+
+
+ + ); +}; + +SubscriptionCheckout.propTypes = { + model: PropTypes.object.isRequired, + setModel: PropTypes.func.isRequired +}; + +export default SubscriptionCheckout; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js new file mode 100644 index 000000000..8f74f74d5 --- /dev/null +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -0,0 +1,5 @@ +import React from 'react'; + +const SubscriptionCustomization = () => {}; + +export default SubscriptionCustomization; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionPayment.js b/containers/payments/subscription/SubscriptionPayment.js new file mode 100644 index 000000000..1e2e5c0b1 --- /dev/null +++ b/containers/payments/subscription/SubscriptionPayment.js @@ -0,0 +1,3 @@ +const SubscriptionPayment = () => {}; + +export default SubscriptionPayment; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionPrices.js b/containers/payments/subscription/SubscriptionPrices.js index 0a0d27a3f..acfa45747 100644 --- a/containers/payments/subscription/SubscriptionPrices.js +++ b/containers/payments/subscription/SubscriptionPrices.js @@ -4,7 +4,7 @@ import { Price } from 'react-components'; import { CYCLE } from 'proton-shared/lib/constants'; import { c } from 'ttag'; -const SubscriptionPrices = ({ cycle, currency, plan }) => { +const SubscriptionPrices = ({ cycle, currency, plan, suffix = c('Suffix').t`/month` }) => { const billiedAmount = ( {plan.Pricing[cycle]} @@ -12,7 +12,7 @@ const SubscriptionPrices = ({ cycle, currency, plan }) => { ); return ( <> - + {plan.Pricing[cycle] / cycle} {cycle === CYCLE.YEARLY && ( @@ -26,6 +26,7 @@ const SubscriptionPrices = ({ cycle, currency, plan }) => { }; SubscriptionPrices.propTypes = { + suffix: PropTypes.string, cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired, plan: PropTypes.shape({ diff --git a/containers/payments/subscription/SubscriptionThanks.js b/containers/payments/subscription/SubscriptionThanks.js new file mode 100644 index 000000000..8dc4d3296 --- /dev/null +++ b/containers/payments/subscription/SubscriptionThanks.js @@ -0,0 +1,3 @@ +const SubscriptionThanks = () => {}; + +export default SubscriptionThanks; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionUpgrade.js b/containers/payments/subscription/SubscriptionUpgrade.js new file mode 100644 index 000000000..8cd5ae9bd --- /dev/null +++ b/containers/payments/subscription/SubscriptionUpgrade.js @@ -0,0 +1,3 @@ +const SubscriptionUpgrade = () => {}; + +export default SubscriptionUpgrade; \ No newline at end of file diff --git a/index.ts b/index.ts index aae390951..9b501b25f 100644 --- a/index.ts +++ b/index.ts @@ -176,6 +176,7 @@ export { default as GiftCodeInput } from './containers/payments/GiftCodeInput'; export { default as PlansSection } from './containers/payments/PlansSection'; export { default as SubscriptionSection } from './containers/payments/subscription/SubscriptionSection'; export { default as SubscriptionModal } from './containers/payments/subscription/SubscriptionModal'; +export { default as NewSubscriptionModal } from './containers/payments/subscription/NewSubscriptionModal'; export { default as BlackFridayModal } from './containers/payments/subscription/BlackFridayModal'; export { default as MailBlackFridayModal } from './containers/payments/subscription/MailBlackFridayModal'; export { default as VPNBlackFridayModal } from './containers/payments/subscription/VPNBlackFridayModal'; From ad107d2e653e3aea6b3d0a04fe78a83fa4cea650 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 4 Dec 2019 13:37:23 +0100 Subject: [PATCH 013/242] Continue --- components/price/Price.js | 8 +- containers/payments/CurrencySelector.js | 21 +- containers/payments/PlansSection.js | 37 +- .../subscription/MailSubscriptionTable.js | 36 +- .../subscription/NewSubscriptionModal.js | 121 +++++- .../subscription/SubscriptionAddonRow.js | 78 ++++ .../subscription/SubscriptionCheckout.js | 40 +- .../subscription/SubscriptionCustomization.js | 382 +++++++++++++++++- .../subscription/SubscriptionFeatureRow.js | 21 + .../payments/subscription/SubscriptionPlan.js | 65 +++ .../subscription/VpnSubscriptionTable.js | 21 +- 11 files changed, 756 insertions(+), 74 deletions(-) create mode 100644 containers/payments/subscription/SubscriptionAddonRow.js create mode 100644 containers/payments/subscription/SubscriptionFeatureRow.js create mode 100644 containers/payments/subscription/SubscriptionPlan.js diff --git a/components/price/Price.js b/components/price/Price.js index 76cbefa92..5fe0cd0d6 100644 --- a/components/price/Price.js +++ b/components/price/Price.js @@ -8,17 +8,19 @@ const CURRENCIES = { CHF: 'CHF' }; -const Price = ({ children: amount = 0, currency = '', className = '', divisor = 100, suffix = '' }) => { +const Price = ({ children: amount = 0, currency = '', className = '', divisor = 100, suffix = '', prefix = '' }) => { const fixedValue = Number(amount / divisor).toFixed(2); const value = fixedValue.replace('.00', '').replace('-', ''); const c = {CURRENCIES[currency] || currency}; const p = amount < 0 ? - : null; const v = {value}; const s = suffix ? {suffix} : null; + const pr = prefix ? {prefix} : null; if (currency === 'USD') { return ( + {pr} {p} {c} {v} @@ -29,6 +31,7 @@ const Price = ({ children: amount = 0, currency = '', className = '', divisor = return ( + {pr} {p} {v} {currency ? <> {c} : null} @@ -42,7 +45,8 @@ Price.propTypes = { children: PropTypes.number, className: PropTypes.string, divisor: PropTypes.number, - suffix: PropTypes.string + suffix: PropTypes.string, + prefix: PropTypes.string }; export default Price; diff --git a/containers/payments/CurrencySelector.js b/containers/payments/CurrencySelector.js index 186ce5ca3..bcfda3b48 100644 --- a/containers/payments/CurrencySelector.js +++ b/containers/payments/CurrencySelector.js @@ -3,6 +3,18 @@ import PropTypes from 'prop-types'; import { Select, Group, ButtonGroup, classnames } from 'react-components'; import { CURRENCIES, DEFAULT_CURRENCY } from 'proton-shared/lib/constants'; +const addSymbol = (currency) => { + if (currency === 'EUR') { + return `€ ${currency}`; + } + + if (currency === 'USD') { + return `$ ${currency}`; + } + + return currency; +}; + const CurrencySelector = ({ currency = DEFAULT_CURRENCY, onSelect, mode = 'select', ...rest }) => { const handleChange = ({ target }) => onSelect(target.value); const options = CURRENCIES.map((c) => ({ text: c, value: c })); @@ -26,7 +38,14 @@ const CurrencySelector = ({ currency = DEFAULT_CURRENCY, onSelect, mode = 'selec } if (mode === 'select') { - return ({ ...option, text: addSymbol(option.text) }))} + onChange={handleChange} + {...rest} + /> + ); } return null; diff --git a/containers/payments/PlansSection.js b/containers/payments/PlansSection.js index 4f42469e1..11d7ecdd9 100644 --- a/containers/payments/PlansSection.js +++ b/containers/payments/PlansSection.js @@ -21,12 +21,11 @@ import { import { checkSubscription, deleteSubscription } from 'proton-shared/lib/api/payments'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE } from 'proton-shared/lib/constants'; -import { getPlans, isBundleEligible } from 'proton-shared/lib/helpers/subscription'; +import { getPlans, isBundleEligible, getPlan } from 'proton-shared/lib/helpers/subscription'; import { isLoyal } from 'proton-shared/lib/helpers/organization'; -import SubscriptionModal from './subscription/SubscriptionModal'; +import NewSubscriptionModal from './subscription/NewSubscriptionModal'; import MailSubscriptionTable from './subscription/MailSubscriptionTable'; -import { mergePlansMap, getCheckParams } from './subscription/helpers'; const PlansSection = () => { const { call } = useEventManager(); @@ -38,6 +37,7 @@ const PlansSection = () => { const [organization = {}, loadingOrganization] = useOrganization(); const [plans = [], loadingPlans] = usePlans(); const api = useApi(); + const { Name } = getPlan(subscription) || {}; const [currency, setCurrency] = useState(DEFAULT_CURRENCY); const [cycle, setCycle] = useState(DEFAULT_CYCLE); @@ -68,29 +68,25 @@ const PlansSection = () => { return handleUnsubscribe(); }; - const handleModal = (newPlansMap, step) => async () => { - if (!newPlansMap) { + const handleModal = async (planID) => { + if (!planID) { handleOpenModal(); return; } - const plansMap = mergePlansMap(newPlansMap, subscription); const couponCode = CouponCode ? CouponCode : undefined; // From current subscription; CouponCode can be null const { Coupon } = await api( - checkSubscription(getCheckParams({ plans, plansMap, currency, cycle, coupon: couponCode })) + checkSubscription({ + PlanIDs: [planID], + Currency: currency, + Cycle: cycle, + CouponCode: couponCode + }) ); + const coupon = Coupon ? Coupon.Code : undefined; // Coupon can equals null - createModal( - - ); + createModal(); }; useEffect(() => { @@ -138,13 +134,10 @@ const PlansSection = () => { { - const { Name } = plans[planIndex]; - handleModal({ [Name]: 1 }); - }} + onSelect={handleModal} /> ); diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 634bb314f..626651ec1 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -2,7 +2,7 @@ import React from 'react'; import { SubscriptionTable } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; -import { getPlan } from 'proton-shared/lib/helpers/subscription'; +import { toMap } from 'proton-shared/lib/helpers/object'; import { c } from 'ttag'; import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; import plusPlanSvg from 'design-system/assets/img/pm-images/plus-plan.svg'; @@ -25,10 +25,11 @@ const FREE_PLAN = { } }; -const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, currency, onSelect }) => { - const plusPlan = apiPlans.find(({ Name }) => Name === PLANS.PLUS); - const professionalPlan = apiPlans.find(({ Name }) => Name === PLANS.PROFESSIONAL); - const visionaryPlan = apiPlans.find(({ Name }) => Name === PLANS.VISIONARY); +const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, currency, onSelect }) => { + const plansMap = toMap(apiPlans, 'Name'); + const plusPlan = plansMap[PLANS.PLUS]; + const professionalPlan = plansMap[PLANS.PROFESSIONAL]; + const visionaryPlan = plansMap[PLANS.VISIONARY]; const plans = [ { planName: 'Free', @@ -63,6 +64,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, ] }, plusPlan && { + planID: plusPlan.ID, planName: PLAN_NAMES[PLANS.PLUS], canCustomize: true, price: , @@ -95,9 +97,17 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, ] }, professionalPlan && { + planID: professionalPlan.ID, planName: PLAN_NAMES[PLANS.PROFESSIONAL], canCustomize: true, - price: , + price: ( + + ), imageSrc: professionalPlanSvg, description: c('Description').t`For large organizations and businesses`, features: [ @@ -127,6 +137,7 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, ] }, visionaryPlan && { + planID: visionaryPlan.ID, planName: PLAN_NAMES[PLANS.VISIONARY], canCustomize: false, price: , @@ -159,24 +170,27 @@ const MailSubscriptionTable = ({ subscription = {}, plans: apiPlans = [], cycle, ] } ]; - const { Name } = getPlan(subscription); return ( <> onSelect(plans[index].planID)} />

* {c('Info concerning plan features').t`Customizable features`}

-

** {c('Info concerning plan features').t`ProtonMail cannot be used for mass emailing or spamming. Legitimate emails are unlimited.`}

+

+ **{' '} + {c('Info concerning plan features') + .t`ProtonMail cannot be used for mass emailing or spamming. Legitimate emails are unlimited.`} +

); }; MailSubscriptionTable.propTypes = { - subscription: PropTypes.object, + planNameSelected: PropTypes.string, plans: PropTypes.arrayOf(PropTypes.object), onSelect: PropTypes.func.isRequired, cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 5f329df78..a980495e2 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,8 +1,24 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { FormModal, CurrencySelector, CycleSelector, usePlans, useNotifications, useApi, useLoading, useSubscription } from 'react-components'; -import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; +import { + FormModal, + usePlans, + useNotifications, + useApi, + useLoading, + useSubscription, + useConfig +} from 'react-components'; +import { + DEFAULT_CURRENCY, + DEFAULT_CYCLE, + CYCLE, + CURRENCIES, + PLAN_SERVICES, + PLAN_TYPES, + CLIENT_TYPES +} from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; import { toMap } from 'proton-shared/lib/helpers/object'; @@ -19,24 +35,58 @@ const STEPS = { THANKS: 3 }; -const NewSubscriptionModal = ({ step: initialStep = STEPS.CUSTOMIZATION, cycle = DEFAULT_CYCLE, currency = DEFAULT_CURRENCY, coupon, planIDs = [], ...rest }) => { +const SERVICES = { + [CLIENT_TYPES.MAIL]: PLAN_SERVICES.MAIL, + [CLIENT_TYPES.VPN]: PLAN_SERVICES.VPN +}; + +const NewSubscriptionModal = ({ + expanded = false, + step: initialStep = STEPS.CUSTOMIZATION, + cycle = DEFAULT_CYCLE, + currency = DEFAULT_CURRENCY, + coupon, + planIDs = {}, + ...rest +}) => { + const TITLE = { + [STEPS.CUSTOMIZATION]: c('Title').t`Plan customization`, + [STEPS.PAYMENT]: c('Title').t`Billing details`, + [STEPS.UPGRADE]: c('Title').t`???`, // TODO + [STEPS.THANKS]: c('Title').t`???` // TODO + }; + + const { CLIENT_TYPE } = useConfig(); const api = useApi(); const [plans, loadingPlans] = usePlans(); const [subscription, loadingSubscription] = useSubscription(); const [loading, withLoading] = useLoading(); const { createNotification } = useNotifications(); - const [model, setModel] = useState({ cycle, currency, coupon, planIDs }); - const [step, setStep] = useState(initialStep); const plansMap = toMap(plans || []); + const [model, setModel] = useState({ + cycle, + currency, + coupon, + planIDs + }); + const [step, setStep] = useState(initialStep); + + const { Name = 'free' } = + Object.entries(model.planIDs) + .filter(([, quantity]) => quantity) + .map(([planID]) => plansMap[planID]) + .find(({ Type, Services }) => Type === PLAN_TYPES.PLAN && Services & SERVICES[CLIENT_TYPE]) || {}; const check = async (newModel = model) => { try { - const { Coupon, Gift } = await api(checkSubscription({ - PlanIDs: newModel.planIDs, - CouponCode: newModel.coupon, - Currency: newModel.currency, - Cycle: newModel.cycle - })); + const { Coupon, Gift } = await api( + checkSubscription({ + PlanIDs: newModel.planIDs, + CouponCode: newModel.coupon, + Currency: newModel.currency, + Cycle: newModel.cycle + }) + ); if (newModel.coupon && newModel.coupon !== Coupon) { const text = c('Error').t`Your coupon is invalid or cannot be applied to your plan`; @@ -58,27 +108,58 @@ const NewSubscriptionModal = ({ step: initialStep = STEPS.CUSTOMIZATION, cycle = const handleSubscribe = async () => { setStep(STEPS.UPGRADE); + await api(subscribe(subscription)); }; + const handleCheckout = async () => {}; + + useEffect(() => { + withLoading(check()); + }, []); + return ( - - {step === STEPS.CUSTOMIZATION && } - {step === STEPS.PAYMENT && } - {step === STEPS.UPGRADE && } - {step === STEPS.THANKS && } + {...rest} + > + {step === STEPS.CUSTOMIZATION && ( +
+
+ +
+
+ +
+
+ )} + {step === STEPS.PAYMENT && } + {step === STEPS.UPGRADE && } + {step === STEPS.THANKS && }
); }; NewSubscriptionModal.propTypes = { + expanded: PropTypes.bool, step: PropTypes.oneOf([STEPS.CUSTOMIZATION, STEPS.PAYMENT, STEPS.UPGRADE, STEPS.THANKS]), cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.TWO_YEARS, CYCLE.YEARLY]), currency: PropTypes.oneOf(CURRENCIES), coupon: PropTypes.string, - planIDs: PropTypes.arrayOf(PropTypes.string) + planIDs: PropTypes.object }; -export default NewSubscriptionModal; \ No newline at end of file +export default NewSubscriptionModal; diff --git a/containers/payments/subscription/SubscriptionAddonRow.js b/containers/payments/subscription/SubscriptionAddonRow.js new file mode 100644 index 000000000..124a4c171 --- /dev/null +++ b/containers/payments/subscription/SubscriptionAddonRow.js @@ -0,0 +1,78 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Button, Select, generateUID } from 'react-components'; +import { range } from 'proton-shared/lib/helpers/array'; +import { identity } from 'proton-shared/lib/helpers/function'; +import { c } from 'ttag'; + +const SubscriptionAddonRow = ({ + label, + price, + format = identity, + start = 0, + min = 0, + max = 999, + quantity = 0, + onChange, + step = 1 +}) => { + const idRef = useRef(); + const options = range(min, max, step).map((number, quantity) => ({ + text: format(start + number), + value: quantity + })); + + useEffect(() => { + idRef.current = generateUID('subscription-addon-row'); + }, []); + + return ( +
+ +
+
+
+
+
+ {feature}
+
+
{plans[0].allFeatures .map((f, i) => { @@ -86,7 +95,13 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI key={`tr${index}`} > {features.map((feature, index) => ( - ))} @@ -102,6 +117,7 @@ const SubscriptionTable = ({ plans, onSelect, currentPlanIndex = 0, mostPopularI }; SubscriptionTable.propTypes = { + currentPlan: PropTypes.string, plans: PropTypes.arrayOf( PropTypes.shape({ planName: PropTypes.string.isRequired, diff --git a/containers/payments/subscription/SubscriptionTable.scss b/containers/payments/subscription/SubscriptionTable.scss index 8d8e64573..c5df9d146 100644 --- a/containers/payments/subscription/SubscriptionTable.scss +++ b/containers/payments/subscription/SubscriptionTable.scss @@ -16,11 +16,15 @@ } .subscriptionTable-header { - height: 220px; + min-height: 22rem; +} + +.subscriptionTable-description { + min-height: 7rem; } .subscriptionTable-footer { - height: 70px; + min-height: 7rem; } .subscriptionTable-image-container { diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index a438f77a0..eb294cb85 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -25,7 +25,7 @@ const FREE_PLAN = { } }; -const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, currency, onSelect }) => { +const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, currency, onSelect, currentPlan }) => { const plansMap = toMap(apiPlans, 'Name'); const vpnBasicPlan = plansMap[PLANS.VPNBASIC]; const vpnPlusPlan = plansMap[PLANS.PROFESSIONAL]; @@ -96,16 +96,20 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c ]; return ( - onSelect(plans[index].planID)} - /> +
+ onSelect(plans[index].planID)} + currentPlan={currentPlan} + /> +
); }; VpnSubscriptionTable.propTypes = { + currentPlan: PropTypes.string, planNameSelected: PropTypes.string, plans: PropTypes.arrayOf(PropTypes.object), onSelect: PropTypes.func.isRequired, From d3810b1e9e6062858f61cd289a0571496ede4ab2 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 6 Dec 2019 18:13:07 +0100 Subject: [PATCH 016/242] Continue --- .../subscription/MailSubscriptionTable.js | 12 +++++--- .../subscription/NewSubscriptionModal.js | 14 ++++++++-- .../subscription/NewSubscriptionModal.scss | 14 ++++++++++ .../subscription/SubscriptionCheckout.js | 8 +++--- .../subscription/SubscriptionCustomization.js | 28 +++++++++++++++---- .../subscription/SubscriptionTable.js | 11 ++++---- .../subscription/VpnSubscriptionTable.js | 21 ++++++++------ 7 files changed, 79 insertions(+), 29 deletions(-) diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 906996f77..ecbdbe841 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -32,7 +32,8 @@ const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, const visionaryPlan = plansMap[PLANS.VISIONARY]; const plans = [ { - planName: 'Free', + name: '', + title: 'Free', canCustomize: true, price: , imageSrc: freePlanSvg, @@ -64,8 +65,9 @@ const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, ] }, plusPlan && { + name: plusPlan.Name, planID: plusPlan.ID, - planName: PLAN_NAMES[PLANS.PLUS], + title: PLAN_NAMES[PLANS.PLUS], canCustomize: true, price: , imageSrc: plusPlanSvg, @@ -97,8 +99,9 @@ const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, ] }, professionalPlan && { + name: professionalPlan.Name, planID: professionalPlan.ID, - planName: PLAN_NAMES[PLANS.PROFESSIONAL], + title: PLAN_NAMES[PLANS.PROFESSIONAL], canCustomize: true, price: ( , imageSrc: visionaryPlanSvg, diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 0d6077241..9917e3fdf 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,7 +1,15 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { FormModal, usePlans, useApi, useLoading, useSubscription, useNotifications } from 'react-components'; +import { + FormModal, + usePlans, + useApi, + useLoading, + useSubscription, + useNotifications, + useVPNCountries +} from 'react-components'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; @@ -47,6 +55,7 @@ const NewSubscriptionModal = ({ }; const api = useApi(); + const [vpnCountries, loadingVpnCountries] = useVPNCountries(); const [plans, loadingPlans] = usePlans(); const [subscription, loadingSubscription] = useSubscription(); const [loading, withLoading] = useLoading(); @@ -109,13 +118,14 @@ const NewSubscriptionModal = ({ footer={null} className="pm-modal--full subscription-modal" title={TITLE[step]} - loading={loading || loadingPlans || loadingSubscription} + loading={loading || loadingPlans || loadingSubscription || loadingVpnCountries} {...rest} > {step === STEPS.CUSTOMIZATION && (
Name === ADDON_NAMES.VPN); const [organization, loadingOrganization] = useOrganization(); const loyal = isLoyal(organization); - const subTotal = getSubTotal({ plansMap: model.planIDs, cycle: CYCLE.MONTHLY, plans }); + const subTotal = getSubTotal({ plansMap: model.planIDs, cycle: model.cycle, plans }) / model.cycle; const total = checkResult.Amount + checkResult.CouponDiscount; const monthlyTotal = total / model.cycle; - const discount = total - subTotal; + const discount = monthlyTotal - subTotal; const collection = orderBy( Object.entries(model.planIDs).map(([planID, quantity]) => ({ ...plansMap[planID], quantity })), 'Type' @@ -105,7 +105,7 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, key={ID} className={Type === PLAN_TYPES.PLAN ? 'bold' : ''} title={Type === PLAN_TYPES.PLAN ? Title : getTitle(Name, quantity)} - amount={quantity * Pricing[CYCLE.MONTHLY]} + amount={(quantity * Pricing[model.cycle]) / model.cycle} currency={model.currency} /> ); @@ -176,7 +176,7 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout,
- {model.coupon || [CYCLE.YEARLY, CYCLE.TWO_YEARS].includes(model.cycle) ? ( + {model.coupon ? (
{ +const SubscriptionCustomization = ({ vpnCountries = {}, plans, model, setModel, expanded = false }) => { const { CLIENT_TYPE } = useConfig(); const plansMap = toMap(plans, 'Name'); const plusPlan = plansMap[PLANS.PLUS]; @@ -161,8 +161,7 @@ const SubscriptionCustomization = ({ plans, model, setModel, expanded = false }) [PLANS.VPNPLUS]: c('Decription') .t`VPN connections can be allocated to users within your organization. Each device requires one connection.`, [PLANS.PROFESSIONAL]: c('Decription') - .t`Each additional user comes automatically with 5 GB storage space and 5 email addresses.`, - [PLANS.PLUS]: c('Decription').t`.` + .t`Each additional user comes automatically with 5 GB storage space and 5 email addresses.` }; const FEATURES = { @@ -254,19 +253,31 @@ const SubscriptionCustomization = ({ plans, model, setModel, expanded = false }) ], [VPNFREE]: [ , - , + , , ], [PLANS.VPNBASIC]: [ , - , // TODO + , , ], [PLANS.VPNPLUS]: [ , - , // TODO + , ,
- {plans.map(({ planName, price, imageSrc, description, features = [], canCustomize }, index) => { + {plans.map(({ name, title, price, imageSrc, description, features = [], canCustomize }, index) => { return ( -
+
) : null} -
{planName}
+
{title}
{price}
- {planName} + {title}

@@ -120,7 +120,8 @@ SubscriptionTable.propTypes = { currentPlan: PropTypes.string, plans: PropTypes.arrayOf( PropTypes.shape({ - planName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, price: PropTypes.node.isRequired, imageSrc: PropTypes.string.isRequired, description: PropTypes.node.isRequired, diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index eb294cb85..358e4bada 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -1,5 +1,5 @@ import React from 'react'; -import { SubscriptionTable } from 'react-components'; +import { SubscriptionTable, useVPNCountries } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; import { toMap } from 'proton-shared/lib/helpers/object'; @@ -30,15 +30,17 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c const vpnBasicPlan = plansMap[PLANS.VPNBASIC]; const vpnPlusPlan = plansMap[PLANS.PROFESSIONAL]; const visionaryPlan = plansMap[PLANS.VISIONARY]; + const [vpnCountries] = useVPNCountries(); const plans = [ { - planName: 'Free', + name: '', + title: 'Free', price: , imageSrc: freePlanSvg, description: c('Description').t`Privacy and security for everyone`, features: [ c('Feature').t`1 VPN connection`, - c('Feature').t`Servers in 3 countries`, + c('Feature').t`Servers in ${vpnCountries.free.length} countries`, c('Feature').t`Medium speed`, c('Feature').t`No logs/No ads`, {c('Feature').t`Filesharing/bitorrent support`}, @@ -48,14 +50,15 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c ] }, vpnBasicPlan && { + name: vpnBasicPlan.Name, planID: vpnBasicPlan.ID, - planName: PLAN_NAMES[PLANS.VPNBASIC], + title: PLAN_NAMES[PLANS.VPNBASIC], price: , imageSrc: plusPlanSvg, description: c('Description').t`Basic privacy features`, features: [ c('Feature').t`2 VPN connection`, - c('Feature').t`Servers in XX countries`, // TODO + c('Feature').t`Servers in ${vpnCountries.basic.length} countries`, c('Feature').t`High speed`, c('Feature').t`No logs/No ads`, {c('Feature').t`Filesharing/bitorrent support`}, @@ -65,14 +68,15 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c ] }, vpnPlusPlan && { + name: vpnPlusPlan.Name, planID: vpnPlusPlan.ID, - planName: PLAN_NAMES[PLANS.VPNPLUS], + title: PLAN_NAMES[PLANS.VPNPLUS], price: , imageSrc: professionalPlanSvg, description: c('Description').t`Advanced security features`, features: [ c('Feature').t`5 VPN connection`, - c('Feature').t`Servers in XX countries`, // TODO + c('Feature').t`Servers in ${vpnCountries.all.length} countries`, c('Feature').t`Highest speed (10 Gbps)`, c('Feature').t`No logs/No ads`, c('Feature').t`Filesharing/bitorrent support`, @@ -82,8 +86,9 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c ] }, visionaryPlan && { + name: visionaryPlan.Name, planID: visionaryPlan.ID, - planName: PLAN_NAMES[PLANS.VISIONARY], + title: PLAN_NAMES[PLANS.VISIONARY], price: , imageSrc: visionaryPlanSvg, description: c('Description').t`The complete privacy suite`, From 912b87b634afa5efc974aa085669aa82be232d0a Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 9 Dec 2019 13:31:37 +0100 Subject: [PATCH 017/242] Continue --- containers/payments/DiscountBadge.js | 33 ++------- .../subscription/MailSubscriptionTable.js | 11 ++- .../subscription/NewSubscriptionModal.js | 3 + .../subscription/SubscriptionAddonRow.js | 6 +- .../subscription/SubscriptionCheckout.js | 25 +++++-- .../subscription/SubscriptionCustomization.js | 72 +++++++++++-------- .../payments/subscription/SubscriptionPlan.js | 6 +- .../subscription/SubscriptionTable.js | 14 ++-- .../subscription/VpnSubscriptionTable.js | 38 ++++++++-- 9 files changed, 127 insertions(+), 81 deletions(-) diff --git a/containers/payments/DiscountBadge.js b/containers/payments/DiscountBadge.js index e7f859c9f..b02c49477 100644 --- a/containers/payments/DiscountBadge.js +++ b/containers/payments/DiscountBadge.js @@ -2,36 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; import { Badge } from 'react-components'; -import { COUPON_CODES, BLACK_FRIDAY, CYCLE } from 'proton-shared/lib/constants'; - -import CycleDiscountBadge from './CycleDiscountBadge'; +import { COUPON_CODES, BLACK_FRIDAY } from 'proton-shared/lib/constants'; const { BUNDLE, PMTEAM } = COUPON_CODES; -const DiscountBadge = ({ code, cycle }) => { +const DiscountBadge = ({ code }) => { if (code === BUNDLE) { - if (cycle === CYCLE.YEARLY) { - return ( - - -36% - - ); - } - - if (cycle === CYCLE.TWO_YEARS) { - return ( - - -47% - - ); - } - return ( -20% @@ -54,12 +30,11 @@ const DiscountBadge = ({ code, cycle }) => { return -100%; } - return ; + return {code}; }; DiscountBadge.propTypes = { - code: PropTypes.string, - cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]) + code: PropTypes.string }; export default DiscountBadge; diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index ecbdbe841..0a358d614 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -25,7 +25,15 @@ const FREE_PLAN = { } }; -const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, currency, onSelect, currentPlan }) => { +const MailSubscriptionTable = ({ + planNameSelected, + plans: apiPlans = [], + cycle, + currency, + onSelect, + currentPlan, + ...rest +}) => { const plansMap = toMap(apiPlans, 'Name'); const plusPlan = plansMap[PLANS.PLUS]; const professionalPlan = plansMap[PLANS.PROFESSIONAL]; @@ -183,6 +191,7 @@ const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, mostPopularIndex={1} plans={plans} onSelect={(index) => onSelect(plans[index].planID)} + {...rest} />

* {c('Info concerning plan features').t`Customizable features`}

diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 9917e3fdf..c4eac4356 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -106,6 +106,9 @@ const NewSubscriptionModal = ({ }; const handleCheckout = () => { + if (!checkResult.AmountDue) { + return handleSubscribe(); + } setStep(STEPS.PAYMENT); }; diff --git a/containers/payments/subscription/SubscriptionAddonRow.js b/containers/payments/subscription/SubscriptionAddonRow.js index ec72f0f4f..391e2ca5b 100644 --- a/containers/payments/subscription/SubscriptionAddonRow.js +++ b/containers/payments/subscription/SubscriptionAddonRow.js @@ -31,10 +31,10 @@ const SubscriptionAddonRow = ({ -

+
)} {canCustomize ? ( @@ -131,7 +134,10 @@ SubscriptionTable.propTypes = { ), onSelect: PropTypes.func.isRequired, currentPlanIndex: PropTypes.number, - mostPopularIndex: PropTypes.number + mostPopularIndex: PropTypes.number, + selected: PropTypes.string, + update: PropTypes.string, + select: PropTypes.string }; export default SubscriptionTable; diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index 358e4bada..f5bc315d6 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -25,10 +25,18 @@ const FREE_PLAN = { } }; -const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, currency, onSelect, currentPlan }) => { +const VpnSubscriptionTable = ({ + planNameSelected, + plans: apiPlans = [], + cycle, + currency, + onSelect, + currentPlan, + ...rest +}) => { const plansMap = toMap(apiPlans, 'Name'); const vpnBasicPlan = plansMap[PLANS.VPNBASIC]; - const vpnPlusPlan = plansMap[PLANS.PROFESSIONAL]; + const vpnPlusPlan = plansMap[PLANS.VPNPLUS]; const visionaryPlan = plansMap[PLANS.VISIONARY]; const [vpnCountries] = useVPNCountries(); const plans = [ @@ -47,6 +55,18 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c {c('Feature').t`Secure Core and Tor VPN`}, {c('Feature').t`Advanced privacy features`}, {c('Feature').t`Access blocked content`} + ], + allFeatures: [ + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t``, + c('Feature').t`` ] }, vpnBasicPlan && { @@ -57,7 +77,7 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c imageSrc: plusPlanSvg, description: c('Description').t`Basic privacy features`, features: [ - c('Feature').t`2 VPN connection`, + c('Feature').t`2 VPN connections`, c('Feature').t`Servers in ${vpnCountries.basic.length} countries`, c('Feature').t`High speed`, c('Feature').t`No logs/No ads`, @@ -65,7 +85,8 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c {c('Feature').t`Secure Core and Tor VPN`}, {c('Feature').t`Advanced privacy features`}, {c('Feature').t`Access blocked content`} - ] + ], + allFeatures: [c('Feature').t``] }, vpnPlusPlan && { name: vpnPlusPlan.Name, @@ -75,7 +96,7 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c imageSrc: professionalPlanSvg, description: c('Description').t`Advanced security features`, features: [ - c('Feature').t`5 VPN connection`, + c('Feature').t`5 VPN connections`, c('Feature').t`Servers in ${vpnCountries.all.length} countries`, c('Feature').t`Highest speed (10 Gbps)`, c('Feature').t`No logs/No ads`, @@ -83,7 +104,8 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c c('Feature').t`Secure Core and Tor VPN`, c('Feature').t`Advanced privacy features`, c('Feature').t`Access blocked content` - ] + ], + allFeatures: [c('Feature').t``] }, visionaryPlan && { name: visionaryPlan.Name, @@ -96,7 +118,8 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c c('Feature').t`All Plus plan features`, c('Feature').t`10 simultaneous VPN connections`, c('Feature').t`ProtonMail Visionary account` - ] + ], + allFeatures: [c('Feature').t``] } ]; @@ -108,6 +131,7 @@ const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], cycle, c plans={plans} onSelect={(index) => onSelect(plans[index].planID)} currentPlan={currentPlan} + {...rest} />
); From 50918ee8906d6fcbb4f7fed4a4fdb8d7c6bcaea1 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 9 Dec 2019 15:05:30 +0100 Subject: [PATCH 018/242] Continue --- containers/payments/CurrencySelector.js | 2 + containers/payments/CycleSelector.js | 4 +- containers/payments/DiscountBadge.js | 23 ++++++--- containers/payments/PlansSection.js | 10 +++- .../subscription/MailSubscriptionTable.js | 2 +- .../subscription/NewSubscriptionModal.js | 50 +++++++------------ .../subscription/SubscriptionAddonRow.js | 9 ++-- .../subscription/SubscriptionCheckout.js | 17 ++++++- .../subscription/SubscriptionCustomization.js | 20 ++++++-- .../subscription/SubscriptionTable.js | 2 +- .../subscription/VpnSubscriptionTable.js | 2 +- 11 files changed, 88 insertions(+), 53 deletions(-) diff --git a/containers/payments/CurrencySelector.js b/containers/payments/CurrencySelector.js index bcfda3b48..9e3469b7f 100644 --- a/containers/payments/CurrencySelector.js +++ b/containers/payments/CurrencySelector.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Select, Group, ButtonGroup, classnames } from 'react-components'; import { CURRENCIES, DEFAULT_CURRENCY } from 'proton-shared/lib/constants'; +import { c } from 'ttag'; const addSymbol = (currency) => { if (currency === 'EUR') { @@ -40,6 +41,7 @@ const CurrencySelector = ({ currency = DEFAULT_CURRENCY, onSelect, mode = 'selec if (mode === 'select') { return ( ; + return ( + onChange(quantity + 1)} - disabled={quantity === max} + disabled={loading || quantity === max} icon="plus" />
@@ -64,6 +66,7 @@ const SubscriptionAddonRow = ({ }; SubscriptionAddonRow.propTypes = { + loading: PropTypes.bool, label: PropTypes.node.isRequired, price: PropTypes.node.isRequired, start: PropTypes.number, diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index 95db8672d..51df41d5f 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -55,7 +55,16 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, const vpnAddon = plans.find(({ Name }) => Name === ADDON_NAMES.VPN); const [organization, loadingOrganization] = useOrganization(); const loyal = isLoyal(organization); - const subTotal = getSubTotal({ plansMap: model.planIDs, cycle: model.cycle, plans }) / model.cycle; + const subTotal = + getSubTotal({ + cycle: model.cycle, + plans, + plansMap: Object.entries(model.planIDs).reduce((acc, [planID, quantity]) => { + const { Name } = plansMap[planID]; + acc[Name] = quantity; + return acc; + }, {}) + }) / model.cycle; const total = checkResult.Amount + checkResult.CouponDiscount; const monthlyTotal = total / model.cycle; const discount = monthlyTotal - subTotal; @@ -132,7 +141,11 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, currency={model.currency} onSelect={(newCurrency) => setModel({ ...model, currency: newCurrency })} /> - setModel({ ...model, cycle: newCycle })} /> + setModel({ ...model, cycle: newCycle })} + />
{c('Title') diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js index 8405cc201..c89af8906 100644 --- a/containers/payments/subscription/SubscriptionCustomization.js +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -112,7 +112,14 @@ Description.propTypes = { setModel: PropTypes.func.isRequired }; -const SubscriptionCustomization = ({ vpnCountries = {}, plans, model, setModel, expanded = false }) => { +const SubscriptionCustomization = ({ + vpnCountries = {}, + plans, + model, + setModel, + expanded = false, + loading = false +}) => { const { CLIENT_TYPE } = useConfig(); const plansMap = toMap(plans, 'Name'); const plusPlan = plansMap[PLANS.PLUS]; @@ -126,7 +133,7 @@ const SubscriptionCustomization = ({ vpnCountries = {}, plans, model, setModel, const vpnAddon = plansMap[ADDON_NAMES.VPN]; const hasVisionary = !!model.planIDs[visionaryPlan.ID]; const [addresses, loadingAddresses] = useAddresses(); - const hasMailActive = addresses.length > 0; + const hasAddresses = Array.isArray(addresses) && addresses.length > 0; const { mailPlan, vpnPlan } = Object.entries(model.planIDs).reduce( (acc, [planID, quantity]) => { @@ -293,6 +300,7 @@ const SubscriptionCustomization = ({ vpnCountries = {}, plans, model, setModel, const ADDONS = { [PLANS.PLUS]: [ , , ,

{TITLE[mailPlan.Name]}

@@ -469,6 +482,7 @@ const SubscriptionCustomization = ({ vpnCountries = {}, plans, model, setModel, }; SubscriptionCustomization.propTypes = { + loading: PropTypes.bool, vpnCountries: PropTypes.shape({ free: PropTypes.array, basic: PropTypes.array, diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index c62ffb9bb..96f06bc47 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -69,7 +69,7 @@ const SubscriptionTable = ({ {canCustomize ? ( onSelect(index)} + onClick={() => onSelect(index, true)} >{c('Action').t`Customize`} ) : null} diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index f5bc315d6..524d9afb7 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -129,7 +129,7 @@ const VpnSubscriptionTable = ({ currentPlanIndex={INDEXES[planNameSelected] || 0} mostPopularIndex={2} plans={plans} - onSelect={(index) => onSelect(plans[index].planID)} + onSelect={(index, expanded) => onSelect(plans[index].planID, expanded)} currentPlan={currentPlan} {...rest} /> From 40ad5c4ef918a11876ca90b9dbc86db47807b2c4 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 9 Dec 2019 17:32:52 +0100 Subject: [PATCH 019/242] Continue --- components/modal/Footer.js | 2 +- .../subscription/NewSubscriptionModal.js | 108 +++++++++++++++--- .../subscription/SubscriptionCheckout.js | 2 +- .../subscription/SubscriptionCustomization.js | 4 +- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/components/modal/Footer.js b/components/modal/Footer.js index 46d97c3ff..16e53f22a 100644 --- a/components/modal/Footer.js +++ b/components/modal/Footer.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { classnames } from '../../helpers/component'; -const Footer = ({ children, className = 'flex flex-spacebetween', ...rest }) => { +const Footer = ({ children, className = 'flex flex-spacebetween flex-items-center', ...rest }) => { return (
{children} diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 5ee1f5259..ce5a2b767 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,8 +1,18 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { FormModal, usePlans, useApi, useLoading, useSubscription, useVPNCountries } from 'react-components'; -import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; +import { + FormModal, + PrimaryButton, + Button, + Icon, + usePlans, + useApi, + useLoading, + useSubscription, + useVPNCountries +} from 'react-components'; +import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES, COUPON_CODES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; import SubscriptionCustomization from './SubscriptionCustomization'; @@ -20,6 +30,71 @@ const STEPS = { THANKS: 3 }; +const Footer = ({ step, model, checkResult, loading }) => { + if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { + return null; + } + + const cancel = step === STEPS.CUSTOMIZATION ? c('Action').t`Cancel` : c('Action').t`Back`; + const submit = + step === STEPS.CUSTOMIZATION + ? checkResult.AmountDue + ? c('Action').t`Continue` + : c('Action').t`Finish` + : c('Action').t`Finish`; + const upsells = [ + model.cycle === CYCLE.MONTHLY && ( +
+ + {c('Info').t`Save 20% by switching to annual billing`} +
+ ), // TODO + model.cycle === CYCLE.YEARLY && ( +
+ + {c('Info').t`You are saving 20% with annual billing`} +
+ ), // TODO + model.cycle === CYCLE.TWO_YEARS && ( +
+ + {c('Info').t`You are saving 33% with 2-year billing`} +
+ ), // TODO + model.coupon !== COUPON_CODES.BUNDLE && ( +
+ + {c('Info').t`Save an extra 20% by combining Mail and VPN`} +
+ ), // TODO + model.coupon === COUPON_CODES.BUNDLE && ( +
+ + {c('Info').t`You are saving an extra 20% with the bundle discount`} +
+ ) // TODO + ].filter(Boolean); + + return ( + <> + + {upsells} + + {submit} + + + ); +}; + +Footer.propTypes = { + loading: PropTypes.bool, + step: PropTypes.number, + model: PropTypes.object, + checkResult: PropTypes.object +}; + const clearPlanIDs = (planIDs = {}) => { return Object.entries(planIDs).reduce((acc, [planID, quantity]) => { if (!quantity) { @@ -101,7 +176,14 @@ const NewSubscriptionModal = ({ return ( + } className="pm-modal--full subscription-modal" title={TITLE[step]} loading={loading || loadingPlans || loadingSubscription || loadingVpnCountries} @@ -119,17 +201,15 @@ const NewSubscriptionModal = ({ setModel={setModel} />
-
-
- -
+
+
)} diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index 51df41d5f..fc8218e16 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -260,7 +260,7 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, />
- {checkResult.AmountDue ? c('Action').t`Checkout` : c('Action').t`Confirm`} + {checkResult.AmountDue ? c('Action').t`Continue` : c('Action').t`Finish`}
diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js index c89af8906..741520370 100644 --- a/containers/payments/subscription/SubscriptionCustomization.js +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -419,7 +419,7 @@ const SubscriptionCustomization = ({ ...model, planIDs: { ...removeService(model.planIDs, plans, PLAN_SERVICES.MAIL), - [planID]: 1 + ...(planID ? { [planID]: 1 } : {}) } }); }} @@ -452,7 +452,7 @@ const SubscriptionCustomization = ({ ...model, planIDs: { ...removeService(model.planIDs, plans, PLAN_SERVICES.VPN), - [planID]: 1 + ...(planID ? { [planID]: 1 } : {}) } }); }} From 0e4547f51ce51b42c895393a616d47f8b464beb9 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 9 Dec 2019 17:41:18 +0100 Subject: [PATCH 020/242] Continue --- containers/payments/subscription/NewSubscriptionModal.scss | 5 +++++ containers/payments/subscription/SubscriptionTable.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/containers/payments/subscription/NewSubscriptionModal.scss b/containers/payments/subscription/NewSubscriptionModal.scss index feddba7c2..34939872e 100644 --- a/containers/payments/subscription/NewSubscriptionModal.scss +++ b/containers/payments/subscription/NewSubscriptionModal.scss @@ -26,4 +26,9 @@ .subscription-modal .subscriptionTable-description, .subscription-modal .subscriptionTable-footer { min-height: auto; +} + +// Hide all features row in the subscription modal +.subscription-modal .subscriptionTable-all-features-container { + display: none; } \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 96f06bc47..3920d46ce 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -78,9 +78,9 @@ const SubscriptionTable = ({ ); })}
-
+
- {showAllFeatures ? c('Action').t`Close feature comparison` : c('Action').t`Compare all features`} + {showAllFeatures ? c('Action').t`Hide additional features` : c('Action').t`Show all features`}
{showAllFeatures ? ( From 0101d479d7f172c6faab46bb965f6e0da16d2f55 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 10 Dec 2019 12:09:15 +0100 Subject: [PATCH 021/242] Continue --- .../subscription/MailFeaturesTable.js | 181 ++++++++++++++++++ .../subscription/MailSubscriptionTable.js | 90 ++------- .../subscription/NewSubscriptionModal.js | 91 ++------- .../subscription/NewSubscriptionModal.scss | 5 - .../NewSubscriptionModalFooter.js | 80 ++++++++ .../subscription/SubscriptionCheckout.js | 17 +- .../subscription/SubscriptionFeaturesModal.js | 27 +++ .../subscription/SubscriptionPrices.js | 12 +- .../subscription/SubscriptionTable.js | 41 +--- .../payments/subscription/VpnFeaturesTable.js | 150 +++++++++++++++ .../subscription/VpnSubscriptionTable.js | 43 ++--- 11 files changed, 502 insertions(+), 235 deletions(-) create mode 100644 containers/payments/subscription/MailFeaturesTable.js create mode 100644 containers/payments/subscription/NewSubscriptionModalFooter.js create mode 100644 containers/payments/subscription/SubscriptionFeaturesModal.js create mode 100644 containers/payments/subscription/VpnFeaturesTable.js diff --git a/containers/payments/subscription/MailFeaturesTable.js b/containers/payments/subscription/MailFeaturesTable.js new file mode 100644 index 000000000..31eabdb34 --- /dev/null +++ b/containers/payments/subscription/MailFeaturesTable.js @@ -0,0 +1,181 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { usePlans, Loader } from 'react-components'; +import { toMap } from 'proton-shared/lib/helpers/object'; +import { PLANS } from 'proton-shared/lib/constants'; +import { c } from 'ttag'; + +import SubscriptionPrices from './SubscriptionPrices'; + +const MailFeaturesTable = ({ cycle, currency }) => { + const [plans, loadingPlans] = usePlans(); + const plansMap = toMap(plans, 'Name'); + + if (loadingPlans) { + return ; + } + + return ( + <> +
+ {feature}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Free
+ +
+
Plus
+ +
+
Professional
+ +
+
Visionary
+ +
{c('Feature').t`1 user`}{c('Feature').t`1 user`}{c('Feature').t`1 - 5000 users *`}{c('Feature').t`6 users`}
{c('Feature').t`0.5 GB storage`}{c('Feature').t`5 GB storage *`}{c('Feature').t`5 GB storage / user *`}{c('Feature').t`20 GB storage`}
{c('Feature').t`1 address`}{c('Feature').t`5 addresses *`}{c('Feature').t`5 addresses / user *`}{c('Feature').t`50 addresses`}
{c('Feature').t`No domain support`}{c('Feature').t`1 custom domain *`}{c('Feature').t`2 custom domains *`}{c('Feature').t`10 custom domains`}
{c('Feature').t`150 messages per day`}{c('Feature').t`Unlimited messages **`}{c('Feature').t`Unlimited messages **`}{c('Feature').t`Unlimited messages **`}
{c('Feature').t`3 folders / labels`}{c('Feature').t`200 folders / labels`}{c('Feature').t`Unlimited folders / labels`}{c('Feature').t`Unlimited folders / labels`}
{c('Feature').t`Limited support`}{c('Feature').t`Priority support`}{c('Feature').t`Priority support`}{c('Feature').t`Priority support`}
+ {c('Feature').t`Encrypted contacts`} + {c('Feature').t`Encrypted contacts`}{c('Feature').t`Encrypted contacts`}{c('Feature').t`Encrypted contacts`}
+ {c('Feature').t`Address verification`} + {c('Feature').t`Address verification`}{c('Feature').t`Address verification`}{c('Feature').t`Address verification`}
+ {c('Feature').t`Custom filters`} + {c('Feature').t`Custom filters`}{c('Feature').t`Custom filters`}{c('Feature').t`Custom filters`}
+ {c('Feature').t`IMAP/SMTP support`} + {c('Feature').t`IMAP/SMTP support`}{c('Feature').t`IMAP/SMTP support`}{c('Feature').t`IMAP/SMTP support`}
+ {c('Feature').t`Autoresponder`} + {c('Feature').t`Autoresponder`}{c('Feature').t`Autoresponder`}{c('Feature').t`Autoresponder`}
+ {c('Feature').t`@pm.me short domain`} + {c('Feature').t`@pm.me short domain`}{c('Feature').t`@pm.me short domain`}{c('Feature').t`@pm.me short domain`}
+ {c('Feature').t`Catch all email`} + + {c('Feature').t`Catch all email`} + {c('Feature').t`Catch all email`}{c('Feature').t`Catch all email`}
+ {c('Feature').t`Multi user support`} + + {c('Feature').t`Multi user support`} + {c('Feature').t`Multi user support`}{c('Feature').t`Multi user support`}
+ {c('Feature').t`ProtonVPN included`} + + {c('Feature').t`ProtonVPN included`} + + {c('Feature').t`ProtonVPN included`} + {c('Feature').t`ProtonVPN included`}
+

* {c('Info concerning plan features').t`Denotes customizable features`}

+

+ **{' '} + {c('Info concerning plan features') + .t`ProtonMail cannot be used for mass emailing or spamming. Legitimate emails are unlimited.`} +

+ + ); +}; + +MailFeaturesTable.propTypes = { + cycle: PropTypes.number, + currency: PropTypes.string +}; + +export default MailFeaturesTable; diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 1b4899413..6ec739ccb 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -1,5 +1,5 @@ import React from 'react'; -import { SubscriptionTable } from 'react-components'; +import { SubscriptionTable, LinkButton, useModals } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; import { toMap } from 'proton-shared/lib/helpers/object'; @@ -10,6 +10,7 @@ import professionalPlanSvg from 'design-system/assets/img/pm-images/professional import visionaryPlanSvg from 'design-system/assets/img/pm-images/visionary-plan.svg'; import SubscriptionPrices from './SubscriptionPrices'; +import SubscriptionFeaturesModal from './SubscriptionFeaturesModal'; const INDEXES = { [PLANS.PLUS]: 1, @@ -17,14 +18,6 @@ const INDEXES = { [PLANS.VISIONARY]: 3 }; -const FREE_PLAN = { - Pricing: { - [CYCLE.MONTHLY]: 0, - [CYCLE.YEARLY]: 0, - [CYCLE.TWO_YEARS]: 0 - } -}; - const MailSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], @@ -34,6 +27,7 @@ const MailSubscriptionTable = ({ currentPlan, ...rest }) => { + const { createModal } = useModals(); const plansMap = toMap(apiPlans, 'Name'); const plusPlan = plansMap[PLANS.PLUS]; const professionalPlan = plansMap[PLANS.PROFESSIONAL]; @@ -43,7 +37,7 @@ const MailSubscriptionTable = ({ name: '', title: 'Free', canCustomize: true, - price: , + price: , imageSrc: freePlanSvg, description: c('Description').t`Basic private and secure comminications`, features: [ @@ -53,23 +47,6 @@ const MailSubscriptionTable = ({ c('Feature').t`No domain support`, c('Feature').t`150 messages/day`, c('Feature').t`ProtonVPN (optional)` - ], - allFeatures: [ - c('Feature').t`1 user`, - c('Feature').t`500 MB storage`, - c('Feature').t`1 address`, - c('Feature').t`No domain support`, - c('Feature').t`150 messages per day`, - c('Feature').t`3 folders/labels`, - {c('Feature').t`Encrypted contacts`}, - {c('Feature').t`Address verification`}, - {c('Feature').t`Filters`}, - {c('Feature').t`IMAP/SMTP support`}, - {c('Feature').t`Auto-responder`}, - {c('Feature').t`@pm.me short email`}, - {c('Feature').t`Catch-all email`}, - {c('Feature').t`Multi-user support`}, - c('Feature').t`Limited support` ] }, plusPlan && { @@ -87,23 +64,6 @@ const MailSubscriptionTable = ({ c('Feature').t`Supports 1 domain *`, c('Feature').t`Folder, labels, filters, auto-reply, IMAP/SMTP and more`, c('Feature').t`ProtonVPN (optional)` - ], - allFeatures: [ - c('Feature').t`1 user`, - c('Feature').t`5 GB storage *`, - c('Feature').t`5 addresses *`, - c('Feature').t`1 custom domain *`, - c('Feature').t`Unlimited messages **`, - c('Feature').t`Unlimited folders/labels`, - c('Feature').t`Encrypted contacts`, - c('Feature').t`Address verification`, - c('Feature').t`Filters`, - c('Feature').t`IMAP/SMTP support`, - c('Feature').t`Auto-responder`, - c('Feature').t`@pm.me short email`, - {c('Feature').t`Catch-all email`}, - {c('Feature').t`Multi-user support`}, - c('Feature').t`Normal support` ] }, professionalPlan && { @@ -128,23 +88,6 @@ const MailSubscriptionTable = ({ c('Feature').t`Supports 2 domains *`, c('Feature').t`Catch-all email, multi-user management`, c('Feature').t`Priority support` - ], - allFeatures: [ - c('Feature').t`1-5000 user`, - c('Feature').t`5 GB per user *`, - c('Feature').t`5 addresses per user *`, - c('Feature').t`2 custom domains *`, - c('Feature').t`Unlimited messages **`, - c('Feature').t`Unlimited folders/labels`, - c('Feature').t`Encrypted contacts`, - c('Feature').t`Address verification`, - c('Feature').t`Filters`, - c('Feature').t`IMAP/SMTP support`, - c('Feature').t`Auto-responder`, - c('Feature').t`@pm.me short email`, - c('Feature').t`Catch-all email`, - c('Feature').t`Multi-user support`, - c('Feature').t`Priority support` ] }, visionaryPlan && { @@ -162,23 +105,6 @@ const MailSubscriptionTable = ({ c('Feature').t`Supports 10 domains`, c('Feature').t`Includes all features`, c('Feature').t`Includes ProtonVPN` - ], - allFeatures: [ - c('Feature').t`6 users`, - c('Feature').t`20 GB storage`, - c('Feature').t`50 addresses`, - c('Feature').t`10 custom domains *`, - c('Feature').t`Unlimited messages **`, - c('Feature').t`Unlimited folders/labels`, - c('Feature').t`Encrypted contacts`, - c('Feature').t`Address verification`, - c('Feature').t`Filters`, - c('Feature').t`IMAP/SMTP support`, - c('Feature').t`Auto-responder`, - c('Feature').t`@pm.me short email`, - c('Feature').t`Catch-all email`, - c('Feature').t`Multi-user support`, - c('Feature').t`Priority support` ] } ]; @@ -199,6 +125,14 @@ const MailSubscriptionTable = ({ {c('Info concerning plan features') .t`ProtonMail cannot be used for mass emailing or spamming. Legitimate emails are unlimited.`}

+
+ createModal()} + > + {c('Action').t`Show all features`} + +
); }; diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index ce5a2b767..3381dfa90 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,18 +1,8 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { - FormModal, - PrimaryButton, - Button, - Icon, - usePlans, - useApi, - useLoading, - useSubscription, - useVPNCountries -} from 'react-components'; -import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES, COUPON_CODES } from 'proton-shared/lib/constants'; +import { FormModal, usePlans, useApi, useLoading, useSubscription, useVPNCountries } from 'react-components'; +import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; import SubscriptionCustomization from './SubscriptionCustomization'; @@ -20,6 +10,7 @@ import SubscriptionPayment from './SubscriptionPayment'; import SubscriptionUpgrade from './SubscriptionUpgrade'; import SubscriptionThanks from './SubscriptionThanks'; import SubscriptionCheckout from './SubscriptionCheckout'; +import NewSubscriptionModalFooter from './NewSubscriptionModalFooter'; import './NewSubscriptionModal.scss'; @@ -30,71 +21,6 @@ const STEPS = { THANKS: 3 }; -const Footer = ({ step, model, checkResult, loading }) => { - if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { - return null; - } - - const cancel = step === STEPS.CUSTOMIZATION ? c('Action').t`Cancel` : c('Action').t`Back`; - const submit = - step === STEPS.CUSTOMIZATION - ? checkResult.AmountDue - ? c('Action').t`Continue` - : c('Action').t`Finish` - : c('Action').t`Finish`; - const upsells = [ - model.cycle === CYCLE.MONTHLY && ( -
- - {c('Info').t`Save 20% by switching to annual billing`} -
- ), // TODO - model.cycle === CYCLE.YEARLY && ( -
- - {c('Info').t`You are saving 20% with annual billing`} -
- ), // TODO - model.cycle === CYCLE.TWO_YEARS && ( -
- - {c('Info').t`You are saving 33% with 2-year billing`} -
- ), // TODO - model.coupon !== COUPON_CODES.BUNDLE && ( -
- - {c('Info').t`Save an extra 20% by combining Mail and VPN`} -
- ), // TODO - model.coupon === COUPON_CODES.BUNDLE && ( -
- - {c('Info').t`You are saving an extra 20% with the bundle discount`} -
- ) // TODO - ].filter(Boolean); - - return ( - <> - - {upsells} - - {submit} - - - ); -}; - -Footer.propTypes = { - loading: PropTypes.bool, - step: PropTypes.number, - model: PropTypes.object, - checkResult: PropTypes.object -}; - const clearPlanIDs = (planIDs = {}) => { return Object.entries(planIDs).reduce((acc, [planID, quantity]) => { if (!quantity) { @@ -135,6 +61,12 @@ const NewSubscriptionModal = ({ planIDs }); const [step, setStep] = useState(initialStep); + const submit = + step === STEPS.CUSTOMIZATION + ? checkResult.AmountDue + ? c('Action').t`Continue` + : c('Action').t`Finish` + : c('Action').t`Pay`; const check = async (newModel = model) => { try { @@ -177,10 +109,10 @@ const NewSubscriptionModal = ({ return ( } @@ -203,6 +135,7 @@ const NewSubscriptionModal = ({
{ + const [addresses, loadingAddresses] = useAddresses(); + + if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { + return null; + } + + if (loadingAddresses) { + return ; + } + + const hasAddresses = Array.isArray(addresses) && addresses.length > 0; + const cancel = step === STEPS.CUSTOMIZATION ? c('Action').t`Cancel` : c('Action').t`Back`; + const upsells = [ + model.cycle === CYCLE.MONTHLY && ( +
+ + {c('Info').t`Save 20% by switching to annual billing`} +
+ ), + model.cycle === CYCLE.YEARLY && ( +
+ + {c('Info').t`You are saving 20% with annual billing`} +
+ ), + model.cycle === CYCLE.TWO_YEARS && ( +
+ + {c('Info').t`You are saving 33% with 2-year billing`} +
+ ), + hasAddresses && model.coupon !== COUPON_CODES.BUNDLE && ( +
+ + {c('Info').t`Save an extra 20% by combining Mail and VPN`} +
+ ), + hasAddresses && model.coupon === COUPON_CODES.BUNDLE && ( +
+ + {c('Info').t`You are saving an extra 20% with the bundle discount`} +
+ ) + ].filter(Boolean); + + return ( + <> + + {upsells} + + {submit} + + + ); +}; + +NewSubscriptionModalFooter.propTypes = { + submit: PropTypes.string, + loading: PropTypes.bool, + step: PropTypes.number, + model: PropTypes.object +}; + +export default NewSubscriptionModalFooter; diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index fc8218e16..a2394aae6 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -34,7 +34,9 @@ const CheckoutRow = ({ title, amount = 0, currency, className = '' }) => { return (
{title}
- {amount} + + {amount} +
); }; @@ -46,7 +48,15 @@ CheckoutRow.propTypes = { currency: PropTypes.string.isRequired }; -const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, loading }) => { +const SubscriptionCheckout = ({ + submit = c('Action').t`Pay`, + plans, + model, + setModel, + checkResult, + onCheckout, + loading +}) => { const plansMap = toMap(plans); const storageAddon = plans.find(({ Name }) => Name === ADDON_NAMES.SPACE); const addressAddon = plans.find(({ Name }) => Name === ADDON_NAMES.ADDRESS); @@ -260,7 +270,7 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, />
- {checkResult.AmountDue ? c('Action').t`Continue` : c('Action').t`Finish`} + {submit}
@@ -282,6 +292,7 @@ const SubscriptionCheckout = ({ plans, model, setModel, checkResult, onCheckout, }; SubscriptionCheckout.propTypes = { + submit: PropTypes.string, plans: PropTypes.array.isRequired, checkResult: PropTypes.object.isRequired, model: PropTypes.object.isRequired, diff --git a/containers/payments/subscription/SubscriptionFeaturesModal.js b/containers/payments/subscription/SubscriptionFeaturesModal.js new file mode 100644 index 000000000..11d2db10d --- /dev/null +++ b/containers/payments/subscription/SubscriptionFeaturesModal.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormModal, useConfig } from 'react-components'; +import { c } from 'ttag'; +import { CYCLE, CLIENT_TYPES, DEFAULT_CURRENCY } from 'proton-shared/lib/constants'; + +import VpnFeaturesTable from './VpnFeaturesTable'; +import MailFeaturesTable from './MailFeaturesTable'; + +const SubscriptionFeaturesModal = ({ cycle = CYCLE.MONTHLY, currency = DEFAULT_CURRENCY, ...rest }) => { + const { CLIENT_TYPE } = useConfig(); + const title = c('Title').t`Plans comparison`; + + return ( + + {CLIENT_TYPE === CLIENT_TYPES.VPN ? : null} + {CLIENT_TYPE === CLIENT_TYPES.MAIL ? : null} + + ); +}; + +SubscriptionFeaturesModal.propTypes = { + cycle: PropTypes.number, + currency: PropTypes.string +}; + +export default SubscriptionFeaturesModal; diff --git a/containers/payments/subscription/SubscriptionPrices.js b/containers/payments/subscription/SubscriptionPrices.js index acfa45747..90f9d27f6 100644 --- a/containers/payments/subscription/SubscriptionPrices.js +++ b/containers/payments/subscription/SubscriptionPrices.js @@ -4,7 +4,15 @@ import { Price } from 'react-components'; import { CYCLE } from 'proton-shared/lib/constants'; import { c } from 'ttag'; -const SubscriptionPrices = ({ cycle, currency, plan, suffix = c('Suffix').t`/month` }) => { +const FREE_PLAN = { + Pricing: { + [CYCLE.MONTHLY]: 0, + [CYCLE.YEARLY]: 0, + [CYCLE.TWO_YEARS]: 0 + } +}; + +const SubscriptionPrices = ({ cycle, currency, plan = FREE_PLAN, suffix = c('Suffix').t`/month` }) => { const billiedAmount = ( {plan.Pricing[cycle]} @@ -31,7 +39,7 @@ SubscriptionPrices.propTypes = { currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired, plan: PropTypes.shape({ Pricing: PropTypes.object - }).isRequired + }) }; export default SubscriptionPrices; diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 3920d46ce..51e1402f6 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useToggle, Button, classnames, LinkButton } from 'react-components'; +import { Button, classnames, LinkButton } from 'react-components'; import { c } from 'ttag'; const SubscriptionTable = ({ @@ -13,8 +13,6 @@ const SubscriptionTable = ({ update = c('Action').t`Update`, select = c('Action').t`Select` }) => { - const { state: showAllFeatures, toggle: toggleFeatures } = useToggle(false); - return (
@@ -78,43 +76,6 @@ const SubscriptionTable = ({ ); })}
-
- - {showAllFeatures ? c('Action').t`Hide additional features` : c('Action').t`Show all features`} - -
- {showAllFeatures ? ( -
- - - {plans[0].allFeatures - .map((f, i) => { - return plans.map(({ allFeatures = [] }) => allFeatures[i]); - }) - .map((features = [], index) => { - return ( - - {features.map((feature, index) => ( - - ))} - - ); - })} - -
- {feature} -
-
- ) : null}
); }; diff --git a/containers/payments/subscription/VpnFeaturesTable.js b/containers/payments/subscription/VpnFeaturesTable.js new file mode 100644 index 000000000..4eb566b4f --- /dev/null +++ b/containers/payments/subscription/VpnFeaturesTable.js @@ -0,0 +1,150 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { usePlans, useVPNCountries, Loader } from 'react-components'; +import { toMap } from 'proton-shared/lib/helpers/object'; +import { PLANS } from 'proton-shared/lib/constants'; +import { c } from 'ttag'; + +import SubscriptionPrices from './SubscriptionPrices'; + +const VpnFeaturesTable = ({ cycle, currency }) => { + const [vpnCountries, loadingVpnCountries] = useVPNCountries(); + const [plans, loadingPlans] = usePlans(); + const plansMap = toMap(plans, 'Name'); + + if (loadingPlans || loadingVpnCountries) { + return ; + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Free
+ +
+
Basic
+ +
+
Plus
+ +
+
Visionary
+ +
{c('Feature').t`1 VPN connection`}{c('Feature').t`2 VPN connections`}{c('Feature').t`5 VPN connections`}{c('Feature').t`10 VPN connections`}
{c('Feature').t`Servers in ${vpnCountries.free.length} countries`}{c('Feature').t`Servers in ${vpnCountries.basic.length} countries`}{c('Feature').t`Servers in ${vpnCountries.all.length} countries`}{c('Feature').t`Servers in ${vpnCountries.all.length} countries`}
{c('Feature').t`Medium speed`}{c('Feature').t`High speed`}{c('Feature').t`High speed`}{c('Feature').t`High speed`}
+ {c('Feature').t`Filesharing / P2P`} + {c('Feature').t`Filesharing / P2P`}{c('Feature').t`Filesharing / P2P`}{c('Feature').t`Filesharing / P2P`}
+ {c('Feature').t`Secure core and Tor VPN`} + + {c('Feature').t`Secure core and Tor VPN`} + {c('Feature').t`Secure core and Tor VPN`}{c('Feature').t`Secure core and Tor VPN`}
+ {c('Feature').t`Advanced privacy features`} + + {c('Feature').t`Advanced privacy features`} + {c('Feature').t`Advanced privacy features`}{c('Feature').t`Advanced privacy features`}
+ {c('Feature').t`Access blocked content`} + + {c('Feature').t`Access blocked content`} + {c('Feature').t`Access blocked content`}{c('Feature').t`Access blocked content`}
{c('Feature').t`ProtonMail (optional) *`}{c('Feature').t`ProtonMail (optional) *`}{c('Feature').t`ProtonMail (optional) *`}{c('Feature').t`ProtonMail included`}
{c('Feature').t`No logs / no ads`}
{c('Feature').t`Perfect Forward Secrecy (PFS)`}
{c('Feature').t`AES-256 encryption`}
{c('Feature').t`DNS leak protection`}
{c('Feature').t`Kill switch`}
{c('Feature').t`Always-on VPN`}
{c('Feature').t`100% anonymous`}
{c('Feature').t`10 Gpbs servers`}
{c('Feature').t`Split tunneling support`}
{c('Feature').t`Swiss based`}
{c('Feature').t`Professional support`}
+

* {c('Info concerning plan features').t`Denotes customizable features`}

+ + ); +}; + +VpnFeaturesTable.propTypes = { + cycle: PropTypes.number, + currency: PropTypes.string +}; + +export default VpnFeaturesTable; diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index 524d9afb7..e6c8a9a23 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -1,5 +1,5 @@ import React from 'react'; -import { SubscriptionTable, useVPNCountries } from 'react-components'; +import { SubscriptionTable, useVPNCountries, LinkButton, useModals } from 'react-components'; import PropTypes from 'prop-types'; import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; import { toMap } from 'proton-shared/lib/helpers/object'; @@ -10,6 +10,7 @@ import professionalPlanSvg from 'design-system/assets/img/pv-images/plans/vpnplu import visionaryPlanSvg from 'design-system/assets/img/pv-images/plans/visionary-plan.svg'; import SubscriptionPrices from './SubscriptionPrices'; +import SubscriptionFeaturesModal from './SubscriptionFeaturesModal'; const INDEXES = { [PLANS.VPNBASIC]: 1, @@ -17,14 +18,6 @@ const INDEXES = { [PLANS.VISIONARY]: 3 }; -const FREE_PLAN = { - Pricing: { - [CYCLE.MONTHLY]: 0, - [CYCLE.YEARLY]: 0, - [CYCLE.TWO_YEARS]: 0 - } -}; - const VpnSubscriptionTable = ({ planNameSelected, plans: apiPlans = [], @@ -34,6 +27,7 @@ const VpnSubscriptionTable = ({ currentPlan, ...rest }) => { + const { createModal } = useModals(); const plansMap = toMap(apiPlans, 'Name'); const vpnBasicPlan = plansMap[PLANS.VPNBASIC]; const vpnPlusPlan = plansMap[PLANS.VPNPLUS]; @@ -43,7 +37,7 @@ const VpnSubscriptionTable = ({ { name: '', title: 'Free', - price: , + price: , imageSrc: freePlanSvg, description: c('Description').t`Privacy and security for everyone`, features: [ @@ -55,18 +49,6 @@ const VpnSubscriptionTable = ({ {c('Feature').t`Secure Core and Tor VPN`}, {c('Feature').t`Advanced privacy features`}, {c('Feature').t`Access blocked content`} - ], - allFeatures: [ - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t``, - c('Feature').t`` ] }, vpnBasicPlan && { @@ -85,8 +67,7 @@ const VpnSubscriptionTable = ({ {c('Feature').t`Secure Core and Tor VPN`}, {c('Feature').t`Advanced privacy features`}, {c('Feature').t`Access blocked content`} - ], - allFeatures: [c('Feature').t``] + ] }, vpnPlusPlan && { name: vpnPlusPlan.Name, @@ -104,8 +85,7 @@ const VpnSubscriptionTable = ({ c('Feature').t`Secure Core and Tor VPN`, c('Feature').t`Advanced privacy features`, c('Feature').t`Access blocked content` - ], - allFeatures: [c('Feature').t``] + ] }, visionaryPlan && { name: visionaryPlan.Name, @@ -118,8 +98,7 @@ const VpnSubscriptionTable = ({ c('Feature').t`All Plus plan features`, c('Feature').t`10 simultaneous VPN connections`, c('Feature').t`ProtonMail Visionary account` - ], - allFeatures: [c('Feature').t``] + ] } ]; @@ -133,6 +112,14 @@ const VpnSubscriptionTable = ({ currentPlan={currentPlan} {...rest} /> +
+ createModal()} + > + {c('Action').t`Show all features`} + +
); }; From 06fe1c27387f54c77894eef224735f2461be92a9 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 10 Dec 2019 12:16:35 +0100 Subject: [PATCH 022/242] Continue --- .../subscription/NewSubscriptionModalFooter.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/containers/payments/subscription/NewSubscriptionModalFooter.js b/containers/payments/subscription/NewSubscriptionModalFooter.js index 340feb4f7..4779aa09c 100644 --- a/containers/payments/subscription/NewSubscriptionModalFooter.js +++ b/containers/payments/subscription/NewSubscriptionModalFooter.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useAddresses, Icon, Button, PrimaryButton, Loader } from 'react-components'; +import { useAddresses, Button, PrimaryButton, Loader } from 'react-components'; import { c } from 'ttag'; import { CYCLE, COUPON_CODES } from 'proton-shared/lib/constants'; +import checkmarkSvg from 'design-system/assets/img/shared/checkmark-icon.svg'; +import percentageSvg from 'design-system/assets/img/shared/percentage-icon.svg'; const STEPS = { CUSTOMIZATION: 0, @@ -11,6 +13,9 @@ const STEPS = { THANKS: 3 }; +const CheckmarkIcon = () => checkmark; +const PercentageIcon = () => percentage; + const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { const [addresses, loadingAddresses] = useAddresses(); @@ -27,31 +32,31 @@ const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { const upsells = [ model.cycle === CYCLE.MONTHLY && (
- + {c('Info').t`Save 20% by switching to annual billing`}
), model.cycle === CYCLE.YEARLY && (
- + {c('Info').t`You are saving 20% with annual billing`}
), model.cycle === CYCLE.TWO_YEARS && (
- + {c('Info').t`You are saving 33% with 2-year billing`}
), hasAddresses && model.coupon !== COUPON_CODES.BUNDLE && (
- + {c('Info').t`Save an extra 20% by combining Mail and VPN`}
), hasAddresses && model.coupon === COUPON_CODES.BUNDLE && (
- + {c('Info').t`You are saving an extra 20% with the bundle discount`}
) From 4436494c42c6c54a0690b0140d67746f46a95ed6 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 10 Dec 2019 16:00:54 +0100 Subject: [PATCH 023/242] Continue --- .../subscription/NewSubscriptionModal.js | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 3381dfa90..444a0f352 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { FormModal, usePlans, useApi, useLoading, useSubscription, useVPNCountries } from 'react-components'; +import { FormModal, usePlans, useApi, useLoading, useVPNCountries, useEventManager } from 'react-components'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; @@ -48,9 +48,9 @@ const NewSubscriptionModal = ({ }; const api = useApi(); + const { call } = useEventManager(); const [vpnCountries, loadingVpnCountries] = useVPNCountries(); const [plans, loadingPlans] = usePlans(); - const [subscription, loadingSubscription] = useSubscription(); const [loading, withLoading] = useLoading(); const [loadingCheck, withLoadingCheck] = useLoading(); const [checkResult, setCheckResult] = useState({}); @@ -91,8 +91,26 @@ const NewSubscriptionModal = ({ }; const handleSubscribe = async () => { - setStep(STEPS.UPGRADE); - await withLoading(api(subscribe(subscription))); + try { + setStep(STEPS.UPGRADE); + await withLoading( + api( + subscribe({ + Amount: checkResult.AmountDue, + PlanIDs: model.planIDs, + CouponCode: model.coupon, + Currency: model.currency, + Cycle: model.cycle + // TODO add payments details + }) + ) + ); + await withLoading(call()); + setStep(STEPS.THANKS); + } catch (error) { + setStep(checkResult.AmountDue ? STEPS.PAYMENT : STEPS.CUSTOMIZATION); + throw error; + } }; const handleCheckout = () => { @@ -113,12 +131,12 @@ const NewSubscriptionModal = ({ submit={submit} step={step} model={model} - loading={loading || loadingPlans || loadingSubscription || loadingVpnCountries} + loading={loading || loadingPlans || loadingVpnCountries} /> } className="pm-modal--full subscription-modal" title={TITLE[step]} - loading={loading || loadingPlans || loadingSubscription || loadingVpnCountries} + loading={loading || loadingPlans || loadingVpnCountries} {...rest} > {step === STEPS.CUSTOMIZATION && ( From a2105ffd7ae9d8eec0e1d843c53d878ad82e93e3 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 12 Dec 2019 16:35:40 +0100 Subject: [PATCH 024/242] Continue --- .../paymentMethods/usePaymentMethods.js | 24 +++++- containers/payments/Bitcoin.js | 4 +- containers/payments/Payment.js | 78 +++++++++++++------ .../subscription/NewSubscriptionModal.js | 58 +++++++++++--- .../NewSubscriptionModalFooter.js | 15 ++-- .../subscription/SubscriptionCheckout.js | 6 +- .../subscription/SubscriptionPayment.js | 3 - .../subscription/SubscriptionTable.js | 3 +- containers/payments/usePayment.js | 4 + 9 files changed, 143 insertions(+), 52 deletions(-) delete mode 100644 containers/payments/subscription/SubscriptionPayment.js diff --git a/containers/paymentMethods/usePaymentMethods.js b/containers/paymentMethods/usePaymentMethods.js index 14638dbd4..7a9c99382 100644 --- a/containers/paymentMethods/usePaymentMethods.js +++ b/containers/paymentMethods/usePaymentMethods.js @@ -28,16 +28,29 @@ const usePaymentMethods = ({ amount, coupon, type }) => { } }; + const getIcon = (type) => { + switch (type) { + case PAYMENT_METHOD_TYPES.CARD: + return 'payments-type-card'; + case PAYMENT_METHOD_TYPES.PAYPAL: + return 'payments-type-pp'; + default: + return ''; + } + }; + const options = [ { + icon: 'payments-type-card', value: PAYMENT_METHOD_TYPES.CARD, - text: c('Payment method option').t`Pay with credit/debit card` + text: c('Payment method option').t`Credit/debit card` } ]; if (methods.length) { options.unshift( ...methods.map(({ ID: value, Details, Type }, index) => ({ + icon: getIcon(Type), text: [ getMethod(Type, Details), isExpired(Details) && `(${c('Info').t`Expired`})`, @@ -53,19 +66,22 @@ const usePaymentMethods = ({ amount, coupon, type }) => { if (!alreadyHavePayPal && (isPaypalAmountValid || isInvoice)) { options.push({ - text: c('Payment method option').t`Pay with PayPal`, + icon: 'payments-type-pp', + text: c('Payment method option').t`PayPal`, value: PAYMENT_METHOD_TYPES.PAYPAL }); } if (!isSignup && coupon !== BLACK_FRIDAY.COUPON_CODE) { options.push({ - text: c('Payment method option').t`Pay with Bitcoin`, + icon: 'payments-type-bt', + text: c('Payment method option').t`Bitcoin`, value: 'bitcoin' }); options.push({ - text: c('Label').t`Pay with cash`, + icon: 'payments', + text: c('Label').t`Cash`, value: 'cash' }); } diff --git a/containers/payments/Bitcoin.js b/containers/payments/Bitcoin.js index 966c062dc..b6785a5ad 100644 --- a/containers/payments/Bitcoin.js +++ b/containers/payments/Bitcoin.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { c } from 'ttag'; import { Alert, Price, Button, Loader, useConfig, useApi, useLoading } from 'react-components'; import { createBitcoinPayment } from 'proton-shared/lib/api/payments'; -import { MIN_BITCOIN_AMOUNT, BTC_DONATION_ADDRESS, CLIENT_TYPES } from 'proton-shared/lib/constants'; +import { MIN_BITCOIN_AMOUNT, BTC_DONATION_ADDRESS, CLIENT_TYPES, CURRENCIES } from 'proton-shared/lib/constants'; import BitcoinQRCode from './BitcoinQRCode'; import BitcoinDetails from './BitcoinDetails'; @@ -79,7 +79,7 @@ const Bitcoin = ({ amount, currency, type }) => { Bitcoin.propTypes = { amount: PropTypes.number.isRequired, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']), + currency: PropTypes.oneOf(CURRENCIES), type: PropTypes.string }; diff --git a/containers/payments/Payment.js b/containers/payments/Payment.js index 040821b6f..d15c08c98 100644 --- a/containers/payments/Payment.js +++ b/containers/payments/Payment.js @@ -1,12 +1,11 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { Label, Row, Field, Alert, Price } from 'react-components'; -import { CYCLE, PAYMENT_METHOD_TYPES, MIN_DONATION_AMOUNT, MIN_CREDIT_AMOUNT } from 'proton-shared/lib/constants'; +import { classnames, Radio, Icon, Row, Alert, Price, LinkButton, Loader } from 'react-components'; +import { PAYMENT_METHOD_TYPES, MIN_DONATION_AMOUNT, MIN_CREDIT_AMOUNT } from 'proton-shared/lib/constants'; import Method from './Method'; import toDetails from './toDetails'; -import PaymentMethodsSelect from '../paymentMethods/PaymentMethodsSelect'; import usePaymentMethods from '../paymentMethods/usePaymentMethods'; const { CARD, PAYPAL, CASH, BITCOIN } = PAYMENT_METHOD_TYPES; @@ -17,16 +16,25 @@ const Payment = ({ amount, currency, coupon, - cycle, onParameters, method, onMethod, onValidCard, onPay, - fieldClassName, card }) => { const { methods, options, loading } = usePaymentMethods({ amount, coupon, type }); + const lastCustomMethod = [...options] + .reverse() + .find( + ({ value }) => + ![ + PAYMENT_METHOD_TYPES.CARD, + PAYMENT_METHOD_TYPES.PAYPAL, + PAYMENT_METHOD_TYPES.CASH, + PAYMENT_METHOD_TYPES.BITCOIN + ].includes(value) + ); const handleCard = ({ card, isValid }) => { onValidCard(isValid); @@ -41,6 +49,10 @@ const Payment = ({ } }; + useEffect(() => { + handleChangeMethod(options[0].value); + }, [methods.length]); + if (type === 'donation' && amount < MIN_DONATION_AMOUNT) { const price = ( @@ -68,22 +80,46 @@ const Payment = ({ return {c('Error').jt`The minimum payment we accept is ${price}`}; } + if (loading) { + return ; + } + return ( <> - - -
- +
+ + {options.map(({ text, value, disabled, icon }, index) => { + return ( + + ); + })} +
+ + + {c('Link').t`Use gift code`} +
+
+
{children} - +
); @@ -114,9 +150,7 @@ Payment.propTypes = { method: PropTypes.string, onMethod: PropTypes.func, onValidCard: PropTypes.func, - cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]), - onPay: PropTypes.func, - fieldClassName: PropTypes.string + onPay: PropTypes.func }; export default Payment; diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 444a0f352..a0c023b56 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -1,12 +1,22 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { FormModal, usePlans, useApi, useLoading, useVPNCountries, useEventManager } from 'react-components'; +import { + Alert, + FormModal, + Payment, + usePlans, + useApi, + useLoading, + useVPNCountries, + useEventManager, + usePayment, + useCard +} from 'react-components'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; import SubscriptionCustomization from './SubscriptionCustomization'; -import SubscriptionPayment from './SubscriptionPayment'; import SubscriptionUpgrade from './SubscriptionUpgrade'; import SubscriptionThanks from './SubscriptionThanks'; import SubscriptionCheckout from './SubscriptionCheckout'; @@ -54,6 +64,9 @@ const NewSubscriptionModal = ({ const [loading, withLoading] = useLoading(); const [loadingCheck, withLoadingCheck] = useLoading(); const [checkResult, setCheckResult] = useState({}); + const card = useCard(); + const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); + const { Code: couponCode } = checkResult.Coupon || {}; // Coupon can be null const [model, setModel] = useState({ cycle, currency, @@ -100,8 +113,8 @@ const NewSubscriptionModal = ({ PlanIDs: model.planIDs, CouponCode: model.coupon, Currency: model.currency, - Cycle: model.cycle - // TODO add payments details + Cycle: model.cycle, + ...parameters }) ) ); @@ -165,12 +178,37 @@ const NewSubscriptionModal = ({
)} {step === STEPS.PAYMENT && ( - +
+
+

{c('Title').t`Payment method`}

+ {c('Info').t`You can use any of your saved payment methods or add a new one.`} + +
+
+ +
+
)} {step === STEPS.UPGRADE && } {step === STEPS.THANKS && } diff --git a/containers/payments/subscription/NewSubscriptionModalFooter.js b/containers/payments/subscription/NewSubscriptionModalFooter.js index 4779aa09c..54e5c73ef 100644 --- a/containers/payments/subscription/NewSubscriptionModalFooter.js +++ b/containers/payments/subscription/NewSubscriptionModalFooter.js @@ -16,7 +16,7 @@ const STEPS = { const CheckmarkIcon = () => checkmark; const PercentageIcon = () => percentage; -const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { +const NewSubscriptionModalFooter = ({ submit, step, model, loading, disabled = false }) => { const [addresses, loadingAddresses] = useAddresses(); if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { @@ -31,31 +31,31 @@ const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { const cancel = step === STEPS.CUSTOMIZATION ? c('Action').t`Cancel` : c('Action').t`Back`; const upsells = [ model.cycle === CYCLE.MONTHLY && ( -
+
{c('Info').t`Save 20% by switching to annual billing`}
), model.cycle === CYCLE.YEARLY && ( -
+
{c('Info').t`You are saving 20% with annual billing`}
), model.cycle === CYCLE.TWO_YEARS && ( -
+
{c('Info').t`You are saving 33% with 2-year billing`}
), hasAddresses && model.coupon !== COUPON_CODES.BUNDLE && ( -
+
{c('Info').t`Save an extra 20% by combining Mail and VPN`}
), hasAddresses && model.coupon === COUPON_CODES.BUNDLE && ( -
+
{c('Info').t`You are saving an extra 20% with the bundle discount`}
@@ -68,7 +68,7 @@ const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { {cancel} {upsells} - + {submit} @@ -76,6 +76,7 @@ const NewSubscriptionModalFooter = ({ submit, step, model, loading }) => { }; NewSubscriptionModalFooter.propTypes = { + disabled: PropTypes.bool, submit: PropTypes.string, loading: PropTypes.bool, step: PropTypes.number, diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index a2394aae6..ce88fe773 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -50,7 +50,8 @@ CheckoutRow.propTypes = { const SubscriptionCheckout = ({ submit = c('Action').t`Pay`, - plans, + disabled = false, + plans = [], model, setModel, checkResult, @@ -269,7 +270,7 @@ const SubscriptionCheckout = ({ className="bold" />
- + {submit}
@@ -292,6 +293,7 @@ const SubscriptionCheckout = ({ }; SubscriptionCheckout.propTypes = { + disabled: PropTypes.bool, submit: PropTypes.string, plans: PropTypes.array.isRequired, checkResult: PropTypes.object.isRequired, diff --git a/containers/payments/subscription/SubscriptionPayment.js b/containers/payments/subscription/SubscriptionPayment.js deleted file mode 100644 index 1e2e5c0b1..000000000 --- a/containers/payments/subscription/SubscriptionPayment.js +++ /dev/null @@ -1,3 +0,0 @@ -const SubscriptionPayment = () => {}; - -export default SubscriptionPayment; \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionTable.js b/containers/payments/subscription/SubscriptionTable.js index 51e1402f6..d69d5742c 100644 --- a/containers/payments/subscription/SubscriptionTable.js +++ b/containers/payments/subscription/SubscriptionTable.js @@ -89,8 +89,7 @@ SubscriptionTable.propTypes = { price: PropTypes.node.isRequired, imageSrc: PropTypes.string.isRequired, description: PropTypes.node.isRequired, - features: PropTypes.arrayOf(PropTypes.node).isRequired, - allFeatures: PropTypes.arrayOf(PropTypes.node).isRequired + features: PropTypes.arrayOf(PropTypes.node).isRequired }) ), onSelect: PropTypes.func.isRequired, diff --git a/containers/payments/usePayment.js b/containers/payments/usePayment.js index 862dc59d3..315f8f2e5 100644 --- a/containers/payments/usePayment.js +++ b/containers/payments/usePayment.js @@ -16,6 +16,10 @@ const usePayment = () => { }; const canPay = () => { + if (!method) { + return false; + } + if ([BITCOIN, CASH].includes(method)) { return false; } From b79770fb6d0d9d7d3a9d338377c320d9de2ac3c9 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 12 Dec 2019 17:51:45 +0100 Subject: [PATCH 025/242] Continue --- .../paymentMethods/usePaymentMethods.js | 18 ++++----- containers/payments/Bitcoin.js | 19 ++++++---- containers/payments/Method.js | 37 +++++++++++++++++-- containers/payments/Payment.js | 3 +- .../subscription/NewSubscriptionModal.js | 14 ++++++- 5 files changed, 67 insertions(+), 24 deletions(-) diff --git a/containers/paymentMethods/usePaymentMethods.js b/containers/paymentMethods/usePaymentMethods.js index 7a9c99382..0a8f2bfbc 100644 --- a/containers/paymentMethods/usePaymentMethods.js +++ b/containers/paymentMethods/usePaymentMethods.js @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { c } from 'ttag'; import { useApi, useLoading, useAuthentication } from 'react-components'; -import { BLACK_FRIDAY, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; +import { BLACK_FRIDAY, PAYMENT_METHOD_TYPES, MIN_BITCOIN_AMOUNT } from 'proton-shared/lib/constants'; import { isExpired } from 'proton-shared/lib/helpers/card'; import { queryPaymentMethods } from 'proton-shared/lib/api/payments'; @@ -20,9 +20,9 @@ const usePaymentMethods = ({ amount, coupon, type }) => { const getMethod = (type, { Brand = '', Last4 = '', Payer = '' }) => { switch (type) { case PAYMENT_METHOD_TYPES.CARD: - return `[${Brand}] •••• ${Last4}`; + return `${Brand} - ${Last4}`; case PAYMENT_METHOD_TYPES.PAYPAL: - return `[PayPal] ${Payer}`; + return `PayPal - ${Payer}`; default: return ''; } @@ -49,13 +49,9 @@ const usePaymentMethods = ({ amount, coupon, type }) => { if (methods.length) { options.unshift( - ...methods.map(({ ID: value, Details, Type }, index) => ({ + ...methods.map(({ ID: value, Details, Type }) => ({ icon: getIcon(Type), - text: [ - getMethod(Type, Details), - isExpired(Details) && `(${c('Info').t`Expired`})`, - index === 0 && `(${c('Info').t`default`})` - ] + text: [getMethod(Type, Details), isExpired(Details) && `(${c('Info').t`Expired`})`] .filter(Boolean) .join(' '), value, @@ -72,13 +68,15 @@ const usePaymentMethods = ({ amount, coupon, type }) => { }); } - if (!isSignup && coupon !== BLACK_FRIDAY.COUPON_CODE) { + if (!isSignup && coupon !== BLACK_FRIDAY.COUPON_CODE && amount > MIN_BITCOIN_AMOUNT) { options.push({ icon: 'payments-type-bt', text: c('Payment method option').t`Bitcoin`, value: 'bitcoin' }); + } + if (!isSignup && coupon !== BLACK_FRIDAY.COUPON_CODE) { options.push({ icon: 'payments', text: c('Label').t`Cash`, diff --git a/containers/payments/Bitcoin.js b/containers/payments/Bitcoin.js index b6785a5ad..6c819fb91 100644 --- a/containers/payments/Bitcoin.js +++ b/containers/payments/Bitcoin.js @@ -15,18 +15,16 @@ const Bitcoin = ({ amount, currency, type }) => { const { CLIENT_TYPE } = useConfig(); const [loading, withLoading] = useLoading(); const [error, setError] = useState(false); - const [amountBitcoin, setAmountBitcoin] = useState(); - const [address, setAddress] = useState(); + const [model, setModel] = useState({}); const request = async () => { setError(false); try { const { AmountBitcoin, Address } = await api(createBitcoinPayment(amount, currency)); - - setAmountBitcoin(AmountBitcoin); - setAddress(type === 'donation' ? BTC_DONATION_ADDRESS : Address); + setModel({ amountBitcoin: AmountBitcoin, address: type === 'donation' ? BTC_DONATION_ADDRESS : Address }); } catch (error) { setError(true); + throw error; } }; @@ -45,7 +43,7 @@ const Bitcoin = ({ amount, currency, type }) => { return ; } - if (error || !amountBitcoin || !address) { + if (error || !model.amountBitcoin || !model.address) { return ( <> {c('Error').t`Error connecting to the Bitcoin API.`} @@ -57,8 +55,13 @@ const Bitcoin = ({ amount, currency, type }) => { return ( <>
- - + +
{type === 'invoice' ? ( {c('Info') diff --git a/containers/payments/Method.js b/containers/payments/Method.js index b3e087def..fa41e541e 100644 --- a/containers/payments/Method.js +++ b/containers/payments/Method.js @@ -1,7 +1,13 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; -import { Loader } from 'react-components'; +import { Alert, Loader } from 'react-components'; +import { c } from 'ttag'; +import treeDSecureSvg from 'design-system/assets/img/shared/bank-icons/3-d-secure.svg'; +import americanExpressSafekeySvg from 'design-system/assets/img/shared/bank-icons/american-express-safekey.svg'; +import discoverProtectBuySvg from 'design-system/assets/img/shared/bank-icons/discover-protectbuy.svg'; +import mastercardSecurecodeSvg from 'design-system/assets/img/shared/bank-icons/mastercard-securecode.svg'; +import verifiedByVisaSvg from 'design-system/assets/img/shared/bank-icons/verified-by-visa.svg'; import Card from './Card'; import PaymentMethodDetails from '../paymentMethods/PaymentMethodDetails'; @@ -11,6 +17,21 @@ import Bitcoin from './Bitcoin'; const { CARD, PAYPAL, BITCOIN, CASH } = PAYMENT_METHOD_TYPES; +const Alert3DS = () => { + return ( + +
{c('Info').t`We use 3-D Secure to protect your payments.`}
+
+ + + + + +
+
+ ); +}; + const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, loading, card: cardToUse }) => { const { card, updateCard, errors, isValid } = cardToUse; @@ -23,7 +44,12 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, } if (method === CARD) { - return ; + return ( + <> + + + + ); } if (method === CASH) { @@ -41,7 +67,12 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, const { Details, Type } = methods.find(({ ID }) => method === ID) || {}; if (Details) { - return ; + return ( + <> + + + + ); } return null; diff --git a/containers/payments/Payment.js b/containers/payments/Payment.js index d15c08c98..34e896a5f 100644 --- a/containers/payments/Payment.js +++ b/containers/payments/Payment.js @@ -50,7 +50,8 @@ const Payment = ({ }; useEffect(() => { - handleChangeMethod(options[0].value); + const { value } = options.find(({ disabled }) => !disabled); + handleChangeMethod(value); }, [methods.length]); if (type === 'donation' && amount < MIN_DONATION_AMOUNT) { diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index a0c023b56..9451dc433 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -81,6 +81,11 @@ const NewSubscriptionModal = ({ : c('Action').t`Finish` : c('Action').t`Pay`; + /** + * Check current configuration to see if it's valid + * @param {Object} newModel + * @returns {Promise} + */ const check = async (newModel = model) => { try { const result = await api( @@ -103,6 +108,10 @@ const NewSubscriptionModal = ({ } }; + /** + * Subscribe to a new subscription + * @return {Promise} + */ const handleSubscribe = async () => { try { setStep(STEPS.UPGRADE); @@ -144,7 +153,8 @@ const NewSubscriptionModal = ({ submit={submit} step={step} model={model} - loading={loading || loadingPlans || loadingVpnCountries} + disabled={step === STEPS.PAYMENT ? !canPay : false} + loading={loadingCheck} /> } className="pm-modal--full subscription-modal" @@ -205,7 +215,7 @@ const NewSubscriptionModal = ({ onCheckout={handleCheckout} model={model} setModel={setModel} - disabled={canPay} + disabled={!canPay} />
From 8d5c59bd5a31c79e121eae0c1368118f5fbcd623 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 12 Dec 2019 17:59:30 +0100 Subject: [PATCH 026/242] Continue --- .../payments/subscription/NewSubscriptionModal.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 9451dc433..dcc00bda0 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -48,6 +48,7 @@ const NewSubscriptionModal = ({ currency = DEFAULT_CURRENCY, coupon, planIDs = {}, + onClose, ...rest }) => { const TITLE = { @@ -142,6 +143,15 @@ const NewSubscriptionModal = ({ setStep(STEPS.PAYMENT); }; + const handleClose = (e) => { + if (step === STEPS.PAYMENT) { + setStep(STEPS.CUSTOMIZATION); + return; + } + + onClose(e); + }; + useEffect(() => { withLoadingCheck(check()); }, [model.cycle, model.planIDs]); @@ -160,6 +170,7 @@ const NewSubscriptionModal = ({ className="pm-modal--full subscription-modal" title={TITLE[step]} loading={loading || loadingPlans || loadingVpnCountries} + onClose={handleClose} {...rest} > {step === STEPS.CUSTOMIZATION && ( From fd3ca465ccfc6696ffcc6d738946232c009727f9 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 13 Dec 2019 08:31:27 +0100 Subject: [PATCH 027/242] Continue --- .../subscription/SubscriptionCheckout.js | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index ce88fe773..3e2bc19ab 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -98,24 +98,21 @@ const SubscriptionCheckout = ({ const bonusStorage = humanSize(LOYAL_BONUS_STORAGE, 'GB'); const getTitle = (planName, quantity) => { + const addresses = quantity * addressAddon.MaxAddresses; + const storage = humanSize(quantity * storageAddon.MaxSpace, 'GB'); + const domains = quantity * domainAddon.MaxDomains; + const members = quantity * memberAddon.MaxMembers; + const vpn = quantity * vpnAddon.MaxVPN; return { - [ADDON_NAMES.ADDRESS]: c('Addon').t`+ ${quantity * addressAddon.MaxAddresses} email addresses`, - [ADDON_NAMES.SPACE]: c('Addon').t`+ ${humanSize(quantity * storageAddon.MaxSpace, 'GB')} storage`, + [ADDON_NAMES.ADDRESS]: c('Addon').t`+ ${addresses} email addresses`, + [ADDON_NAMES.SPACE]: c('Addon').t`+ ${storage} storage`, [ADDON_NAMES.DOMAIN]: c('Addon').ngettext( - msgid`+ ${quantity * domainAddon.MaxDomains} custom domain`, - `+ ${quantity * domainAddon.MaxDomains} custom domains`, - quantity * domainAddon.MaxDomains + msgid`+ ${domains} custom domain`, + `+ ${domains} custom domains`, + domains ), - [ADDON_NAMES.MEMBER]: c('Addon').ngettext( - msgid`+ ${quantity * memberAddon.MaxMembers} user`, - `+ ${quantity * memberAddon.MaxMembers} users`, - quantity * memberAddon.MaxMembers - ), - [ADDON_NAMES.VPN]: c('Addon').ngettext( - msgid`+ ${quantity * vpnAddon.MaxMembers} connection`, - `+ ${quantity * vpnAddon.MaxMembers} connections`, - quantity * vpnAddon.MaxMembers - ) + [ADDON_NAMES.MEMBER]: c('Addon').ngettext(msgid`+ ${members} user`, `+ ${members} users`, members), + [ADDON_NAMES.VPN]: c('Addon').ngettext(msgid`+ ${vpn} connection`, `+ ${vpn} connections`, vpn) }[planName]; }; From cf70db76cd249446634445c52fe077c80f47026b Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 13 Dec 2019 08:50:38 +0100 Subject: [PATCH 028/242] Continue --- .../subscription/SubscriptionCustomization.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js index 741520370..5487db1e0 100644 --- a/containers/payments/subscription/SubscriptionCustomization.js +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -173,6 +173,15 @@ const SubscriptionCustomization = ({ .t`Each additional user comes automatically with 5 GB storage space and 5 email addresses.` }; + const plusAddresses = (model.planIDs[addressAddon.ID] || 0) * addressAddon.MaxAddresses + plusPlan.MaxAddresses; + const plusDomains = (model.planIDs[domainAddon.ID] || 0) * domainAddon.MaxDomains + plusPlan.MaxDomains; + const professionalMembers = + (model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxMembers + professionalPlan.MaxMembers; + const professionalAddresses = + (model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxAddresses + professionalPlan.MaxAddresses; + const professionalDomains = + (model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxDomains + professionalPlan.MaxDomains; + const FEATURES = { [FREE]: [ , @@ -193,18 +202,15 @@ const SubscriptionCustomization = ({ , , @@ -214,11 +220,9 @@ const SubscriptionCustomization = ({ key="member" icon="organization-users" feature={c('Feature').ngettext( - msgid`${(model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxMembers + - professionalPlan.MaxMembers} user`, - `${(model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxMembers + - professionalPlan.MaxMembers} users`, - (model.planIDs[memberAddon.ID] || 0) * memberAddon.MaxMembers + professionalPlan.MaxMembers + msgid`${professionalMembers} user`, + `${professionalMembers} users`, + professionalMembers )} />, , , From 8cfb860d7ea25f10796dd644a620d55b29b62d61 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 17 Dec 2019 17:56:04 +0100 Subject: [PATCH 029/242] Continue --- containers/invoices/PayInvoiceModal.js | 21 ++- .../paymentMethods/PaymentMethodDetails.js | 63 +++++-- .../paymentMethods/usePaymentMethods.js | 2 +- containers/payments/Card.js | 20 ++- containers/payments/CardNumberInput.js | 3 +- containers/payments/Cash.js | 4 +- containers/payments/CreditsModal.js | 22 +-- containers/payments/DonateModal.js | 21 ++- containers/payments/EditCardModal.js | 2 +- containers/payments/Method.js | 56 +++--- containers/payments/PayPalButton.js | 62 +++++++ containers/payments/PayPalView.js | 87 +++++++++ containers/payments/Payment.js | 63 +++---- containers/payments/PaymentGiftCode.js | 50 ++++++ .../payments/subscription/GiftCodeForm.js | 38 ++-- .../subscription/MailSubscriptionTable.js | 4 +- .../subscription/NewSubscriptionModal.js | 169 +++++++++++------- .../subscription/NewSubscriptionModal.scss | 4 +- .../NewSubscriptionModalFooter.js | 16 +- .../subscription/SubscriptionCheckout.js | 53 +----- .../subscription/SubscriptionModal.js | 8 +- .../subscription/SubscriptionPrices.js | 4 +- .../subscription/SubscriptionThanks.js | 22 ++- .../subscription/VpnSubscriptionTable.js | 4 +- containers/payments/toDetails.js | 4 +- containers/payments/useCard.js | 7 +- containers/payments/usePayment.js | 48 ++++- hooks/usePayPal.js | 49 +++++ index.ts | 1 + 29 files changed, 618 insertions(+), 289 deletions(-) create mode 100644 containers/payments/PayPalButton.js create mode 100644 containers/payments/PayPalView.js create mode 100644 containers/payments/PaymentGiftCode.js create mode 100644 hooks/usePayPal.js diff --git a/containers/invoices/PayInvoiceModal.js b/containers/invoices/PayInvoiceModal.js index a00a47ced..f31fd96b0 100644 --- a/containers/invoices/PayInvoiceModal.js +++ b/containers/invoices/PayInvoiceModal.js @@ -18,19 +18,16 @@ import { toPrice } from 'proton-shared/lib/helpers/string'; import Payment from '../payments/Payment'; import usePayment from '../payments/usePayment'; -import useCard from '../payments/useCard'; import { handlePaymentToken } from '../payments/paymentTokenHelper'; const PayInvoiceModal = ({ invoice, fetchInvoices, ...rest }) => { const { createModal } = useModals(); const [loading, withLoading] = useLoading(); const api = useApi(); - const card = useCard(); const { result = {}, loading: loadingCheck } = useApiResult(() => checkInvoice(invoice.ID), []); const { AmountDue, Amount, Currency, Credit } = result; - const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); - const handleSubmit = async (params = parameters) => { + const handleSubmit = async (params) => { const requestBody = await handlePaymentToken({ params: { ...params, Amount: AmountDue, Currency }, api, @@ -41,10 +38,16 @@ const PayInvoiceModal = ({ invoice, fetchInvoices, ...rest }) => { rest.onClose(); }; + const { card, setCard, errors, method, setMethod, parameters, canPay, paypal, paypalCredit } = usePayment({ + amount: AmountDue, + currency: Currency, + onPay: handleSubmit + }); + return ( withLoading(handleSubmit())} + onSubmit={() => withLoading(handleSubmit(parameters))} loading={loading} close={c('Action').t`Close`} submit={canPay && c('Action').t`Pay`} @@ -87,12 +90,12 @@ const PayInvoiceModal = ({ invoice, fetchInvoices, ...rest }) => { method={method} amount={AmountDue} currency={Currency} - parameters={parameters} card={card} - onParameters={setParameters} onMethod={setMethod} - onValidCard={setCardValidity} - onPay={handleSubmit} + onCard={setCard} + errors={errors} + paypal={paypal} + paypalCredit={paypalCredit} /> ) : null} diff --git a/containers/paymentMethods/PaymentMethodDetails.js b/containers/paymentMethods/PaymentMethodDetails.js index 0d5b0e056..1e8133606 100644 --- a/containers/paymentMethods/PaymentMethodDetails.js +++ b/containers/paymentMethods/PaymentMethodDetails.js @@ -4,24 +4,52 @@ import { Bordered } from 'react-components'; import { c } from 'ttag'; import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; +const banks = require.context('design-system/assets/img/shared/bank-icons', true, /.svg$/); + +const banksMap = banks.keys().reduce((acc, key) => { + acc[key] = () => banks(key); + return acc; +}, {}); + +const getBankSvg = (type = '') => { + const key = `./cc-${type}.svg`; + if (!banksMap[key]) { + return; + } + return banksMap[key](); +}; + +const BANKS = { + 'American Express': 'american-express', + 'Diners Club': 'diners-club', + Discover: 'discover', + JCB: 'jcb', + Maestro: 'maestro', + MasterCard: 'mastercard', + UnionPay: 'unionpay', + Visa: 'visa' +}; + const PaymentMethodDetails = ({ type, details = {} }) => { - const { Last4, Name, ExpMonth, ExpYear, Payer } = details; + const { Last4, Name, ExpMonth, ExpYear, Payer, Brand = '' } = details; + if (type === PAYMENT_METHOD_TYPES.CARD) { + const bankIcon = getBankSvg(BANKS[Brand]); return ( - -

- •••• •••• •••• {Last4} -

-
-
-
{c('Label').t`Cardholder name`}:
- {Name} + + {bankIcon ? {Brand} : null} + + •••• •••• •••• {Last4} +
+
+ + {Name}
-
-
{c('Label for credit card').t`Expiration`}:
- +
+ + {ExpMonth}/{ExpYear} - +
@@ -29,11 +57,12 @@ const PaymentMethodDetails = ({ type, details = {} }) => { } if (type === PAYMENT_METHOD_TYPES.PAYPAL) { + const bankIcon = getBankSvg('paypal'); return ( - -

- PayPal {Payer} -

+ + PayPal + + PayPal {Payer} ); } diff --git a/containers/paymentMethods/usePaymentMethods.js b/containers/paymentMethods/usePaymentMethods.js index 0a8f2bfbc..baaeed944 100644 --- a/containers/paymentMethods/usePaymentMethods.js +++ b/containers/paymentMethods/usePaymentMethods.js @@ -78,7 +78,7 @@ const usePaymentMethods = ({ amount, coupon, type }) => { if (!isSignup && coupon !== BLACK_FRIDAY.COUPON_CODE) { options.push({ - icon: 'payments', + icon: 'payments-type-cash', text: c('Label').t`Cash`, value: 'cash' }); diff --git a/containers/payments/Card.js b/containers/payments/Card.js index 9e493cc18..a6acbd354 100644 --- a/containers/payments/Card.js +++ b/containers/payments/Card.js @@ -1,7 +1,7 @@ import React from 'react'; import { c } from 'ttag'; import PropTypes from 'prop-types'; -import { Block, Input, Select } from 'react-components'; +import { Label, Block, Input, Select } from 'react-components'; import { getFullList } from '../../helpers/countries'; import ExpInput from './ExpInput'; @@ -14,19 +14,23 @@ const Card = ({ card, errors, onChange, loading = false }) => { return ( <> + + onChange('number', value)} error={errors.number} @@ -36,6 +40,7 @@ const Card = ({ card, errors, onChange, loading = false }) => {
+ { />
+ {
+ { { const { CLIENT_TYPE } = useConfig(); - const email = CLIENT_TYPE === VPN ? 'contact@protonvpn.com' : 'contact@protonmail.com'; + const email = {CLIENT_TYPE === VPN ? 'contact@protonvpn.com' : 'contact@protonmail.com'}; return ( {c('Info for cash payment method') - .t`To pay via Cash, please email us at ${email} for instructions.`} + .jt`To pay via Cash, please email us at ${email} for instructions.`} ); }; diff --git a/containers/payments/CreditsModal.js b/containers/payments/CreditsModal.js index 38ef951c8..3d0fb9647 100644 --- a/containers/payments/CreditsModal.js +++ b/containers/payments/CreditsModal.js @@ -20,7 +20,6 @@ import { DEFAULT_CURRENCY, DEFAULT_CREDITS_AMOUNT, CLIENT_TYPES, MIN_CREDIT_AMOU import PaymentSelector from './PaymentSelector'; import Payment from './Payment'; import usePayment from './usePayment'; -import useCard from './useCard'; import { handlePaymentToken } from './paymentTokenHelper'; const getCurrenciesI18N = () => ({ @@ -36,17 +35,14 @@ const CreditsModal = (props) => { const { CLIENT_TYPE } = useConfig(); const { call } = useEventManager(); const { createModal } = useModals(); - const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); const { createNotification } = useNotifications(); const [loading, withLoading] = useLoading(); const [currency, setCurrency] = useState(DEFAULT_CURRENCY); const [amount, setAmount] = useState(DEFAULT_CREDITS_AMOUNT); - const card = useCard(); - const i18n = getCurrenciesI18N(); const i18nCurrency = i18n[currency]; - const handleSubmit = async (params = parameters) => { + const handleSubmit = async (params) => { const requestBody = await handlePaymentToken({ params: { ...params, Amount: amount, Currency: currency }, api, @@ -58,10 +54,16 @@ const CreditsModal = (props) => { createNotification({ text: c('Success').t`Credits added` }); }; + const { card, setCard, errors, method, setMethod, parameters, canPay, paypal, paypalCredit } = usePayment({ + amount, + currency, + onPay: handleSubmit + }); + return ( withLoading(handleSubmit())} + onSubmit={() => withLoading(handleSubmit(parameters))} loading={loading} submit={canPay && amount >= MIN_CREDIT_AMOUNT && c('Action').t`Top up`} close={c('Action').t`Close`} @@ -93,12 +95,12 @@ const CreditsModal = (props) => { method={method} amount={amount} currency={currency} - parameters={parameters} card={card} - onParameters={setParameters} onMethod={setMethod} - onValidCard={setCardValidity} - onPay={handleSubmit} + onCard={setCard} + errors={errors} + paypal={paypal} + paypalCredit={paypalCredit} /> ); diff --git a/containers/payments/DonateModal.js b/containers/payments/DonateModal.js index 7ebda258a..601bb9904 100644 --- a/containers/payments/DonateModal.js +++ b/containers/payments/DonateModal.js @@ -8,7 +8,6 @@ import { DEFAULT_CURRENCY, DEFAULT_DONATION_AMOUNT } from 'proton-shared/lib/con import PaymentSelector from './PaymentSelector'; import Payment from './Payment'; import usePayment from './usePayment'; -import useCard from './useCard'; import { handlePaymentToken } from './paymentTokenHelper'; const DonateModal = ({ ...rest }) => { @@ -17,11 +16,9 @@ const DonateModal = ({ ...rest }) => { const { createNotification } = useNotifications(); const [currency, setCurrency] = useState(DEFAULT_CURRENCY); const [amount, setAmount] = useState(DEFAULT_DONATION_AMOUNT); - const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); const { createModal } = useModals(); - const card = useCard(); - const handleSubmit = async (params = parameters) => { + const handleSubmit = async (params) => { const requestBody = await handlePaymentToken({ params: { ...params, Amount: amount, Currency: currency }, api, @@ -35,9 +32,15 @@ const DonateModal = ({ ...rest }) => { }); }; + const { card, setCard, errors, method, setMethod, parameters, canPay, paypal, paypalCredit } = usePayment({ + amount, + currency, + onPay: handleSubmit + }); + return ( withLoading(handleSubmit())} + onSubmit={() => withLoading(handleSubmit(parameters))} loading={loading} title={c('Title').t`Make a donation`} submit={canPay && c('Action').t`Donate`} @@ -60,12 +63,12 @@ const DonateModal = ({ ...rest }) => { method={method} amount={amount} currency={currency} - parameters={parameters} card={card} - onParameters={setParameters} onMethod={setMethod} - onValidCard={setCardValidity} - onPay={handleSubmit} + onCard={setCard} + errors={errors} + paypal={paypal} + paypalCredit={paypalCredit} /> ); diff --git a/containers/payments/EditCardModal.js b/containers/payments/EditCardModal.js index c43358216..91867ccff 100644 --- a/containers/payments/EditCardModal.js +++ b/containers/payments/EditCardModal.js @@ -16,7 +16,7 @@ const EditCardModal = ({ card: existingCard, onClose, onChange, ...rest }) => { const { createNotification } = useNotifications(); const { createModal } = useModals(); const title = existingCard ? c('Title').t`Edit credit/debit card` : c('Title').t`Add credit/debit card`; - const { card, updateCard, errors, isValid } = useCard(existingCard); + const [card, updateCard, errors, isValid] = useCard(existingCard); const handleSubmit = async (event) => { if (!isValid) { diff --git a/containers/payments/Method.js b/containers/payments/Method.js index fa41e541e..7f86bceee 100644 --- a/containers/payments/Method.js +++ b/containers/payments/Method.js @@ -1,9 +1,8 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; +import { PAYMENT_METHOD_TYPES, CURRENCIES } from 'proton-shared/lib/constants'; import { Alert, Loader } from 'react-components'; import { c } from 'ttag'; -import treeDSecureSvg from 'design-system/assets/img/shared/bank-icons/3-d-secure.svg'; import americanExpressSafekeySvg from 'design-system/assets/img/shared/bank-icons/american-express-safekey.svg'; import discoverProtectBuySvg from 'design-system/assets/img/shared/bank-icons/discover-protectbuy.svg'; import mastercardSecurecodeSvg from 'design-system/assets/img/shared/bank-icons/mastercard-securecode.svg'; @@ -11,7 +10,7 @@ import verifiedByVisaSvg from 'design-system/assets/img/shared/bank-icons/verifi import Card from './Card'; import PaymentMethodDetails from '../paymentMethods/PaymentMethodDetails'; -import PayPal from './PayPal'; +import PayPalView from './PayPalView'; import Cash from './Cash'; import Bitcoin from './Bitcoin'; @@ -22,23 +21,28 @@ const Alert3DS = () => {
{c('Info').t`We use 3-D Secure to protect your payments.`}
- - - - + + +
); }; -const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, loading, card: cardToUse }) => { - const { card, updateCard, errors, isValid } = cardToUse; - - useEffect(() => { - onCard({ card, isValid }); - }, [card]); - +const Method = ({ + type, + amount = 0, + currency, + onCard, + method, + methods, + loading, + card = {}, + errors = {}, + paypal = {}, + paypalCredit = {} +}) => { if (loading) { return ; } @@ -46,7 +50,7 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, if (method === CARD) { return ( <> - + ); @@ -61,7 +65,9 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, } if (method === PAYPAL) { - return ; + return ( + + ); } const { Details, Type } = methods.find(({ ID }) => method === ID) || {}; @@ -70,7 +76,7 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, return ( <> - + {Type === CARD ? : null} ); } @@ -81,13 +87,15 @@ const Method = ({ type, amount = 0, currency, onCard, onPayPal, method, methods, Method.propTypes = { loading: PropTypes.bool, method: PropTypes.string.isRequired, - methods: PropTypes.array, - type: PropTypes.oneOf(['signup', 'subscription', 'invoice', 'donation', 'credit']), - amount: PropTypes.number, + methods: PropTypes.array.isRequired, + type: PropTypes.oneOf(['signup', 'subscription', 'invoice', 'donation', 'credit']).isRequired, + amount: PropTypes.number.isRequired, card: PropTypes.object.isRequired, - onCard: PropTypes.func, - onPayPal: PropTypes.func, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']) + onCard: PropTypes.func.isRequired, + errors: PropTypes.object.isRequired, + currency: PropTypes.oneOf(CURRENCIES).isRequired, + paypal: PropTypes.object.isRequired, + paypalCredit: PropTypes.object.isRequired }; export default Method; diff --git a/containers/payments/PayPalButton.js b/containers/payments/PayPalButton.js new file mode 100644 index 000000000..7886198f4 --- /dev/null +++ b/containers/payments/PayPalButton.js @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button, useNotifications } from 'react-components'; +import { doNotWindowOpen } from 'proton-shared/lib/helpers/browser'; +import { MIN_PAYPAL_AMOUNT, MAX_PAYPAL_AMOUNT } from 'proton-shared/lib/constants'; +import { c } from 'ttag'; + +const PayPalButton = ({ amount, children, className, paypal }) => { + const [retry, setRetry] = useState(false); + const { createNotification } = useNotifications(); + + if (amount < MIN_PAYPAL_AMOUNT) { + return null; + } + + if (amount > MAX_PAYPAL_AMOUNT) { + return null; + } + + if (doNotWindowOpen()) { + return null; + } + + if (retry) { + const handleRetry = () => { + paypal.onToken(); + setRetry(false); + }; + return ; + } + + if (paypal.loadingVerification) { + return ; + } + + const handleClick = async () => { + try { + await paypal.onVerification(); + } catch (error) { + // if not coming from API error + if (error.message && !error.config) { + createNotification({ text: error.message, type: 'error' }); + } + setRetry(true); + } + }; + + return ( + + ); +}; + +PayPalButton.propTypes = { + className: PropTypes.string, + amount: PropTypes.number.isRequired, + children: PropTypes.node.isRequired, + paypal: PropTypes.object.isRequired +}; + +export default PayPalButton; diff --git a/containers/payments/PayPalView.js b/containers/payments/PayPalView.js new file mode 100644 index 000000000..b9c657d13 --- /dev/null +++ b/containers/payments/PayPalView.js @@ -0,0 +1,87 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert, DoNotWindowOpenAlertError, Price, Loader } from 'react-components'; +import { MIN_PAYPAL_AMOUNT, MAX_PAYPAL_AMOUNT } from 'proton-shared/lib/constants'; +import { doNotWindowOpen } from 'proton-shared/lib/helpers/browser'; +import { c } from 'ttag'; + +import PayPalButton from './PayPalButton'; + +const PayPalView = ({ type, amount, currency, paypal, paypalCredit }) => { + if (type === 'payment' && amount < MIN_PAYPAL_AMOUNT) { + return ( + + {c('Error').t`Amount below minimum.`} {`(${({MIN_PAYPAL_AMOUNT})})`} + + ); + } + + if (amount > MAX_PAYPAL_AMOUNT) { + return {c('Error').t`Amount above the maximum.`}; + } + + if (doNotWindowOpen()) { + return ; + } + + const clickHere = ( + + {c('Link').t`click here`} + + ); + + return ( + <> + {paypal.loading ? ( + <> + + {c('Info').t`Please verify the payment in the new tab.`} + + ) : null} + {!paypal.loading && ['signup', 'subscription', 'invoice', 'credit'].includes(type) ? ( + <> + + {c('Info') + .t`We will redirect you to PayPal in a new browser tab to complete this transaction. If you use any pop-up blockers, please disable them to continue.`} + + {c('Info') + .jt`You must have a credit card or bank account linked with your PayPal account. If your PayPal account doesn't have that, please ${clickHere}.`} + + ) : null} + {!paypal.loading && type === 'update' ? ( + <> + + {c('Info') + .t`This will enable PayPal to be used to pay for your Proton subscription. We will redirect you to PayPal in a new browser tab. If you use any pop-up blockers, please disable them to continue.`} + + {c('Info') + .t`You must have a credit card or bank account linked with your PayPal account in order to add it as a payment method.`} + + ) : null} + {!paypal.loading && type === 'donation' ? ( + <> + + {c('Info') + .t`We will redirect you to PayPal in a new browser tab to complete this transaction. If you use any pop-up blockers, please disable them to continue.`} + + + ) : null} + + ); +}; + +PayPalView.propTypes = { + type: PropTypes.oneOf(['signup', 'subscription', 'invoice', 'donation', 'credit', 'update']), + amount: PropTypes.number.isRequired, + currency: PropTypes.string.isRequired, + onPay: PropTypes.func, + paypal: PropTypes.object.isRequired, + paypalCredit: PropTypes.object.isRequired +}; + +export default PayPalView; diff --git a/containers/payments/Payment.js b/containers/payments/Payment.js index 34e896a5f..23839dbb3 100644 --- a/containers/payments/Payment.js +++ b/containers/payments/Payment.js @@ -1,27 +1,29 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { classnames, Radio, Icon, Row, Alert, Price, LinkButton, Loader } from 'react-components'; -import { PAYMENT_METHOD_TYPES, MIN_DONATION_AMOUNT, MIN_CREDIT_AMOUNT } from 'proton-shared/lib/constants'; +import { classnames, Radio, Icon, Row, Alert, Price, Loader } from 'react-components'; +import { + PAYMENT_METHOD_TYPES, + MIN_DONATION_AMOUNT, + MIN_CREDIT_AMOUNT, + DEFAULT_CURRENCY, + CURRENCIES +} from 'proton-shared/lib/constants'; import Method from './Method'; -import toDetails from './toDetails'; import usePaymentMethods from '../paymentMethods/usePaymentMethods'; -const { CARD, PAYPAL, CASH, BITCOIN } = PAYMENT_METHOD_TYPES; - const Payment = ({ children, type, - amount, - currency, - coupon, - onParameters, + amount = 0, + currency = DEFAULT_CURRENCY, + coupon = '', method, onMethod, - onValidCard, - onPay, - card + card, + onCard, + errors }) => { const { methods, options, loading } = usePaymentMethods({ amount, coupon, type }); const lastCustomMethod = [...options] @@ -36,22 +38,9 @@ const Payment = ({ ].includes(value) ); - const handleCard = ({ card, isValid }) => { - onValidCard(isValid); - isValid && onParameters({ Payment: { Type: CARD, Details: toDetails(card) } }); - }; - - const handleChangeMethod = (newMethod) => { - onMethod(newMethod); - - if (![CARD, PAYPAL, CASH, BITCOIN].includes(newMethod)) { - onParameters({ PaymentMethodID: newMethod }); - } - }; - useEffect(() => { const { value } = options.find(({ disabled }) => !disabled); - handleChangeMethod(value); + onMethod(value); }, [methods.length]); if (type === 'donation' && amount < MIN_DONATION_AMOUNT) { @@ -90,14 +79,13 @@ const Payment = ({
- {options.map(({ text, value, disabled, icon }, index) => { + {options.map(({ text, value, disabled, icon }) => { return ( ); })} -
- - - {c('Link').t`Use gift code`} - -
{children}
@@ -144,14 +126,13 @@ Payment.propTypes = { type: PropTypes.oneOf(['signup', 'subscription', 'invoice', 'donation', 'credit']), amount: PropTypes.number.isRequired, coupon: PropTypes.string, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']), + currency: PropTypes.oneOf(CURRENCIES).isRequired, parameters: PropTypes.object, card: PropTypes.object, - onParameters: PropTypes.func, + onCard: PropTypes.func, method: PropTypes.string, onMethod: PropTypes.func, - onValidCard: PropTypes.func, - onPay: PropTypes.func + errors: PropTypes.object }; export default Payment; diff --git a/containers/payments/PaymentGiftCode.js b/containers/payments/PaymentGiftCode.js new file mode 100644 index 000000000..2eb8f01a0 --- /dev/null +++ b/containers/payments/PaymentGiftCode.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useToggle, Icon, LinkButton } from 'react-components'; +import { isValid } from 'proton-shared/lib/helpers/giftCode'; +import { c } from 'ttag'; + +import GiftCodeForm from './subscription/GiftCodeForm'; + +const PaymentGiftCode = ({ gift = '', onApply, loading }) => { + const { state, toggle } = useToggle(); + const [code, setCode] = useState(''); + + if (gift) { + return ( +
+ + + {gift} + + onApply('')} /> +
+ ); + } + + if (state) { + const handleSubmit = () => { + if (!isValid(code)) { + return; + } + + onApply(code); + }; + + return ; + } + + return ( +
+ {c('Link').t`Add a gift code`} +
+ ); +}; + +PaymentGiftCode.propTypes = { + loading: PropTypes.bool, + gift: PropTypes.string, + onApply: PropTypes.func.isRequired +}; + +export default PaymentGiftCode; diff --git a/containers/payments/subscription/GiftCodeForm.js b/containers/payments/subscription/GiftCodeForm.js index cf069a02e..1ddd98362 100644 --- a/containers/payments/subscription/GiftCodeForm.js +++ b/containers/payments/subscription/GiftCodeForm.js @@ -1,37 +1,31 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { PrimaryButton, GiftCodeInput, useNotifications } from 'react-components'; +import { PrimaryButton, GiftCodeInput } from 'react-components'; import { isValid } from 'proton-shared/lib/helpers/giftCode'; -const GiftCodeForm = ({ onChange, model }) => { - const { createNotification } = useNotifications(); - const [gift, setGift] = useState(model.gift || ''); - const handleChange = ({ target }) => setGift(target.value); - - const handleClick = () => { - if (!isValid(gift)) { - createNotification({ text: c('Error').t`Invalid gift code`, type: 'error' }); - return; - } - onChange({ ...model, gift }, true); - }; - +const GiftCodeForm = ({ code, loading, disabled, onChange, onSubmit }) => { return ( -
-
- -
-
- {c('Action').t`Apply`} +
+
+ onChange(target.value)} />
+ {c('Action').t`Apply`}
); }; GiftCodeForm.propTypes = { onChange: PropTypes.func.isRequired, - model: PropTypes.object.isRequired + onSubmit: PropTypes.func, + loading: PropTypes.bool, + disabled: PropTypes.bool, + code: PropTypes.string }; export default GiftCodeForm; diff --git a/containers/payments/subscription/MailSubscriptionTable.js b/containers/payments/subscription/MailSubscriptionTable.js index 6ec739ccb..63f574d1f 100644 --- a/containers/payments/subscription/MailSubscriptionTable.js +++ b/containers/payments/subscription/MailSubscriptionTable.js @@ -1,7 +1,7 @@ import React from 'react'; import { SubscriptionTable, LinkButton, useModals } from 'react-components'; import PropTypes from 'prop-types'; -import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; +import { PLAN_NAMES, PLANS, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { toMap } from 'proton-shared/lib/helpers/object'; import { c } from 'ttag'; import freePlanSvg from 'design-system/assets/img/pm-images/free-plan.svg'; @@ -143,7 +143,7 @@ MailSubscriptionTable.propTypes = { plans: PropTypes.arrayOf(PropTypes.object), onSelect: PropTypes.func.isRequired, cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired + currency: PropTypes.oneOf(CURRENCIES).isRequired }; export default MailSubscriptionTable; diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index dcc00bda0..2619cd160 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -2,7 +2,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; import { + classnames, Alert, + PrimaryButton, FormModal, Payment, usePlans, @@ -11,9 +13,9 @@ import { useVPNCountries, useEventManager, usePayment, - useCard + useUser } from 'react-components'; -import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; +import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; import SubscriptionCustomization from './SubscriptionCustomization'; @@ -23,6 +25,8 @@ import SubscriptionCheckout from './SubscriptionCheckout'; import NewSubscriptionModalFooter from './NewSubscriptionModalFooter'; import './NewSubscriptionModal.scss'; +import PayPalButton from '../PayPalButton'; +import PaymentGiftCode from '../PaymentGiftCode'; const STEPS = { CUSTOMIZATION: 0, @@ -53,20 +57,19 @@ const NewSubscriptionModal = ({ }) => { const TITLE = { [STEPS.CUSTOMIZATION]: c('Title').t`Plan customization`, - [STEPS.PAYMENT]: c('Title').t`Billing details`, - [STEPS.UPGRADE]: c('Title').t`???`, // TODO - [STEPS.THANKS]: c('Title').t`???` // TODO + [STEPS.PAYMENT]: c('Title').t`Checkout`, + [STEPS.UPGRADE]: c('Title').t`Upgrading`, + [STEPS.THANKS]: c('Title').t`Thanks` }; const api = useApi(); + const [user] = useUser(); const { call } = useEventManager(); const [vpnCountries, loadingVpnCountries] = useVPNCountries(); const [plans, loadingPlans] = usePlans(); const [loading, withLoading] = useLoading(); const [loadingCheck, withLoadingCheck] = useLoading(); const [checkResult, setCheckResult] = useState({}); - const card = useCard(); - const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); const { Code: couponCode } = checkResult.Coupon || {}; // Coupon can be null const [model, setModel] = useState({ cycle, @@ -75,45 +78,8 @@ const NewSubscriptionModal = ({ planIDs }); const [step, setStep] = useState(initialStep); - const submit = - step === STEPS.CUSTOMIZATION - ? checkResult.AmountDue - ? c('Action').t`Continue` - : c('Action').t`Finish` - : c('Action').t`Pay`; - - /** - * Check current configuration to see if it's valid - * @param {Object} newModel - * @returns {Promise} - */ - const check = async (newModel = model) => { - try { - const result = await api( - checkSubscription({ - PlanIDs: clearPlanIDs(newModel.planIDs), - CouponCode: newModel.coupon, - Currency: newModel.currency, - Cycle: newModel.cycle - }) - ); - const { Code = '' } = result.Coupon || {}; // Coupon can equal null - newModel.coupon = Code; - - setModel(newModel); - setCheckResult(result); - } catch (error) { - setModel(model); - throw error; - } - }; - - /** - * Subscribe to a new subscription - * @return {Promise} - */ - const handleSubscribe = async () => { + const handleSubscribe = async (params) => { try { setStep(STEPS.UPGRADE); await withLoading( @@ -122,9 +88,10 @@ const NewSubscriptionModal = ({ Amount: checkResult.AmountDue, PlanIDs: model.planIDs, CouponCode: model.coupon, + GiftCode: model.gift, Currency: model.currency, Cycle: model.cycle, - ...parameters + ...params }) ) ); @@ -136,11 +103,81 @@ const NewSubscriptionModal = ({ } }; + const { card, setCard, errors, method, setMethod, parameters, canPay, paypal, paypalCredit } = usePayment({ + amount: checkResult.AmountDue, + currency: checkResult.Currency, + onPay: handleSubscribe + }); + + const Submit = ({ className }) => { + if (step === STEPS.CUSTOMIZATION) { + return ( + {c('Action') + .t`Continue`} + ); + } + + if (method === PAYMENT_METHOD_TYPES.PAYPAL) { + return ( + {c('Action') + .t`Pay`} + ); + } + + if ([PAYMENT_METHOD_TYPES.CASH, PAYMENT_METHOD_TYPES.BITCOIN].includes(method)) { + return ( + {c('Action') + .t`Done`} + ); + } + + return ( + {c('Action') + .t`Pay`} + ); + }; + + Submit.propTypes = { + className: PropTypes.string + }; + + const check = async (newModel = model) => { + try { + const result = await api( + checkSubscription({ + PlanIDs: clearPlanIDs(newModel.planIDs), + CouponCode: newModel.coupon, + Currency: newModel.currency, + Cycle: newModel.cycle, + GiftCode: newModel.gift + }) + ); + + const { Code = '' } = result.Coupon || {}; // Coupon can equal null + newModel.coupon = Code; + + if (!result.Gift) { + delete newModel.gift; + } + + setModel(newModel); + setCheckResult(result); + } catch (error) { + setModel(model); + throw error; + } + }; + const handleCheckout = () => { if (!checkResult.AmountDue) { - return handleSubscribe(); + return handleSubscribe(parameters); + } + + if (step === STEPS.CUSTOMIZATION) { + return setStep(STEPS.PAYMENT); } - setStep(STEPS.PAYMENT); + + handleSubscribe(parameters); }; const handleClose = (e) => { @@ -152,24 +189,22 @@ const NewSubscriptionModal = ({ onClose(e); }; + const handleGift = (gift = '') => { + withLoadingCheck(check({ ...model, gift })); + }; + useEffect(() => { withLoadingCheck(check()); }, [model.cycle, model.planIDs]); return ( - } - className="pm-modal--full subscription-modal" + hasClose={step === STEPS.CUSTOMIZATION} + footer={} step={step} model={model} />} + className={classnames(['pm-modal--full subscription-modal', user.isFree && 'is-free-user'])} title={TITLE[step]} loading={loading || loadingPlans || loadingVpnCountries} + onSubmit={handleCheckout} onClose={handleClose} {...rest} > @@ -187,7 +222,7 @@ const NewSubscriptionModal = ({
} plans={plans} checkResult={checkResult} loading={loadingCheck} @@ -195,6 +230,7 @@ const NewSubscriptionModal = ({ model={model} setModel={setModel} /> +
)} @@ -205,34 +241,35 @@ const NewSubscriptionModal = ({ {c('Info').t`You can use any of your saved payment methods or add a new one.`}
} plans={plans} checkResult={checkResult} loading={loadingCheck} onCheckout={handleCheckout} model={model} setModel={setModel} - disabled={!canPay} /> +
)} {step === STEPS.UPGRADE && } - {step === STEPS.THANKS && } + {step === STEPS.THANKS && } ); }; diff --git a/containers/payments/subscription/NewSubscriptionModal.scss b/containers/payments/subscription/NewSubscriptionModal.scss index feddba7c2..c43005adc 100644 --- a/containers/payments/subscription/NewSubscriptionModal.scss +++ b/containers/payments/subscription/NewSubscriptionModal.scss @@ -13,8 +13,8 @@ } // Hide first subscription table in the subscription modal -.subscription-modal .subscriptionCustomization-section:first-child .vpnSubscriptionTable-container, -.subscription-modal .subscriptionCustomization-section:first-child .mailSubscriptionTable-container { +.subscription-modal.is-free-user .subscriptionCustomization-section:first-child .vpnSubscriptionTable-container, +.subscription-modal.is-free-user .subscriptionCustomization-section:first-child .mailSubscriptionTable-container { display: none; } diff --git a/containers/payments/subscription/NewSubscriptionModalFooter.js b/containers/payments/subscription/NewSubscriptionModalFooter.js index 54e5c73ef..381066df9 100644 --- a/containers/payments/subscription/NewSubscriptionModalFooter.js +++ b/containers/payments/subscription/NewSubscriptionModalFooter.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useAddresses, Button, PrimaryButton, Loader } from 'react-components'; +import { useAddresses, Button, Loader } from 'react-components'; import { c } from 'ttag'; import { CYCLE, COUPON_CODES } from 'proton-shared/lib/constants'; import checkmarkSvg from 'design-system/assets/img/shared/checkmark-icon.svg'; @@ -16,7 +16,7 @@ const STEPS = { const CheckmarkIcon = () => checkmark; const PercentageIcon = () => percentage; -const NewSubscriptionModalFooter = ({ submit, step, model, loading, disabled = false }) => { +const NewSubscriptionModalFooter = ({ submit, step, model }) => { const [addresses, loadingAddresses] = useAddresses(); if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { @@ -64,21 +64,15 @@ const NewSubscriptionModalFooter = ({ submit, step, model, loading, disabled = f return ( <> - + {upsells} - - {submit} - + {submit} ); }; NewSubscriptionModalFooter.propTypes = { - disabled: PropTypes.bool, - submit: PropTypes.string, - loading: PropTypes.bool, + submit: PropTypes.node.isRequired, step: PropTypes.number, model: PropTypes.object }; diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index 3e2bc19ab..2ee5766b3 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -1,16 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { c, msgid } from 'ttag'; -import { - CurrencySelector, - CycleSelector, - PrimaryButton, - Icon, - Loader, - Price, - useOrganization, - classnames -} from 'react-components'; +import { CurrencySelector, CycleSelector, Loader, Price, useOrganization, classnames } from 'react-components'; import { isLoyal } from 'proton-shared/lib/helpers/organization'; import { toMap } from 'proton-shared/lib/helpers/object'; import { orderBy } from 'proton-shared/lib/helpers/array'; @@ -48,16 +39,7 @@ CheckoutRow.propTypes = { currency: PropTypes.string.isRequired }; -const SubscriptionCheckout = ({ - submit = c('Action').t`Pay`, - disabled = false, - plans = [], - model, - setModel, - checkResult, - onCheckout, - loading -}) => { +const SubscriptionCheckout = ({ submit = c('Action').t`Pay`, plans = [], model, setModel, checkResult, loading }) => { const plansMap = toMap(plans); const storageAddon = plans.find(({ Name }) => Name === ADDON_NAMES.SPACE); const addressAddon = plans.find(({ Name }) => Name === ADDON_NAMES.ADDRESS); @@ -144,16 +126,16 @@ const SubscriptionCheckout = ({ return ( <>
- setModel({ ...model, currency: newCurrency })} - /> setModel({ ...model, cycle: newCycle })} /> + setModel({ ...model, currency: newCurrency })} + />
{c('Title') @@ -266,24 +248,7 @@ const SubscriptionCheckout = ({ currency={model.currency} className="bold" /> -
- - {submit} - -
-
-
-
- - {c('Info').t`Guarantee`} -
-
{c('Info').t`30-days money back guaranteed`}
-
- - {c('Info').t`Secure`} -
-
{c('Info') - .t`Payments are protected with TLS encryption and Swiss privacy laws`}
+
{submit}
); @@ -291,7 +256,7 @@ const SubscriptionCheckout = ({ SubscriptionCheckout.propTypes = { disabled: PropTypes.bool, - submit: PropTypes.string, + submit: PropTypes.node, plans: PropTypes.array.isRequired, checkResult: PropTypes.object.isRequired, model: PropTypes.object.isRequired, diff --git a/containers/payments/subscription/SubscriptionModal.js b/containers/payments/subscription/SubscriptionModal.js index bde8a853c..498e61c39 100644 --- a/containers/payments/subscription/SubscriptionModal.js +++ b/containers/payments/subscription/SubscriptionModal.js @@ -53,14 +53,14 @@ const SubscriptionModal = ({ const api = useApi(); const { createModal } = useModals(); const [loading, setLoading] = useState(false); - const { method, setMethod, parameters, setParameters, canPay, setCardValidity } = usePayment(); + const { method, setMethod, parameters, canPay, setCardValidity } = usePayment(); const { createNotification } = useNotifications(); const [check, setCheck] = useState({}); const [plans] = usePlans(); const [model, setModel] = useState({ cycle, currency, coupon, plansMap }); const { call } = useEventManager(); const { step, next, previous, goTo } = useStep(initialStep); - const card = useCard(); + const [card, setCard, errors] = useCard(); const callCheck = async (m = model) => { try { @@ -224,12 +224,12 @@ const SubscriptionModal = ({ cycle={model.cycle} currency={model.currency} coupon={model.coupon} - parameters={parameters} card={card} - onParameters={setParameters} onMethod={setMethod} onValidCard={setCardValidity} onPay={handleSubmit} + onCard={setCard} + errors={errors} /> ), diff --git a/containers/payments/subscription/SubscriptionPrices.js b/containers/payments/subscription/SubscriptionPrices.js index 90f9d27f6..136ad098f 100644 --- a/containers/payments/subscription/SubscriptionPrices.js +++ b/containers/payments/subscription/SubscriptionPrices.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Price } from 'react-components'; -import { CYCLE } from 'proton-shared/lib/constants'; +import { CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { c } from 'ttag'; const FREE_PLAN = { @@ -36,7 +36,7 @@ const SubscriptionPrices = ({ cycle, currency, plan = FREE_PLAN, suffix = c('Suf SubscriptionPrices.propTypes = { suffix: PropTypes.string, cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired, + currency: PropTypes.oneOf(CURRENCIES).isRequired, plan: PropTypes.shape({ Pricing: PropTypes.object }) diff --git a/containers/payments/subscription/SubscriptionThanks.js b/containers/payments/subscription/SubscriptionThanks.js index 8dc4d3296..f4af8e710 100644 --- a/containers/payments/subscription/SubscriptionThanks.js +++ b/containers/payments/subscription/SubscriptionThanks.js @@ -1,3 +1,21 @@ -const SubscriptionThanks = () => {}; +import React from 'react'; +import PropTypes from 'prop-types'; +import { c } from 'ttag'; +import { Button } from 'react-components'; -export default SubscriptionThanks; \ No newline at end of file +const SubscriptionThanks = ({ onClose }) => { + return ( + <> +
{c('Info').t`Thanks`}
+
+ +
+ + ); +}; + +SubscriptionThanks.propTypes = { + onClose: PropTypes.func.isRequired +}; + +export default SubscriptionThanks; diff --git a/containers/payments/subscription/VpnSubscriptionTable.js b/containers/payments/subscription/VpnSubscriptionTable.js index e6c8a9a23..6b506201a 100644 --- a/containers/payments/subscription/VpnSubscriptionTable.js +++ b/containers/payments/subscription/VpnSubscriptionTable.js @@ -1,7 +1,7 @@ import React from 'react'; import { SubscriptionTable, useVPNCountries, LinkButton, useModals } from 'react-components'; import PropTypes from 'prop-types'; -import { PLAN_NAMES, PLANS, CYCLE } from 'proton-shared/lib/constants'; +import { PLAN_NAMES, PLANS, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; import { toMap } from 'proton-shared/lib/helpers/object'; import { c } from 'ttag'; import freePlanSvg from 'design-system/assets/img/pv-images/plans/free-plan.svg'; @@ -130,7 +130,7 @@ VpnSubscriptionTable.propTypes = { plans: PropTypes.arrayOf(PropTypes.object), onSelect: PropTypes.func.isRequired, cycle: PropTypes.oneOf([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]).isRequired, - currency: PropTypes.oneOf(['EUR', 'CHF', 'USD']).isRequired + currency: PropTypes.oneOf(CURRENCIES).isRequired }; export default VpnSubscriptionTable; diff --git a/containers/payments/toDetails.js b/containers/payments/toDetails.js index 1df21e2f4..ab34a98d5 100644 --- a/containers/payments/toDetails.js +++ b/containers/payments/toDetails.js @@ -3,9 +3,11 @@ const formatYear = (year) => { return `${pre}${year}`; }; +const clear = (v) => String(v).trim(); + const toDetails = ({ number, month: ExpMonth, year, cvc: CVC, fullname, zip: ZIP, country: Country }) => { return { - Name: String(fullname).trim(), + Name: clear(fullname), Number: String(number).replace(/\s+/g, ''), ExpMonth, ExpYear: formatYear(year), diff --git a/containers/payments/useCard.js b/containers/payments/useCard.js index ca31bcd64..756195660 100644 --- a/containers/payments/useCard.js +++ b/containers/payments/useCard.js @@ -9,12 +9,7 @@ const useCard = (initialCard = getDefaultCard()) => { const errors = getErrors(card); const isValid = !Object.keys(errors).length; - return { - card, - updateCard, - errors, - isValid - }; + return [card, updateCard, errors, isValid]; }; export default useCard; diff --git a/containers/payments/usePayment.js b/containers/payments/usePayment.js index 315f8f2e5..d3ad0ce66 100644 --- a/containers/payments/usePayment.js +++ b/containers/payments/usePayment.js @@ -1,12 +1,29 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; +import { useCard, usePayPal } from 'react-components'; + +import toDetails from './toDetails'; const { CARD, BITCOIN, CASH, PAYPAL } = PAYMENT_METHOD_TYPES; -const usePayment = () => { +const usePayment = ({ amount, currency, onPay }) => { + const [card, setCard, errors, isValid] = useCard(); const [method, setMethod] = useState(''); const [parameters, setParameters] = useState({}); - const [isCardValid, setCardValidity] = useState(false); + + const paypal = usePayPal({ + amount, + currency, + type: PAYMENT_METHOD_TYPES.PAYPAL, + onPay + }); + + const paypalCredit = usePayPal({ + amount, + currency, + type: PAYMENT_METHOD_TYPES.PAYPAL_CREDIT, + onPay + }); const hasToken = () => { const { Payment = {} } = parameters; @@ -24,7 +41,7 @@ const usePayment = () => { return false; } - if (method === CARD && !isCardValid) { + if (method === CARD && !isValid) { return false; } @@ -35,13 +52,32 @@ const usePayment = () => { return true; }; + useEffect(() => { + if (![CARD, PAYPAL, CASH, BITCOIN].includes(method)) { + setParameters({ PaymentMethodID: method }); + } + + if (method === CARD) { + setParameters({ Payment: { Type: CARD, Details: toDetails(card) } }); + } + + // Reset parameters when switching methods + if ([PAYPAL, CASH, BITCOIN].includes(method)) { + setParameters({}); + } + }, [method, card]); + return { + paypal, + paypalCredit, + card, + setCard, + errors, method, setMethod, parameters, setParameters, - canPay: canPay(), - setCardValidity + canPay: canPay() }; }; diff --git a/hooks/usePayPal.js b/hooks/usePayPal.js new file mode 100644 index 000000000..f41552338 --- /dev/null +++ b/hooks/usePayPal.js @@ -0,0 +1,49 @@ +import { useEffect, useState, useRef } from 'react'; +import { createToken } from 'proton-shared/lib/api/payments'; +import { useApi, useLoading } from 'react-components'; + +import { toParams, process } from '../containers/payments/paymentTokenHelper'; + +const usePayPal = ({ amount: Amount = 0, currency: Currency = '', type: Type, onPay }) => { + const api = useApi(); + const [model, setModel] = useState({}); + const abortRef = useRef(); + const [loadingVerification, withLoadingVerification] = useLoading(); + const [loadingToken, withLoadingToken] = useLoading(); + const onCancel = () => abortRef.current && abortRef.current.abort(); + + const onToken = async () => { + const result = api( + createToken({ + Amount, + Currency, + Payment: { Type } + }) + ); + setModel(result); + }; + + const onVerification = async () => { + abortRef.current = new AbortController(); + const { Token, ApprovalURL, ReturnHost } = model; + await process({ Token, api, ApprovalURL, ReturnHost, signal: abortRef.current.signal }); + onPay(toParams({ Amount, Currency }, Token, Type)); + }; + + useEffect(() => { + if (Amount) { + withLoadingToken(onToken()); + } + }, [Amount, Currency]); + + return { + isReady: !!model.Token, + loadingToken, + loadingVerification, + onCancel, + onToken: () => withLoadingToken(onToken()), + onVerification: () => withLoadingVerification(onVerification()) + }; +}; + +export default usePayPal; diff --git a/index.ts b/index.ts index 9b501b25f..e852290fe 100644 --- a/index.ts +++ b/index.ts @@ -337,6 +337,7 @@ export { default as OpenVPNAccountSection } from './containers/vpn/OpenVPNAccoun export { default as ProtonVPNResourcesSection } from './containers/vpn/ProtonVPNResourcesSection/ProtonVPNResourcesSection'; export { default as ProtonVPNCredentialsSection } from './containers/vpn/ProtonVPNCredentialsSection/ProtonVPNCredentialsSection'; +export { default as usePayPal } from './hooks/usePayPal'; export { useUser, useGetUser } from './hooks/useUser'; export { default as useUserVPN } from './hooks/useUserVPN'; export { default as useBlackFriday } from './hooks/useBlackFriday'; From f1e49cb89797993fce18150737c4d0e4e163dcaf Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 17 Dec 2019 18:44:36 +0100 Subject: [PATCH 030/242] Add VPN icons --- containers/payments/Bitcoin.js | 16 +++--- containers/payments/BitcoinDetails.js | 26 +++++----- .../subscription/NewSubscriptionModal.js | 51 ++++++++++--------- .../subscription/SubscriptionCustomization.js | 28 +++++----- containers/payments/usePayment.js | 5 ++ 5 files changed, 65 insertions(+), 61 deletions(-) diff --git a/containers/payments/Bitcoin.js b/containers/payments/Bitcoin.js index 6c819fb91..485056ace 100644 --- a/containers/payments/Bitcoin.js +++ b/containers/payments/Bitcoin.js @@ -54,13 +54,15 @@ const Bitcoin = ({ amount, currency, type }) => { return ( <> -
- +
+
+ +
{type === 'invoice' ? ( diff --git a/containers/payments/BitcoinDetails.js b/containers/payments/BitcoinDetails.js index 2a02514b9..85466d87d 100644 --- a/containers/payments/BitcoinDetails.js +++ b/containers/payments/BitcoinDetails.js @@ -1,26 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { Row, Label, Field, Copy } from 'react-components'; +import { Copy } from 'react-components'; const BitcoinDetails = ({ amount, address }) => { return (
- - - - {amount} - - - - - - +
+ + {amount} +
+
+ +
+ {address} - + - - +
+
); }; diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 2619cd160..e2f1d5d73 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -98,7 +98,7 @@ const NewSubscriptionModal = ({ await withLoading(call()); setStep(STEPS.THANKS); } catch (error) { - setStep(checkResult.AmountDue ? STEPS.PAYMENT : STEPS.CUSTOMIZATION); + setStep(STEPS.PAYMENT); throw error; } }; @@ -109,7 +109,7 @@ const NewSubscriptionModal = ({ onPay: handleSubscribe }); - const Submit = ({ className }) => { + const SubmitButton = ({ className }) => { if (step === STEPS.CUSTOMIZATION) { return ( {c('Action') @@ -137,7 +137,7 @@ const NewSubscriptionModal = ({ ); }; - Submit.propTypes = { + SubmitButton.propTypes = { className: PropTypes.string }; @@ -169,10 +169,6 @@ const NewSubscriptionModal = ({ }; const handleCheckout = () => { - if (!checkResult.AmountDue) { - return handleSubscribe(parameters); - } - if (step === STEPS.CUSTOMIZATION) { return setStep(STEPS.PAYMENT); } @@ -200,7 +196,7 @@ const NewSubscriptionModal = ({ return ( } step={step} model={model} />} + footer={} step={step} model={model} />} className={classnames(['pm-modal--full subscription-modal', user.isFree && 'is-free-user'])} title={TITLE[step]} loading={loading || loadingPlans || loadingVpnCountries} @@ -222,7 +218,7 @@ const NewSubscriptionModal = ({
} + submit={} plans={plans} checkResult={checkResult} loading={loadingCheck} @@ -238,25 +234,32 @@ const NewSubscriptionModal = ({

{c('Title').t`Payment method`}

- {c('Info').t`You can use any of your saved payment methods or add a new one.`} - + {checkResult.AmountDue ? ( + <> + {c('Info') + .t`You can use any of your saved payment methods or add a new one.`} + + + ) : ( + {c('Info').t`No payment is required at this time.`} + )}
} + submit={} plans={plans} checkResult={checkResult} loading={loadingCheck} diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js index 5487db1e0..369bd72c8 100644 --- a/containers/payments/subscription/SubscriptionCustomization.js +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -261,38 +261,34 @@ const SubscriptionCustomization = ({ ], [VPNFREE]: [ - , + , , - , - + , + ], [PLANS.VPNBASIC]: [ - , + , , - , - + , + ], [PLANS.VPNPLUS]: [ - , + , , - , - , + , + , ] }; diff --git a/containers/payments/usePayment.js b/containers/payments/usePayment.js index d3ad0ce66..22f92109d 100644 --- a/containers/payments/usePayment.js +++ b/containers/payments/usePayment.js @@ -33,6 +33,11 @@ const usePayment = ({ amount, currency, onPay }) => { }; const canPay = () => { + if (!amount) { + // Amount equals 0 + return true; + } + if (!method) { return false; } From 38ef96ed4a18ea8cd619d691851b4b096a976179 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 18 Dec 2019 10:54:03 +0100 Subject: [PATCH 031/242] Continue --- .../subscription/NewSubscriptionModal.js | 77 +++++++++--- .../subscription/NewSubscriptionModal.scss | 4 - .../NewSubscriptionModalFooter.js | 38 ++++-- .../subscription/SubscriptionCheckout.js | 116 +++++++++--------- 4 files changed, 145 insertions(+), 90 deletions(-) diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index e2f1d5d73..4366d74a5 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -13,10 +13,11 @@ import { useVPNCountries, useEventManager, usePayment, - useUser + useUser, + useNotifications } from 'react-components'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; -import { checkSubscription, subscribe } from 'proton-shared/lib/api/payments'; +import { checkSubscription, subscribe, deleteSubscription } from 'proton-shared/lib/api/payments'; import SubscriptionCustomization from './SubscriptionCustomization'; import SubscriptionUpgrade from './SubscriptionUpgrade'; @@ -45,6 +46,8 @@ const clearPlanIDs = (planIDs = {}) => { }, {}); }; +const hasPlans = (planIDs = {}) => Object.keys(clearPlanIDs(planIDs)).length; + const NewSubscriptionModal = ({ expanded = false, step: initialStep = STEPS.CUSTOMIZATION, @@ -65,6 +68,7 @@ const NewSubscriptionModal = ({ const api = useApi(); const [user] = useUser(); const { call } = useEventManager(); + const { createNotification } = useNotifications(); const [vpnCountries, loadingVpnCountries] = useVPNCountries(); const [plans, loadingPlans] = usePlans(); const [loading, withLoading] = useLoading(); @@ -79,23 +83,43 @@ const NewSubscriptionModal = ({ }); const [step, setStep] = useState(initialStep); + const TOTAL_ZERO = { + Amount: 0, + AmountDue: 0, + CouponDiscount: 0, + Currency: model.currency, + Cycle: model.cycle, + Proration: 0, + Gift: 0, + Credit: 0 + }; + + const handleUnsubscribe = async () => { + await api(deleteSubscription()); + await call(); + onClose(); + createNotification({ text: c('Success').t`You have successfully unsubscribed` }); + }; + const handleSubscribe = async (params) => { + if (!hasPlans(model.planIDs)) { + return handleUnsubscribe(); + } + try { setStep(STEPS.UPGRADE); - await withLoading( - api( - subscribe({ - Amount: checkResult.AmountDue, - PlanIDs: model.planIDs, - CouponCode: model.coupon, - GiftCode: model.gift, - Currency: model.currency, - Cycle: model.cycle, - ...params - }) - ) + await api( + subscribe({ + Amount: checkResult.AmountDue, + PlanIDs: model.planIDs, + CouponCode: model.coupon, + GiftCode: model.gift, + Currency: model.currency, + Cycle: model.cycle, + ...params + }) ); - await withLoading(call()); + await call(); setStep(STEPS.THANKS); } catch (error) { setStep(STEPS.PAYMENT); @@ -106,7 +130,9 @@ const NewSubscriptionModal = ({ const { card, setCard, errors, method, setMethod, parameters, canPay, paypal, paypalCredit } = usePayment({ amount: checkResult.AmountDue, currency: checkResult.Currency, - onPay: handleSubscribe + onPay(params) { + return withLoading(handleSubscribe(params)); + } }); const SubmitButton = ({ className }) => { @@ -131,6 +157,14 @@ const NewSubscriptionModal = ({ ); } + if (!checkResult.AmountDue) { + return ( + {c( + 'Action' + ).t`Complete`} + ); + } + return ( {c('Action') .t`Pay`} @@ -142,6 +176,11 @@ const NewSubscriptionModal = ({ }; const check = async (newModel = model) => { + if (!hasPlans(newModel.planIDs)) { + setCheckResult(TOTAL_ZERO); + return; + } + try { const result = await api( checkSubscription({ @@ -173,7 +212,7 @@ const NewSubscriptionModal = ({ return setStep(STEPS.PAYMENT); } - handleSubscribe(parameters); + withLoading(handleSubscribe(parameters)); }; const handleClose = (e) => { @@ -267,7 +306,9 @@ const NewSubscriptionModal = ({ model={model} setModel={setModel} /> - + {checkResult.Amount ? ( + + ) : null}
)} diff --git a/containers/payments/subscription/NewSubscriptionModal.scss b/containers/payments/subscription/NewSubscriptionModal.scss index c43005adc..5774ad9d3 100644 --- a/containers/payments/subscription/NewSubscriptionModal.scss +++ b/containers/payments/subscription/NewSubscriptionModal.scss @@ -8,10 +8,6 @@ display: none; } -.subscription-modal .subscriptionTable [data-plan-name="visionary"] { - display: none; -} - // Hide first subscription table in the subscription modal .subscription-modal.is-free-user .subscriptionCustomization-section:first-child .vpnSubscriptionTable-container, .subscription-modal.is-free-user .subscriptionCustomization-section:first-child .mailSubscriptionTable-container { diff --git a/containers/payments/subscription/NewSubscriptionModalFooter.js b/containers/payments/subscription/NewSubscriptionModalFooter.js index 381066df9..050fd3a5e 100644 --- a/containers/payments/subscription/NewSubscriptionModalFooter.js +++ b/containers/payments/subscription/NewSubscriptionModalFooter.js @@ -3,8 +3,10 @@ import PropTypes from 'prop-types'; import { useAddresses, Button, Loader } from 'react-components'; import { c } from 'ttag'; import { CYCLE, COUPON_CODES } from 'proton-shared/lib/constants'; -import checkmarkSvg from 'design-system/assets/img/shared/checkmark-icon.svg'; -import percentageSvg from 'design-system/assets/img/shared/percentage-icon.svg'; +import shieldSvg from 'design-system/assets/img/shared/shield.svg'; +import percentageSvg from 'design-system/assets/img/shared/percentage.svg'; +import clockSvg from 'design-system/assets/img/shared/clock.svg'; +import tickSvg from 'design-system/assets/img/shared/tick.svg'; const STEPS = { CUSTOMIZATION: 0, @@ -13,8 +15,10 @@ const STEPS = { THANKS: 3 }; -const CheckmarkIcon = () => checkmark; +const TickIcon = () => checkmark; const PercentageIcon = () => percentage; +const ShieldIcon = () => percentage; +const ClockIcon = () => percentage; const NewSubscriptionModalFooter = ({ submit, step, model }) => { const [addresses, loadingAddresses] = useAddresses(); @@ -30,35 +34,47 @@ const NewSubscriptionModalFooter = ({ submit, step, model }) => { const hasAddresses = Array.isArray(addresses) && addresses.length > 0; const cancel = step === STEPS.CUSTOMIZATION ? c('Action').t`Cancel` : c('Action').t`Back`; const upsells = [ - model.cycle === CYCLE.MONTHLY && ( + step === STEPS.CUSTOMIZATION && model.cycle === CYCLE.MONTHLY && (
{c('Info').t`Save 20% by switching to annual billing`}
), - model.cycle === CYCLE.YEARLY && ( + step === STEPS.CUSTOMIZATION && model.cycle === CYCLE.YEARLY && (
- + {c('Info').t`You are saving 20% with annual billing`}
), - model.cycle === CYCLE.TWO_YEARS && ( + step === STEPS.CUSTOMIZATION && model.cycle === CYCLE.TWO_YEARS && (
- + {c('Info').t`You are saving 33% with 2-year billing`}
), - hasAddresses && model.coupon !== COUPON_CODES.BUNDLE && ( + step === STEPS.CUSTOMIZATION && hasAddresses && model.coupon !== COUPON_CODES.BUNDLE && (
{c('Info').t`Save an extra 20% by combining Mail and VPN`}
), - hasAddresses && model.coupon === COUPON_CODES.BUNDLE && ( + step === STEPS.CUSTOMIZATION && hasAddresses && model.coupon === COUPON_CODES.BUNDLE && (
- + {c('Info').t`You are saving an extra 20% with the bundle discount`}
+ ), + step === STEPS.PAYMENT && ( +
+ + {c('Info').t`30-days money back guaranteed`} +
+ ), + step === STEPS.PAYMENT && ( +
+ + {c('Info').t`Payments are protected with TLS encryption and Swiss privacy laws`} +
) ].filter(Boolean); diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index 2ee5766b3..09021a3b4 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -190,66 +190,68 @@ const SubscriptionCheckout = ({ submit = c('Action').t`Pay`, plans = [], model, )}
-
- {model.coupon ? ( + {checkResult.Amount ? ( +
+ {model.coupon ? ( +
+ + + {c('Title').t`Coupon discount`} + + + } + amount={discount} + currency={model.currency} + className="small mt0 mb0" + /> +
+ ) : null}
- - - {c('Title').t`Coupon discount`} - - - } - amount={discount} - currency={model.currency} - className="small mt0 mb0" - /> + {[CYCLE.YEARLY, CYCLE.TWO_YEARS].includes(model.cycle) ? ( + + ) : null} + + {checkResult.Proration ? ( + + ) : null} + {checkResult.Credit ? ( + + ) : null} + {checkResult.Gift ? ( + + ) : null}
- ) : null} -
- {[CYCLE.YEARLY, CYCLE.TWO_YEARS].includes(model.cycle) ? ( - - ) : null} - - {checkResult.Proration ? ( - - ) : null} - {checkResult.Credit ? ( - - ) : null} - {checkResult.Gift ? ( - - ) : null} + +
{submit}
- -
{submit}
-
+ ) : null} ); }; From d41ee00989f3b6f32f06be89f27eb6d3aa7f83a0 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 19 Dec 2019 14:14:26 +0100 Subject: [PATCH 032/242] Continue --- containers/payments/BitcoinDetails.js | 2 +- containers/payments/Cash.js | 10 ++- containers/payments/PayPalView.js | 8 ++- containers/payments/Payment.js | 6 +- .../subscription/NewSubscriptionModal.js | 10 ++- .../subscription/NewSubscriptionModal.scss | 5 ++ .../subscription/SubscriptionSection.js | 4 +- .../subscription/SubscriptionThanks.js | 18 +++-- .../subscription/SubscriptionUpgrade.js | 9 ++- .../subscription/UpsellSubscription.js | 71 +++++++++++++++++++ 10 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 containers/payments/subscription/UpsellSubscription.js diff --git a/containers/payments/BitcoinDetails.js b/containers/payments/BitcoinDetails.js index 85466d87d..66694780d 100644 --- a/containers/payments/BitcoinDetails.js +++ b/containers/payments/BitcoinDetails.js @@ -10,7 +10,7 @@ const BitcoinDetails = ({ amount, address }) => { {amount}
-
+
diff --git a/containers/payments/Cash.js b/containers/payments/Cash.js index dc2fe71f2..7daf0e64f 100644 --- a/containers/payments/Cash.js +++ b/containers/payments/Cash.js @@ -2,6 +2,7 @@ import React from 'react'; import { c } from 'ttag'; import { Alert, useConfig } from 'react-components'; import { CLIENT_TYPES } from 'proton-shared/lib/constants'; +import envelopSvg from 'design-system/assets/img/pm-images/envelop.svg'; const { VPN } = CLIENT_TYPES; @@ -10,8 +11,13 @@ const Cash = () => { const email = {CLIENT_TYPE === VPN ? 'contact@protonvpn.com' : 'contact@protonmail.com'}; return ( - {c('Info for cash payment method') - .jt`To pay via Cash, please email us at ${email} for instructions.`} +
+ {c('Info for cash payment method') + .jt`Please contact us at ${email} for instructions on how pay us with cash.`} +
+ Envelop +
+
); }; diff --git a/containers/payments/PayPalView.js b/containers/payments/PayPalView.js index b9c657d13..ce4fe82ba 100644 --- a/containers/payments/PayPalView.js +++ b/containers/payments/PayPalView.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Alert, DoNotWindowOpenAlertError, Price, Loader } from 'react-components'; import { MIN_PAYPAL_AMOUNT, MAX_PAYPAL_AMOUNT } from 'proton-shared/lib/constants'; import { doNotWindowOpen } from 'proton-shared/lib/helpers/browser'; +import paypalSvg from 'design-system/assets/img/shared/bank-icons/cc-paypal.svg'; import { c } from 'ttag'; import PayPalButton from './PayPalButton'; @@ -36,7 +37,7 @@ const PayPalView = ({ type, amount, currency, paypal, paypalCredit }) => { ); return ( - <> +
{paypal.loading ? ( <> @@ -49,6 +50,9 @@ const PayPalView = ({ type, amount, currency, paypal, paypalCredit }) => { {c('Info') .t`We will redirect you to PayPal in a new browser tab to complete this transaction. If you use any pop-up blockers, please disable them to continue.`} +
+ PayPal +
{c('Info') .jt`You must have a credit card or bank account linked with your PayPal account. If your PayPal account doesn't have that, please ${clickHere}.`} @@ -71,7 +75,7 @@ const PayPalView = ({ type, amount, currency, paypal, paypalCredit }) => { ) : null} - +
); }; diff --git a/containers/payments/Payment.js b/containers/payments/Payment.js index 23839dbb3..0dcf5a260 100644 --- a/containers/payments/Payment.js +++ b/containers/payments/Payment.js @@ -76,8 +76,8 @@ const Payment = ({ return ( <> - -
+ +
{options.map(({ text, value, disabled, icon }) => { return ( @@ -102,7 +102,7 @@ const Payment = ({ ); })}
-
+
} step={step} model={model} />} - className={classnames(['pm-modal--full subscription-modal', user.isFree && 'is-free-user'])} + className={classnames([ + 'subscription-modal', + [STEPS.CUSTOMIZATION, STEPS.PAYMENT].includes(step) && 'pm-modal--full', + user.isFree && 'is-free-user' + ])} title={TITLE[step]} loading={loading || loadingPlans || loadingVpnCountries} onSubmit={handleCheckout} diff --git a/containers/payments/subscription/NewSubscriptionModal.scss b/containers/payments/subscription/NewSubscriptionModal.scss index 5774ad9d3..4ceb40ef7 100644 --- a/containers/payments/subscription/NewSubscriptionModal.scss +++ b/containers/payments/subscription/NewSubscriptionModal.scss @@ -22,4 +22,9 @@ .subscription-modal .subscriptionTable-description, .subscription-modal .subscriptionTable-footer { min-height: auto; +} + +.subscription-modal .pm-label.payment-left { + width: 30%; + min-width: var(--label-width, 18em); } \ No newline at end of file diff --git a/containers/payments/subscription/SubscriptionSection.js b/containers/payments/subscription/SubscriptionSection.js index 80a5560de..8c722efdb 100644 --- a/containers/payments/subscription/SubscriptionSection.js +++ b/containers/payments/subscription/SubscriptionSection.js @@ -20,6 +20,7 @@ import { identity } from 'proton-shared/lib/helpers/function'; import { formatPlans, toPlanNames } from './helpers'; import SubscriptionModal from './SubscriptionModal'; +import UpsellSubscription from './UpsellSubscription'; const AddonRow = ({ label, used, max, format = identity }) => { return ( @@ -131,7 +132,7 @@ const SubscriptionSection = ({ permission }) => { {subTitle} {c('Info') .t`To manage your subscription, update your current plan or select another one from the plan's table.`} -
+
{hasPaidMail && mailPlanName !== 'visionary' ? ( {
)}
+ ); }; diff --git a/containers/payments/subscription/SubscriptionThanks.js b/containers/payments/subscription/SubscriptionThanks.js index f4af8e710..9d32b53c3 100644 --- a/containers/payments/subscription/SubscriptionThanks.js +++ b/containers/payments/subscription/SubscriptionThanks.js @@ -1,14 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { Button } from 'react-components'; +import { PrimaryButton, useConfig } from 'react-components'; +import { CLIENT_TYPES } from 'proton-shared/lib/constants'; +import mailLandscapeSvg from 'design-system/assets/img/pm-images/landscape.svg'; +import vpnLandscapeSvg from 'design-system/assets/img/pv-images/landscape.svg'; const SubscriptionThanks = ({ onClose }) => { + const { CLIENT_TYPE } = useConfig(); + return ( <> -
{c('Info').t`Thanks`}
-
- +

{c('Info').t`Your account has been successfully updated.`}

+

{c('Info') + .t`Download your favorite app today and take privacy with you everywhere you go.`}

+
+ landscape +
+
+ {c('Action').t`Close`}
); diff --git a/containers/payments/subscription/SubscriptionUpgrade.js b/containers/payments/subscription/SubscriptionUpgrade.js index 91b660739..30498a242 100644 --- a/containers/payments/subscription/SubscriptionUpgrade.js +++ b/containers/payments/subscription/SubscriptionUpgrade.js @@ -1,13 +1,20 @@ import React from 'react'; import { c } from 'ttag'; -import { Loader } from 'react-components'; +import { Loader, useConfig } from 'react-components'; +import { CLIENT_TYPES } from 'proton-shared/lib/constants'; const SubscriptionUpgrade = () => { + const { CLIENT_TYPE } = useConfig(); return ( <>

{c('Info') .t`Your account is being upgraded, this may take up to 30 seconds.`}

+

+ {CLIENT_TYPE === CLIENT_TYPES.MAIL + ? c('Info').t`Thank you for supporting ProtonMail` + : c('Info').t`Thank you for supporting ProtonVPN`} +

); }; diff --git a/containers/payments/subscription/UpsellSubscription.js b/containers/payments/subscription/UpsellSubscription.js new file mode 100644 index 000000000..440a57a15 --- /dev/null +++ b/containers/payments/subscription/UpsellSubscription.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { useAddresses, useUser, useSubscription, useModals, PrimaryButton } from 'react-components'; +import { hasMailPlus, hasVpnBasic } from 'proton-shared/lib/helpers/subscription'; +import { c } from 'ttag'; + +import NewSubscriptionModal from './NewSubscriptionModal'; + +const UpsellSubscription = () => { + const [{ hasPaidMail, hasPaidVpn }] = useUser(); + const [subscription] = useSubscription(); + const [addresses] = useAddresses(); + const hasAddresses = Array.isArray(addresses) && addresses.length > 0; + const isFreeMail = !hasPaidMail; + const isFreeVpn = !hasPaidVpn; + const { createModal } = useModals(); + const upsells = [ + isFreeMail && { + title: c('Title').t`Upgrade to ProtonMail Plus`, + description: c('Title') + .t`Upgrade to ProtonMail Plus to get more storage, more email addresses and more ways to customize your mailbox with folders, labels and filters. Upgrading to a paid plan also allows you to get early access to new products.`, + upgradeButton: ( + { + createModal(); + }} + >{c('Action').t`Upgrade`} + ) + }, + hasAddresses && + hasMailPlus(subscription) && { + title: c('Title').t`Upgrade to ProtonMail Professional`, + description: c('Title') + .t`Ugrade to ProtonMail Professional to get multi-user support. This allows you to use ProtonMail host email for your organization and provide separate logins for each user. Professional also comes with priority support.`, + upgradeButton: ( + { + createModal(); + }} + >{c('Action').t`Upgrade`} + ) + }, + isFreeVpn || + (hasVpnBasic(subscription) && { + title: c('Title').t`Upgrade to ProtonVPN Plus`, + description: c('Title') + .t`Upgrade to ProtonVPN Plus to get access to higher speed servers (up to 10 Gbps) and unlock advanced features such as Secure Core VPN, Tor over VPN, and access geo-blocked content (such as Netflix, Youtube, Amazon Prime, etc...).`, + upgradeButton: ( + { + createModal(); + }} + >{c('Action').t`Upgrade`} + ) + }) + ] + .filter(Boolean) + .map(({ title = '', description = '', upgradeButton }, index) => { + return ( +
+ {title} +
+

{description}

+ {upgradeButton} +
+
+ ); + }); + return upsells; +}; + +export default UpsellSubscription; From 53126a021fb53301c2a00da4bd6ac2f472823ea9 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 19 Dec 2019 15:37:04 +0100 Subject: [PATCH 033/242] Continue --- components/image/QRCode.js | 9 +- containers/payments/Bitcoin.js | 28 ++--- .../subscription/NewSubscriptionModal.js | 17 ++- .../NewSubscriptionModalFooter.js | 4 - .../subscription/SubscriptionCustomization.js | 21 +--- .../subscription/SubscriptionThanks.js | 42 +++++-- .../subscription/SubscriptionUpgrade.js | 10 +- .../subscription/UpsellSubscription.js | 114 ++++++++++++------ 8 files changed, 147 insertions(+), 98 deletions(-) diff --git a/components/image/QRCode.js b/components/image/QRCode.js index 8a35acb24..f358a92c4 100644 --- a/components/image/QRCode.js +++ b/components/image/QRCode.js @@ -2,11 +2,16 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import QRCodeJS from 'qrcodejs2'; -const QRCode = ({ url, ...rest }) => { +const QRCode = ({ url: text, ...rest }) => { const divRef = useRef(null); useEffect(() => { - const qrcode = new QRCodeJS(divRef.current, url); + const qrcode = new QRCodeJS(divRef.current, { + text, + width: 128, + height: 128 + }); + return () => { qrcode.clear(); }; diff --git a/containers/payments/Bitcoin.js b/containers/payments/Bitcoin.js index 485056ace..3f7eae990 100644 --- a/containers/payments/Bitcoin.js +++ b/containers/payments/Bitcoin.js @@ -55,9 +55,22 @@ const Bitcoin = ({ amount, currency, type }) => { return ( <>
+ {type === 'invoice' ? ( + {c('Info') + .t`Bitcoin transactions can take some time to be confirmed (up to 24 hours). Once confirmed, we will add credits to your account. After transaction confirmation, you can pay your invoice with the credits.`} + ) : ( + {c('Info') + .t`After making your Bitcoin payment, please follow the instructions below to upgrade.`} + )}
{
- {type === 'invoice' ? ( - {c('Info') - .t`Bitcoin transactions can take some time to be confirmed (up to 24 hours). Once confirmed, we will add credits to your account. After transaction confirmation, you can pay your invoice with the credits.`} - ) : ( - {c('Info') - .t`After making your Bitcoin payment, please follow the instructions below to upgrade.`} - )} ); }; diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 95245b30c..2b233d65d 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -61,8 +61,8 @@ const NewSubscriptionModal = ({ const TITLE = { [STEPS.CUSTOMIZATION]: c('Title').t`Plan customization`, [STEPS.PAYMENT]: c('Title').t`Checkout`, - [STEPS.UPGRADE]: c('Title').t`Processing...`, - [STEPS.THANKS]: c('Title').t`CONGRATULATIONS!` + [STEPS.UPGRADE]:
{c('Title').t`Processing...`}
, + [STEPS.THANKS]:
{c('Title').t`Thank you!`}
}; const api = useApi(); @@ -152,8 +152,9 @@ const NewSubscriptionModal = ({ if ([PAYMENT_METHOD_TYPES.CASH, PAYMENT_METHOD_TYPES.BITCOIN].includes(method)) { return ( - {c('Action') - .t`Done`} + setStep(STEPS.THANKS)}>{c( + 'Action' + ).t`Done`} ); } @@ -235,7 +236,11 @@ const NewSubscriptionModal = ({ return ( } step={step} model={model} />} + footer={ + [STEPS.UPGRADE, STEPS.THANKS].includes(step) ? null : ( + } step={step} model={model} /> + ) + } className={classnames([ 'subscription-modal', [STEPS.CUSTOMIZATION, STEPS.PAYMENT].includes(step) && 'pm-modal--full', @@ -317,7 +322,7 @@ const NewSubscriptionModal = ({
)} {step === STEPS.UPGRADE && } - {step === STEPS.THANKS && } + {step === STEPS.THANKS && } ); }; diff --git a/containers/payments/subscription/NewSubscriptionModalFooter.js b/containers/payments/subscription/NewSubscriptionModalFooter.js index 050fd3a5e..9f1a0f36a 100644 --- a/containers/payments/subscription/NewSubscriptionModalFooter.js +++ b/containers/payments/subscription/NewSubscriptionModalFooter.js @@ -23,10 +23,6 @@ const ClockIcon = () => percentage { const [addresses, loadingAddresses] = useAddresses(); - if ([STEPS.UPGRADE, STEPS.THANKS].includes(step)) { - return null; - } - if (loadingAddresses) { return ; } diff --git a/containers/payments/subscription/SubscriptionCustomization.js b/containers/payments/subscription/SubscriptionCustomization.js index 369bd72c8..a515b01fc 100644 --- a/containers/payments/subscription/SubscriptionCustomization.js +++ b/containers/payments/subscription/SubscriptionCustomization.js @@ -6,6 +6,7 @@ import { PLANS, CYCLE, ADDON_NAMES, CLIENT_TYPES, PLAN_SERVICES, FREE, PLAN_TYPE import { toMap } from 'proton-shared/lib/helpers/object'; import humanSize from 'proton-shared/lib/helpers/humanSize'; import { hasBit } from 'proton-shared/lib/helpers/bitset'; +import { removeService } from 'proton-shared/lib/helpers/subscription'; import SubscriptionPlan from './SubscriptionPlan'; import SubscriptionAddonRow from './SubscriptionAddonRow'; @@ -25,26 +26,6 @@ const TITLE = { [PLANS.VPNPLUS]: 'ProtonVPN Plus' }; -/** - * Remove all plans concerned by a service - * @param {Object} planIDs - * @param {Array} plans - * @param {Integer} service - * @returns {Object} new planIDs - */ -const removeService = (planIDs = {}, plans = [], service = PLAN_SERVICES.MAIL) => { - const plansMap = toMap(plans); - return Object.entries(planIDs).reduce((acc, [planID = '', quantity = 0]) => { - const { Services } = plansMap[planID]; - - if (!hasBit(Services, service)) { - acc[planID] = quantity; - } - - return acc; - }, {}); -}; - const Description = ({ planName, setModel, model, plans }) => { const plansMap = toMap(plans, 'Name'); const plusPlan = plansMap[PLANS.PLUS]; diff --git a/containers/payments/subscription/SubscriptionThanks.js b/containers/payments/subscription/SubscriptionThanks.js index 9d32b53c3..be5896954 100644 --- a/containers/payments/subscription/SubscriptionThanks.js +++ b/containers/payments/subscription/SubscriptionThanks.js @@ -1,23 +1,50 @@ import React from 'react'; import PropTypes from 'prop-types'; import { c } from 'ttag'; -import { PrimaryButton, useConfig } from 'react-components'; -import { CLIENT_TYPES } from 'proton-shared/lib/constants'; +import { PrimaryButton, useConfig, Href } from 'react-components'; +import { CLIENT_TYPES, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; import mailLandscapeSvg from 'design-system/assets/img/pm-images/landscape.svg'; import vpnLandscapeSvg from 'design-system/assets/img/pv-images/landscape.svg'; +import appStoreSvg from 'design-system/assets/img/shared/app-store.svg'; +import playStoreSvg from 'design-system/assets/img/shared/play-store.svg'; -const SubscriptionThanks = ({ onClose }) => { +const SubscriptionThanks = ({ method = '', onClose }) => { const { CLIENT_TYPE } = useConfig(); return ( <> -

{c('Info').t`Your account has been successfully updated.`}

-

{c('Info') +

+ {[PAYMENT_METHOD_TYPES.CASH, PAYMENT_METHOD_TYPES.BITCOIN].includes(method) + ? c('Info').t`Your account will be updated once the payment is cleared.` + : c('Info').t`Your account has been successfully updated.`} +

+

{c('Info') .t`Download your favorite app today and take privacy with you everywhere you go.`}

-
+
landscape
-
+
+ + Play Store + + + App Store + +
+
{c('Action').t`Close`}
@@ -25,6 +52,7 @@ const SubscriptionThanks = ({ onClose }) => { }; SubscriptionThanks.propTypes = { + method: PropTypes.string, onClose: PropTypes.func.isRequired }; diff --git a/containers/payments/subscription/SubscriptionUpgrade.js b/containers/payments/subscription/SubscriptionUpgrade.js index 30498a242..96c2f5b60 100644 --- a/containers/payments/subscription/SubscriptionUpgrade.js +++ b/containers/payments/subscription/SubscriptionUpgrade.js @@ -1,20 +1,14 @@ import React from 'react'; import { c } from 'ttag'; -import { Loader, useConfig } from 'react-components'; -import { CLIENT_TYPES } from 'proton-shared/lib/constants'; +import { Loader } from 'react-components'; const SubscriptionUpgrade = () => { - const { CLIENT_TYPE } = useConfig(); return ( <>

{c('Info') .t`Your account is being upgraded, this may take up to 30 seconds.`}

-

- {CLIENT_TYPE === CLIENT_TYPES.MAIL - ? c('Info').t`Thank you for supporting ProtonMail` - : c('Info').t`Thank you for supporting ProtonVPN`} -

+

{c('Info').t`Thank you for supporting our mission`}

); }; diff --git a/containers/payments/subscription/UpsellSubscription.js b/containers/payments/subscription/UpsellSubscription.js index 440a57a15..af72a05ed 100644 --- a/containers/payments/subscription/UpsellSubscription.js +++ b/containers/payments/subscription/UpsellSubscription.js @@ -1,71 +1,111 @@ import React from 'react'; -import { useAddresses, useUser, useSubscription, useModals, PrimaryButton } from 'react-components'; -import { hasMailPlus, hasVpnBasic } from 'proton-shared/lib/helpers/subscription'; +import { useUser, useSubscription, useModals, usePlans, PrimaryButton, Loader } from 'react-components'; +import { hasMailPlus, hasVpnBasic, removeService } from 'proton-shared/lib/helpers/subscription'; +import { DEFAULT_CURRENCY, DEFAULT_CYCLE, PLAN_SERVICES, PLANS } from 'proton-shared/lib/constants'; +import { toMap } from 'proton-shared/lib/helpers/object'; import { c } from 'ttag'; import NewSubscriptionModal from './NewSubscriptionModal'; const UpsellSubscription = () => { - const [{ hasPaidMail, hasPaidVpn }] = useUser(); - const [subscription] = useSubscription(); - const [addresses] = useAddresses(); - const hasAddresses = Array.isArray(addresses) && addresses.length > 0; + const [{ hasPaidMail, hasPaidVpn }, loadingUser] = useUser(); + const [subscription, loadingSubscription] = useSubscription(); + const [plans, loadingPlans] = usePlans(); + const { Currency = DEFAULT_CURRENCY, Cycle = DEFAULT_CYCLE, Plans = [] } = subscription || {}; const isFreeMail = !hasPaidMail; const isFreeVpn = !hasPaidVpn; const { createModal } = useModals(); - const upsells = [ + + if (loadingUser || loadingSubscription || loadingPlans) { + return ; + } + + const plansMap = toMap(plans, 'Name'); + const planIDs = Plans.reduce((acc, { ID, Quantity }) => { + acc[ID] = Quantity; + return acc; + }, {}); + + return [ isFreeMail && { title: c('Title').t`Upgrade to ProtonMail Plus`, description: c('Title') .t`Upgrade to ProtonMail Plus to get more storage, more email addresses and more ways to customize your mailbox with folders, labels and filters. Upgrading to a paid plan also allows you to get early access to new products.`, upgradeButton: ( { + createModal( + + ); + }} + >{c('Action').t`Upgrade`} + ) + }, + hasMailPlus(subscription) && { + title: c('Title').t`Upgrade to ProtonMail Professional`, + description: c('Title') + .t`Ugrade to ProtonMail Professional to get multi-user support. This allows you to use ProtonMail host email for your organization and provide separate logins for each user. Professional also comes with priority support.`, + upgradeButton: ( + { - createModal(); + createModal( + + ); }} >{c('Action').t`Upgrade`} ) }, - hasAddresses && - hasMailPlus(subscription) && { - title: c('Title').t`Upgrade to ProtonMail Professional`, - description: c('Title') - .t`Ugrade to ProtonMail Professional to get multi-user support. This allows you to use ProtonMail host email for your organization and provide separate logins for each user. Professional also comes with priority support.`, - upgradeButton: ( - { - createModal(); - }} - >{c('Action').t`Upgrade`} - ) - }, - isFreeVpn || - (hasVpnBasic(subscription) && { - title: c('Title').t`Upgrade to ProtonVPN Plus`, - description: c('Title') - .t`Upgrade to ProtonVPN Plus to get access to higher speed servers (up to 10 Gbps) and unlock advanced features such as Secure Core VPN, Tor over VPN, and access geo-blocked content (such as Netflix, Youtube, Amazon Prime, etc...).`, - upgradeButton: ( - { - createModal(); - }} - >{c('Action').t`Upgrade`} - ) - }) + (isFreeVpn || hasVpnBasic(subscription)) && { + title: c('Title').t`Upgrade to ProtonVPN Plus`, + description: c('Title') + .t`Upgrade to ProtonVPN Plus to get access to higher speed servers (up to 10 Gbps) and unlock advanced features such as Secure Core VPN, Tor over VPN, and access geo-blocked content (such as Netflix, Youtube, Amazon Prime, etc...).`, + upgradeButton: ( + { + createModal( + + ); + }} + >{c('Action').t`Upgrade`} + ) + } ] .filter(Boolean) .map(({ title = '', description = '', upgradeButton }, index) => { return (
{title} -
-

{description}

+
+

{description}

{upgradeButton}
); }); - return upsells; }; export default UpsellSubscription; From 253f891988d6533ce25b37cdc548f947bde2e20a Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 19 Dec 2019 16:30:44 +0100 Subject: [PATCH 034/242] Continue --- containers/payments/BillingSection.js | 13 ++-- containers/payments/PlansSection.js | 7 +- .../subscription/BlackFridayNavbarLink.js | 33 ++-------- .../subscription/SubscriptionSection.js | 40 ++++++------ .../subscription/SubscriptionUpgrade.js | 2 +- .../subscription/UnsubscribeButton.js | 65 +++++++++++++++++++ 6 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 containers/payments/subscription/UnsubscribeButton.js diff --git a/containers/payments/BillingSection.js b/containers/payments/BillingSection.js index c0ca58c1b..3211a4c32 100644 --- a/containers/payments/BillingSection.js +++ b/containers/payments/BillingSection.js @@ -19,15 +19,15 @@ import { useModals, usePlans } from 'react-components'; -import { getMonthlyBaseAmount, hasVisionary } from 'proton-shared/lib/helpers/subscription'; +import { getMonthlyBaseAmount, hasVisionary, getPlanIDs } from 'proton-shared/lib/helpers/subscription'; +import humanSize from 'proton-shared/lib/helpers/humanSize'; -import { formatPlans, toPlanNames } from './subscription/helpers'; +import { formatPlans } from './subscription/helpers'; import DiscountBadge from './DiscountBadge'; import GiftCodeModal from './GiftCodeModal'; import CreditsModal from './CreditsModal'; import PlanPrice from './subscription/PlanPrice'; -import SubscriptionModal from './subscription/SubscriptionModal'; -import humanSize from 'proton-shared/lib/helpers/humanSize'; +import NewSubscriptionModal from './subscription/NewSubscriptionModal'; const { MONTHLY, YEARLY, TWO_YEARS } = CYCLE; @@ -48,9 +48,8 @@ const BillingSection = ({ permission }) => { const handleOpenCreditsModal = () => createModal(); const handleOpenSubscriptionModal = () => createModal( - { const { createNotification } = useNotifications(); const { createModal } = useModals(); const [user] = useUser(); - const { isFree } = user; const [subscription = {}, loadingSubscription] = useSubscription(); const [organization = {}, loadingOrganization] = useOrganization(); const [plans = [], loadingPlans] = usePlans(); @@ -54,7 +53,7 @@ const PlansSection = () => { }; const handleOpenModal = async () => { - if (isFree) { + if (user.isFree) { return createNotification({ type: 'error', text: c('Info').t`You already have a free account` }); } await new Promise((resolve, reject) => { @@ -106,6 +105,10 @@ const PlansSection = () => { setCycle(subscription.Cycle || Cycle); }, [loadingSubscription, loadingPlans]); + if (user.isPaid) { + return null; + } + if (subscription.isManagedByMozilla) { return ( <> diff --git a/containers/payments/subscription/BlackFridayNavbarLink.js b/containers/payments/subscription/BlackFridayNavbarLink.js index f64e9ed66..dcb50de6a 100644 --- a/containers/payments/subscription/BlackFridayNavbarLink.js +++ b/containers/payments/subscription/BlackFridayNavbarLink.js @@ -1,22 +1,12 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { - useUser, - useApi, - useLoading, - useBlackFriday, - TopNavbarLink, - SubscriptionModal, - usePlans, - useSubscription, - useModals -} from 'react-components'; +import { useUser, useApi, useLoading, useBlackFriday, TopNavbarLink, usePlans, useModals } from 'react-components'; import { checkLastCancelledSubscription } from './helpers'; +import NewSubscriptionModal from './NewSubscriptionModal'; const BlackFridayNavbarLink = ({ to, location, getModal, ...rest }) => { const [plans, loadingPlans] = usePlans(); - const [subscription, loadingSubscription] = useSubscription(); const { createModal } = useModals(); const api = useApi(); const [loading, withLoading] = useLoading(); @@ -27,22 +17,7 @@ const BlackFridayNavbarLink = ({ to, location, getModal, ...rest }) => { const text = 'Black Friday'; const onSelect = ({ planIDs = [], cycle, currency, couponCode }) => { - const plansMap = planIDs.reduce((acc, planID) => { - const { Name } = plans.find(({ ID }) => ID === planID); - acc[Name] = 1; - return acc; - }, Object.create(null)); - - createModal( - - ); + createModal(); }; const handleClick = () => { @@ -57,7 +32,7 @@ const BlackFridayNavbarLink = ({ to, location, getModal, ...rest }) => { } }, [isBlackFriday, user.isFree]); - if (!isBlackFriday || !isEligible || user.isPaid || loading || loadingPlans || loadingSubscription) { + if (!isBlackFriday || !isEligible || user.isPaid || loading || loadingPlans) { return null; } diff --git a/containers/payments/subscription/SubscriptionSection.js b/containers/payments/subscription/SubscriptionSection.js index 8c722efdb..4ef941770 100644 --- a/containers/payments/subscription/SubscriptionSection.js +++ b/containers/payments/subscription/SubscriptionSection.js @@ -4,7 +4,7 @@ import { c } from 'ttag'; import { Alert, SubTitle, - SmallButton, + LinkButton, Loader, MozillaInfoPanel, Progress, @@ -17,10 +17,12 @@ import { import { PLAN_NAMES } from 'proton-shared/lib/constants'; import humanSize from 'proton-shared/lib/helpers/humanSize'; import { identity } from 'proton-shared/lib/helpers/function'; +import { getPlanIDs } from 'proton-shared/lib/helpers/subscription'; -import { formatPlans, toPlanNames } from './helpers'; -import SubscriptionModal from './SubscriptionModal'; +import { formatPlans } from './helpers'; import UpsellSubscription from './UpsellSubscription'; +import NewSubscriptionModal from './NewSubscriptionModal'; +import UnsubscribeButton from './UnsubscribeButton'; const AddonRow = ({ label, used, max, format = identity }) => { return ( @@ -46,7 +48,7 @@ AddonRow.propTypes = { }; const SubscriptionSection = ({ permission }) => { - const [{ hasPaidMail, hasPaidVpn }] = useUser(); + const [{ hasPaidMail, hasPaidVpn, isPaid }] = useUser(); const [addresses = [], loadingAddresses] = useAddresses(); const [subscription, loadingSubscription] = useSubscription(); const { createModal } = useModals(); @@ -101,9 +103,8 @@ const SubscriptionSection = ({ permission }) => { const handleModal = () => { createModal( - { {c('Info') .t`To manage your subscription, update your current plan or select another one from the plan's table.`}
-
- {hasPaidMail && mailPlanName !== 'visionary' ? ( - - {c('Action').t`Update`} - - ) : null} +
ProtonMail plan
@@ -154,7 +146,9 @@ const SubscriptionSection = ({ permission }) => { : c('Info').t`Not activated`}
-
+
+ {c('Action').t`Manage subscription`} +
{mailAddons.map((props, index) => ( @@ -167,13 +161,21 @@ const SubscriptionSection = ({ permission }) => {
{hasPaidVpn ? PLAN_NAMES[vpnPlanName] : c('Plan').t`Free`}
-
+
+ {c('Action').t`Manage subscription`} +
{vpnAddons.map((props, index) => ( ))}
)} + {isPaid ? ( +
+ {c('Action') + .t`Cancel subscription`} +
+ ) : null}
diff --git a/containers/payments/subscription/SubscriptionUpgrade.js b/containers/payments/subscription/SubscriptionUpgrade.js index 96c2f5b60..2ec528fb3 100644 --- a/containers/payments/subscription/SubscriptionUpgrade.js +++ b/containers/payments/subscription/SubscriptionUpgrade.js @@ -8,7 +8,7 @@ const SubscriptionUpgrade = () => {

{c('Info') .t`Your account is being upgraded, this may take up to 30 seconds.`}

-

{c('Info').t`Thank you for supporting our mission`}

+

{c('Info').t`Thank you for supporting our mission`}

); }; diff --git a/containers/payments/subscription/UnsubscribeButton.js b/containers/payments/subscription/UnsubscribeButton.js new file mode 100644 index 000000000..be938c421 --- /dev/null +++ b/containers/payments/subscription/UnsubscribeButton.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + DowngradeModal, + LossLoyaltyModal, + useApi, + useUser, + useNotifications, + useLoading, + useModals, + useEventManager, + useOrganization +} from 'react-components'; +import { c } from 'ttag'; +import { deleteSubscription } from 'proton-shared/lib/api/payments'; + +import { isLoyal } from 'proton-shared/lib/helpers/organization'; + +const UnsubscribeButton = ({ className, children }) => { + const [user] = useUser(); + const [organization] = useOrganization(); + const { createNotification } = useNotifications(); + const { createModal } = useModals(); + const api = useApi(); + const { call } = useEventManager(); + const [loading, withLoading] = useLoading(); + + const handleUnsubscribe = async () => { + await api(deleteSubscription()); + await call(); + createNotification({ text: c('Success').t`You have successfully unsubscribed` }); + }; + + const handleClick = async () => { + if (user.isFree) { + return createNotification({ type: 'error', text: c('Info').t`You already have a free account` }); + } + + await new Promise((resolve, reject) => { + createModal(); + }); + + if (isLoyal(organization)) { + await new Promise((resolve, reject) => { + createModal(); + }); + } + + return handleUnsubscribe(); + }; + + return ( + + ); +}; + +UnsubscribeButton.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +export default UnsubscribeButton; From 586d00973ef700ccfe340230bc412f30955c6c94 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 20 Dec 2019 09:04:07 +0100 Subject: [PATCH 035/242] Continue --- hooks/usePermissions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/usePermissions.js b/hooks/usePermissions.js index 7ff79ca2a..9ffc2edf0 100644 --- a/hooks/usePermissions.js +++ b/hooks/usePermissions.js @@ -12,12 +12,12 @@ const ROLES = { const usePermissions = () => { const permissions = []; - const [{ Role, isPaid, hasPaidMail, hasPaidVpn }] = useUser(); + const [{ Role, isPaid, hasPaidMail, hasPaidVpn, canPay }] = useUser(); const [{ MaxMembers = 0 } = {}] = useOrganization(); permissions.push(ROLES[Role]); - if ([FREE_ROLE, ADMIN_ROLE].includes(Role)) { + if (canPay) { permissions.push(UPGRADER); } From ac57840bde907e54ab108b4d58224ce0a165f3c0 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 20 Dec 2019 16:32:40 +0100 Subject: [PATCH 036/242] Continue --- containers/payments/PlansSection.js | 4 ---- .../payments/subscription/NewSubscriptionModal.js | 11 +++++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/containers/payments/PlansSection.js b/containers/payments/PlansSection.js index ae4595c29..307f7ea4d 100644 --- a/containers/payments/PlansSection.js +++ b/containers/payments/PlansSection.js @@ -105,10 +105,6 @@ const PlansSection = () => { setCycle(subscription.Cycle || Cycle); }, [loadingSubscription, loadingPlans]); - if (user.isPaid) { - return null; - } - if (subscription.isManagedByMozilla) { return ( <> diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 2b233d65d..3bfdb1fbe 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -74,7 +74,8 @@ const NewSubscriptionModal = ({ const [loading, withLoading] = useLoading(); const [loadingCheck, withLoadingCheck] = useLoading(); const [checkResult, setCheckResult] = useState({}); - const { Code: couponCode } = checkResult.Coupon || {}; // Coupon can be null + const { Code: couponCode, Credit = 0 } = checkResult.Coupon || {}; // Coupon can be null + const creditsRemaining = (user.Credit + Credit) / 100; const [model, setModel] = useState({ cycle, currency, @@ -301,7 +302,13 @@ const NewSubscriptionModal = ({ /> ) : ( - {c('Info').t`No payment is required at this time.`} + <> + {c('Info').t`No payment is required at this time.`} + {checkResult.Credit && creditsRemaining ? ( + {c('Info') + .t`Please note that upon clicking the Confirm button, your account will have ${creditsRemaining} credits remaining.`} + ) : null} + )}
From af99d5dad822b1e334875990bba2ebe669f7ee9f Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 6 Jan 2020 09:34:24 +0100 Subject: [PATCH 037/242] Update style for black mode --- containers/payments/Bitcoin.js | 2 +- containers/payments/Cash.js | 2 +- containers/payments/PayPalView.js | 2 +- containers/payments/subscription/SubscriptionCheckout.js | 4 ++-- containers/payments/subscription/SubscriptionTable.scss | 2 +- containers/payments/subscription/UpsellSubscription.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/containers/payments/Bitcoin.js b/containers/payments/Bitcoin.js index 3f7eae990..92aaa0fa0 100644 --- a/containers/payments/Bitcoin.js +++ b/containers/payments/Bitcoin.js @@ -54,7 +54,7 @@ const Bitcoin = ({ amount, currency, type }) => { return ( <> -
+
{type === 'invoice' ? ( {c('Info') .t`Bitcoin transactions can take some time to be confirmed (up to 24 hours). Once confirmed, we will add credits to your account. After transaction confirmation, you can pay your invoice with the credits.`} diff --git a/containers/payments/Cash.js b/containers/payments/Cash.js index 7daf0e64f..483cffe2d 100644 --- a/containers/payments/Cash.js +++ b/containers/payments/Cash.js @@ -11,7 +11,7 @@ const Cash = () => { const email = {CLIENT_TYPE === VPN ? 'contact@protonvpn.com' : 'contact@protonmail.com'}; return ( -
+
{c('Info for cash payment method') .jt`Please contact us at ${email} for instructions on how pay us with cash.`}
diff --git a/containers/payments/PayPalView.js b/containers/payments/PayPalView.js index ce4fe82ba..410779807 100644 --- a/containers/payments/PayPalView.js +++ b/containers/payments/PayPalView.js @@ -37,7 +37,7 @@ const PayPalView = ({ type, amount, currency, paypal, paypalCredit }) => { ); return ( -
+
{paypal.loading ? ( <> diff --git a/containers/payments/subscription/SubscriptionCheckout.js b/containers/payments/subscription/SubscriptionCheckout.js index 09021a3b4..ab2ad36f7 100644 --- a/containers/payments/subscription/SubscriptionCheckout.js +++ b/containers/payments/subscription/SubscriptionCheckout.js @@ -140,7 +140,7 @@ const SubscriptionCheckout = ({ submit = c('Action').t`Pay`, plans = [], model,
{c('Title') .t`Plan summary`}
-
+
{hasMailPlan ? ( printSummary(PLAN_SERVICES.MAIL) @@ -191,7 +191,7 @@ const SubscriptionCheckout = ({ submit = c('Action').t`Pay`, plans = [], model,
{checkResult.Amount ? ( -
+
{model.coupon ? (
diff --git a/containers/payments/subscription/SubscriptionTable.scss b/containers/payments/subscription/SubscriptionTable.scss index c5df9d146..6748feaf4 100644 --- a/containers/payments/subscription/SubscriptionTable.scss +++ b/containers/payments/subscription/SubscriptionTable.scss @@ -2,7 +2,7 @@ border-left: 2px solid $pm-primary; border-right: 2px solid $pm-primary; border-bottom: 2px solid $pm-primary; - background-color: $pm-global-light; + background-color: var(--bgcolor-input, $pm-global-light); } .subscriptionTable-currentPlan-container { diff --git a/containers/payments/subscription/UpsellSubscription.js b/containers/payments/subscription/UpsellSubscription.js index af72a05ed..a1dd788f9 100644 --- a/containers/payments/subscription/UpsellSubscription.js +++ b/containers/payments/subscription/UpsellSubscription.js @@ -97,7 +97,7 @@ const UpsellSubscription = () => { .filter(Boolean) .map(({ title = '', description = '', upgradeButton }, index) => { return ( -
+
{title}

{description}

From b342970faf2c4ce9817151e61fe5e81f66780709 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 6 Jan 2020 10:07:27 +0100 Subject: [PATCH 038/242] Review --- containers/payments/BillingSection.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/containers/payments/BillingSection.js b/containers/payments/BillingSection.js index 3211a4c32..30c7797b4 100644 --- a/containers/payments/BillingSection.js +++ b/containers/payments/BillingSection.js @@ -28,6 +28,7 @@ import GiftCodeModal from './GiftCodeModal'; import CreditsModal from './CreditsModal'; import PlanPrice from './subscription/PlanPrice'; import NewSubscriptionModal from './subscription/NewSubscriptionModal'; +import CycleDiscountBadge from './CycleDiscountBadge'; const { MONTHLY, YEARLY, TWO_YEARS } = CYCLE; @@ -284,8 +285,14 @@ const BillingSection = ({ permission }) => {
{c('Label').t`Discount`}
- {CouponCode ? {CouponCode} : null} - + {CouponCode ? ( + <> + {CouponCode} + + + ) : ( + + )}
From 345a2522b5ca7a34919ff0f5bb3ff42aa0da141a Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 6 Jan 2020 13:50:32 +0100 Subject: [PATCH 039/242] Review --- .../payments/subscription/GiftCodeForm.js | 2 +- .../subscription/NewSubscriptionModal.js | 20 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/containers/payments/subscription/GiftCodeForm.js b/containers/payments/subscription/GiftCodeForm.js index 1ddd98362..32dcae251 100644 --- a/containers/payments/subscription/GiftCodeForm.js +++ b/containers/payments/subscription/GiftCodeForm.js @@ -6,7 +6,7 @@ import { isValid } from 'proton-shared/lib/helpers/giftCode'; const GiftCodeForm = ({ code, loading, disabled, onChange, onSubmit }) => { return ( -
+
onChange(target.value)} />
diff --git a/containers/payments/subscription/NewSubscriptionModal.js b/containers/payments/subscription/NewSubscriptionModal.js index 3bfdb1fbe..95f48b99b 100644 --- a/containers/payments/subscription/NewSubscriptionModal.js +++ b/containers/payments/subscription/NewSubscriptionModal.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { c } from 'ttag'; import { classnames, + LossLoyaltyModal, Alert, PrimaryButton, FormModal, @@ -14,20 +15,22 @@ import { useEventManager, usePayment, useUser, - useNotifications + useNotifications, + useOrganization, + useModals } from 'react-components'; import { DEFAULT_CURRENCY, DEFAULT_CYCLE, CYCLE, CURRENCIES, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; import { checkSubscription, subscribe, deleteSubscription } from 'proton-shared/lib/api/payments'; +import { isLoyal } from 'proton-shared/lib/helpers/organization'; import SubscriptionCustomization from './SubscriptionCustomization'; import SubscriptionUpgrade from './SubscriptionUpgrade'; import SubscriptionThanks from './SubscriptionThanks'; import SubscriptionCheckout from './SubscriptionCheckout'; import NewSubscriptionModalFooter from './NewSubscriptionModalFooter'; - -import './NewSubscriptionModal.scss'; -import PayPalButton from '../PayPalButton'; import PaymentGiftCode from '../PaymentGiftCode'; +import PayPalButton from '../PayPalButton'; +import './NewSubscriptionModal.scss'; const STEPS = { CUSTOMIZATION: 0, @@ -68,9 +71,11 @@ const NewSubscriptionModal = ({ const api = useApi(); const [user] = useUser(); const { call } = useEventManager(); + const { createModal } = useModals(); const { createNotification } = useNotifications(); const [vpnCountries, loadingVpnCountries] = useVPNCountries(); const [plans, loadingPlans] = usePlans(); + const [organization, loadingOrganization] = useOrganization(); const [loading, withLoading] = useLoading(); const [loadingCheck, withLoadingCheck] = useLoading(); const [checkResult, setCheckResult] = useState({}); @@ -96,6 +101,11 @@ const NewSubscriptionModal = ({ }; const handleUnsubscribe = async () => { + if (isLoyal(organization)) { + await new Promise((resolve, reject) => { + createModal(); + }); + } await api(deleteSubscription()); await call(); onClose(); @@ -248,7 +258,7 @@ const NewSubscriptionModal = ({ user.isFree && 'is-free-user' ])} title={TITLE[step]} - loading={loading || loadingPlans || loadingVpnCountries} + loading={loading || loadingPlans || loadingVpnCountries || loadingOrganization} onSubmit={handleCheckout} onClose={handleClose} {...rest} From 337203f2016befd5a92e83eb85caad1b1c1028d1 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 6 Jan 2020 15:03:05 +0100 Subject: [PATCH 040/242] Review --- containers/payments/Payment.js | 2 +- containers/payments/subscription/NewSubscriptionModal.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/containers/payments/Payment.js b/containers/payments/Payment.js index 0dcf5a260..66a49f301 100644 --- a/containers/payments/Payment.js +++ b/containers/payments/Payment.js @@ -102,7 +102,7 @@ const Payment = ({ ); })}
-
+
Date: Tue, 7 Jan 2020 16:02:27 +0100 Subject: [PATCH 041/242] Fix - UI/responsive fixes --- components/modal/Footer.js | 2 +- components/price/Price.js | 4 +- .../subscription/MailSubscriptionTable.js | 2 +- .../subscription/NewSubscriptionModal.js | 8 +++- .../NewSubscriptionModalFooter.js | 44 +++++++++++-------- .../subscription/SubscriptionCheckout.js | 4 +- .../subscription/SubscriptionTable.js | 2 +- .../subscription/VpnSubscriptionTable.js | 2 +- 8 files changed, 39 insertions(+), 29 deletions(-) diff --git a/components/modal/Footer.js b/components/modal/Footer.js index 16e53f22a..78eed985e 100644 --- a/components/modal/Footer.js +++ b/components/modal/Footer.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { classnames } from '../../helpers/component'; -const Footer = ({ children, className = 'flex flex-spacebetween flex-items-center', ...rest }) => { +const Footer = ({ children, className = 'flex flex-spacebetween flex-items-center flex-nowrap', ...rest }) => { return (