From 9cbf7a9f9739da8303703f6c10acf4adba613fe6 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Thu, 8 Oct 2020 11:52:21 -0400 Subject: [PATCH] [MM-27142] In-Web-App purchase of Mattermost Cloud (#6583) (#6690) * [MM-26468][MM-26483] Progress Bar Common Component/Sidebar Next Steps Component (#5865) * [MM-26483] Progress bar common component and PoC in the sidebar * Styling for the sidebar element and some tests * Added translations * Snapshot update * Style changes * Use same border design as channels * [MM-26465] Background and general layout for cloud onboarding (#5823) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * Lint fix * Update copy * PR feedback Co-authored-by: Mattermod * [MM-26480] Card/Accordion Common Component (#5861) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * Merge'd * PR feedback * Fix box-shadow on collapsed state * [MM-26470] Base Next Step Wizard Controller and Styling (#5893) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * PR feedback * blank * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * Fixed box shadow transition on card * Removed duplicate logic * PR feedback * PR feedback * Fixed hover state on completed cards * re-add margin on complete state * [MM-26466] Close Next Steps Modal and functionality (#5995) * Hooked up the sidebar next steps bar and some fixes * Integration of state into app for next steps view, close next steps view modal preliminary * Styling and help arrow for modal * Missed a translation * PR feedback * Center the next steps modal * PR feedback * Translation fix * [MM-27164] Picture Selector Common Component (#5973) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * PR feedback * blank * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * Fixed box shadow transition on card * Removed duplicate logic * WIP * Functional component that works * Styling and a couple tweaks * A few tests * Snapshots * Type and i18n fixes * PR feedback and test fixes * Added button hover states * PR feedback * Blur select button on select image * Blur on click, not on select image * Update components/picture_selector.tsx Co-authored-by: Nev Angelova Co-authored-by: Nev Angelova * [MM-26482] Textbox Common Component for Cloud Onboarding (#5904) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * WIP * [MM-26472] Textbox Common Component for Cloud Onboarding * Specific styling for the Cloud Onboarding components * Added info component and some other styling * Fixed the error styling * Fixed most of the shifting in the textbox * Lint fix * PR feedback * blank * PR feedback * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * PR feedback * Use box shadow instead of border for changing text input * Improved CSS from Asaad * Removed inner border when focused/error state * Removed unnecessary commented code * Merge'd * Switch to proper BEM * [MM-26469] Complete Profile Step (#6077) * [MM-26469] Complete Profile Step * Lint fix * [MM-26473] Tips and Next Steps screen (#6020) * Screen transitions for loading screen and final screen * Transition screen * Fixed APNG issue * Style for desktop and mobile * Fixed styling * More fixes * Functionality and test fixes * Dev PR feedback * UX PR feedback * UX feedback * Merge'd Co-authored-by: Mattermod * [MM-26471] Team Profile Setup step (#6083) * [MM-26471] Team Profile Setup step * Translation fix * PR feedback * Fixed an issue with an older version of the styles * Removed commented code * WIP * WIP * Final screens WIP * WIP * Lot's of CSS fixes, some SVG's added, general cleanup * Adding translations all over * Have the user_limit_modal component open the PurchaseModal when upgrade button is clicked * merge master (banners), hook up announcement banner 'Upgrade Mattermost Cloud' button * Fix issue with optional subtitle prop in IconMessage * Fix linter * Fix i18n * Fix a bunch of types * Fix country sort definitions * fix most type issues * Fix flow * Fix lint * Fix isDevMode * Fix lint * Attempt at fixing error with payment processing * fix weird issue with padding * remove main menu item * Few changes for MR review * Theme changes * Appease the pipeline gods * some fixes * Fixing card input typescript issue * First batch of CSS fixes * Fix CSS on progress and success screens * Update redux, fix tsc * Fix pipeline * Fix placeholder text colour * fix credit card entry error state * Some changes * One more change * Fixed one issue * Some more fixes * Dropdown styling * Integrate state/province selector with Devin's common component * Fix pipeline * fix i18n * Add types/cloud/sku, remove the customer type in favour of mattermost-redux versiong * Delete customer.ts * Couple UX fixes * One more fix * Adding lower right blue dots * Remove comment * Update components/payment_form/card_input.css Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * Update components/payment_form/payment_form.scss Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * Bunch of changes for PR * More code changes * Remove debug logs * Changes for PR review * Fix i18n * Fixes for PR * Localize * Fixes for PR Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Co-authored-by: Mattermod Co-authored-by: Devin Binnie Co-authored-by: Nev Angelova Co-authored-by: marianunez (cherry picked from commit 7b7607db13f24bd35affdc9c591c80745553f35b) Co-authored-by: Nick Misasi --- actions/cloud.tsx | 71 ++++ .../cloud_announcement_bar/index.ts | 3 + .../user_limit_announcement_bar.tsx | 16 +- components/dropdown_input.scss | 2 + components/dropdown_input.tsx | 10 + .../next_steps_view/next_steps_view.scss | 5 - components/payment_form/card_image.css | 8 + components/payment_form/card_image.tsx | 54 +++ components/payment_form/card_input.css | 38 ++ components/payment_form/card_input.tsx | 185 ++++++++++ components/payment_form/payment_form.scss | 154 ++++++++ components/payment_form/payment_form.tsx | 331 ++++++++++++++++++ components/payment_form/state_selector.tsx | 66 ++++ components/payment_form/stripe.ts | 27 ++ components/purchase_modal/icon_message.scss | 94 +++++ components/purchase_modal/icon_message.tsx | 107 ++++++ components/purchase_modal/index.ts | 57 +++ components/purchase_modal/process_payment.css | 19 + .../purchase_modal/process_payment_setup.tsx | 170 +++++++++ components/purchase_modal/purchase.scss | 153 ++++++++ components/purchase_modal/purchase_modal.tsx | 252 +++++++++++++ .../user_limit_modal.test.tsx | 1 + .../user_limit_modal/user_limit_modal.tsx | 11 +- i18n/en.json | 31 ++ images/cloud-logos/professional.svg | 22 ++ images/cloud/blue-lower.svg | 30 ++ images/cloud/blue.svg | 80 +++++ images/cloud/cards/amex.png | Bin 0 -> 2200 bytes images/cloud/cards/dinersclub.png | Bin 0 -> 32439 bytes images/cloud/cards/discover.jpg | Bin 0 -> 8662 bytes images/cloud/cards/jcb.png | Bin 0 -> 2458 bytes images/cloud/cards/mastercard.png | Bin 0 -> 1005 bytes images/cloud/cards/visa.jpg | Bin 0 -> 18709 bytes images/cloud/mattermost-cloud.svg | 10 + images/cloud/payment_fail.svg | 27 ++ images/cloud/payment_success.svg | 40 +++ images/cloud/processing_payment.svg | 74 ++++ images/cloud/upgrade.svg | 30 ++ images/cloud/waves.svg | 21 ++ images/getting-started.svg | 88 +++++ images/onboarding-bg.svg | 143 ++++++++ images/onboarding-success.svg | 40 +++ package-lock.json | 35 +- package.json | 6 +- sass/components/_announcement-bar.scss | 1 + types/cloud/sku.ts | 38 ++ utils/constants.jsx | 2 + utils/countries.ts | 11 + utils/states.ts | 78 +++++ utils/utils.jsx | 7 + webpack.config.js | 2 +- 51 files changed, 2632 insertions(+), 18 deletions(-) create mode 100644 actions/cloud.tsx create mode 100644 components/payment_form/card_image.css create mode 100644 components/payment_form/card_image.tsx create mode 100644 components/payment_form/card_input.css create mode 100644 components/payment_form/card_input.tsx create mode 100644 components/payment_form/payment_form.scss create mode 100644 components/payment_form/payment_form.tsx create mode 100644 components/payment_form/state_selector.tsx create mode 100644 components/payment_form/stripe.ts create mode 100644 components/purchase_modal/icon_message.scss create mode 100644 components/purchase_modal/icon_message.tsx create mode 100644 components/purchase_modal/index.ts create mode 100644 components/purchase_modal/process_payment.css create mode 100644 components/purchase_modal/process_payment_setup.tsx create mode 100644 components/purchase_modal/purchase.scss create mode 100644 components/purchase_modal/purchase_modal.tsx create mode 100644 images/cloud-logos/professional.svg create mode 100644 images/cloud/blue-lower.svg create mode 100644 images/cloud/blue.svg create mode 100644 images/cloud/cards/amex.png create mode 100644 images/cloud/cards/dinersclub.png create mode 100644 images/cloud/cards/discover.jpg create mode 100644 images/cloud/cards/jcb.png create mode 100644 images/cloud/cards/mastercard.png create mode 100644 images/cloud/cards/visa.jpg create mode 100644 images/cloud/mattermost-cloud.svg create mode 100755 images/cloud/payment_fail.svg create mode 100755 images/cloud/payment_success.svg create mode 100755 images/cloud/processing_payment.svg create mode 100644 images/cloud/upgrade.svg create mode 100644 images/cloud/waves.svg create mode 100644 images/getting-started.svg create mode 100644 images/onboarding-bg.svg create mode 100644 images/onboarding-success.svg create mode 100644 types/cloud/sku.ts create mode 100644 utils/countries.ts create mode 100644 utils/states.ts diff --git a/actions/cloud.tsx b/actions/cloud.tsx new file mode 100644 index 000000000000..ed7b22fd619f --- /dev/null +++ b/actions/cloud.tsx @@ -0,0 +1,71 @@ + +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Stripe} from '@stripe/stripe-js'; +import {getCode} from 'country-list'; + +import {Client4} from 'mattermost-redux/client'; + +import {getConfirmCardSetup} from 'components/payment_form/stripe'; + +import {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; + +// Returns true for success, and false for any error +export function completeStripeAddPaymentMethod(stripe: Stripe, billingDetails: BillingDetails, isDevMode: boolean) { + return async () => { + let paymentSetupIntent: StripeSetupIntent; + try { + paymentSetupIntent = await Client4.createPaymentMethod() as StripeSetupIntent; + } catch (error) { + return error; + } + const cardSetupFunction = getConfirmCardSetup(isDevMode); + const confirmCardSetup = cardSetupFunction(stripe.confirmCardSetup); + + const result = await confirmCardSetup( + paymentSetupIntent.client_secret, + { + payment_method: { + card: billingDetails.card, + billing_details: { + name: billingDetails.name, + address: { + line1: billingDetails.address, + line2: billingDetails.address2, + city: billingDetails.city, + state: billingDetails.state, + country: getCode(billingDetails.country), + postal_code: billingDetails.postalCode, + }, + }, + }, + }, + ); + + if (!result) { + return false; + } + + const {setupIntent, error: stripeError} = result; + + if (stripeError) { + return false; + } + + if (setupIntent == null) { + return false; + } + + if (setupIntent.status !== 'succeeded') { + return false; + } + + try { + await Client4.confirmPaymentMethod(setupIntent.id); + } catch (error) { + return false; + } + + return true; + }; +} diff --git a/components/announcement_bar/cloud_announcement_bar/index.ts b/components/announcement_bar/cloud_announcement_bar/index.ts index d3d9da88f44e..5637345a9d64 100644 --- a/components/announcement_bar/cloud_announcement_bar/index.ts +++ b/components/announcement_bar/cloud_announcement_bar/index.ts @@ -12,6 +12,8 @@ import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; +import {openModal} from 'actions/views/modals'; + import {GlobalState} from 'types/store'; import {Preferences} from 'utils/constants'; @@ -36,6 +38,7 @@ function mapDispatchToProps(dispatch: Dispatch) { { savePreferences, getStandardAnalytics, + openModal, }, dispatch, ), diff --git a/components/announcement_bar/cloud_announcement_bar/user_limit_announcement_bar.tsx b/components/announcement_bar/cloud_announcement_bar/user_limit_announcement_bar.tsx index 5a3601c56048..a5f692d6a3dd 100644 --- a/components/announcement_bar/cloud_announcement_bar/user_limit_announcement_bar.tsx +++ b/components/announcement_bar/cloud_announcement_bar/user_limit_announcement_bar.tsx @@ -10,8 +10,14 @@ import {AnalyticsRow} from 'mattermost-redux/types/admin'; import {isEmpty} from 'lodash'; import {t} from 'utils/i18n'; +import PurchaseModal from 'components/purchase_modal'; -import {Preferences, CloudBanners, AnnouncementBarTypes} from 'utils/constants'; +import { + Preferences, + CloudBanners, + AnnouncementBarTypes, + ModalIdentifiers, +} from 'utils/constants'; import AnnouncementBar from '../default_announcement_bar'; @@ -25,6 +31,7 @@ type Props = { actions: { savePreferences: (userId: string, preferences: PreferenceType[]) => void; getStandardAnalytics: () => void; + openModal: (modalData: {modalId: string; dialogType: any; dialogProps?: any}) => void; }; }; @@ -95,7 +102,12 @@ export default class UserLimitAnnouncementBar extends React.PureComponent type={dismissable ? AnnouncementBarTypes.ADVISOR : AnnouncementBarTypes.CRITICAL_LIGHT} showCloseButton={dismissable} handleClose={this.handleClose} - showModal={() => {}} + showModal={() => + this.props.actions.openModal({ + modalId: ModalIdentifiers.CLOUD_PURCHASE, + dialogType: PurchaseModal, + }) + } modalButtonText={t('admin.billing.subscription.upgradeMattermostCloud.upgradeButton')} modalButtonDefaultText={'Upgrade Mattermost Cloud'} message={dismissable ? t('upgrade.cloud_banner_reached') : t('upgrade.cloud_banner_over')} diff --git a/components/dropdown_input.scss b/components/dropdown_input.scss index 0a201dc6f84f..0ed4a6076e27 100644 --- a/components/dropdown_input.scss +++ b/components/dropdown_input.scss @@ -1,4 +1,5 @@ .DropdownInput { + z-index: 999999; &.Input_container { margin-top: 20px; } @@ -38,6 +39,7 @@ padding: 10px 24px; line-height: 16px; cursor: pointer; + z-index: 999999; } .DropdownInput__option.selected > div { diff --git a/components/dropdown_input.tsx b/components/dropdown_input.tsx index 8819319552f3..608743443a03 100644 --- a/components/dropdown_input.tsx +++ b/components/dropdown_input.tsx @@ -49,6 +49,14 @@ const IndicatorsContainer = (props: any) => { ); }; +const Control = (props: any) => { + return ( +
+ +
+ ); +}; + const Option = (props: any) => { return (
(props: Props) => { components={{ IndicatorsContainer, Option, + Control, }} className={classNames('Input', className, {Input__focus: showLegend})} + classNamePrefix={'DropDown'} value={value} onChange={onChange as any} // types are not working correctly for multiselect styles={{...baseStyles, ...styles}} diff --git a/components/next_steps_view/next_steps_view.scss b/components/next_steps_view/next_steps_view.scss index b7b95ae94526..ba55f6d8e29e 100644 --- a/components/next_steps_view/next_steps_view.scss +++ b/components/next_steps_view/next_steps_view.scss @@ -137,18 +137,15 @@ background-color: var(--sidebar-text-active-border); } } - .Card.complete { border-color: transparent; box-shadow: none; transition-property: box-shadow, border-color; background-color: transparent; - &:hover { border-color: transparent; box-shadow: none; } - & + .Card { margin-top: 12px; transition-property: box-shadow, border-color, margin; @@ -175,7 +172,6 @@ line-height: 26px; color: var(--online-indicator); transition: all 0.3s ease-in-out; - &::before { margin: 0; } @@ -591,7 +587,6 @@ flex: 1 1 100%; padding: 0 12px; } - .NextStepsView__nextStepsCards, .NextStepsView__download, .NextStepsView__tipsMobileMessage { padding: 0 12px; } diff --git a/components/payment_form/card_image.css b/components/payment_form/card_image.css new file mode 100644 index 000000000000..53e5a145c8fe --- /dev/null +++ b/components/payment_form/card_image.css @@ -0,0 +1,8 @@ +.CardImage { + height: auto; + width: auto; + max-height: 30px; + max-width: 30px; + margin-right: 9px; + margin-top: -1px; +} diff --git a/components/payment_form/card_image.tsx b/components/payment_form/card_image.tsx new file mode 100644 index 000000000000..ca1227e0ddb5 --- /dev/null +++ b/components/payment_form/card_image.tsx @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import amex from 'images/cloud/cards/amex.png'; + +import dinersclub from 'images/cloud/cards/dinersclub.png'; +import discover from 'images/cloud/cards/discover.jpg'; +import jcb from 'images/cloud/cards/jcb.png'; +import mastercard from 'images/cloud/cards/mastercard.png'; +import visa from 'images/cloud/cards/visa.jpg'; + +import './card_image.css'; + +type Props = { + brand: string; +} + +export default function CardImage(props: Props) { + const {brand} = props; + + const cardImageSrc = getCardImage(brand); + if (cardImageSrc) { + return ( + {brand} + ); + } + + return null; +} + +function getCardImage(brand: string): string { + switch (brand) { + case 'amex': + return amex; + case 'diners': + return dinersclub; + case 'discover': + return discover; + case 'jcb': + return jcb; + case 'mastercard': + return mastercard; + case 'visa': + return visa; + } + + return ''; +} diff --git a/components/payment_form/card_input.css b/components/payment_form/card_input.css new file mode 100644 index 000000000000..0efb2897a883 --- /dev/null +++ b/components/payment_form/card_input.css @@ -0,0 +1,38 @@ +.StripeElement { + font-family: 'Open Sans'; + font-size: 14px; + position: relative; + padding-top: 8px; + padding-left: 12px; + padding-bottom: 2px !important; + width: 100%; + height: 100%; + font-size: 14px; + line-height: 23px; + color: var(--center-channel-color); + background-color: var(--center-channel-bg); + background-image: none; + border-radius: 4px; + outline: none; + box-shadow: none; +} + +.StripeElement--invalid { + color: var(--error-text); + icon-color: var(--error-text); +} + +.StripeElement::placeholder { + color: rgba(var(--center-channel-color-rgb), 0.72); + opacity: 0.5; + font-size: 14px; +} + +.StripeElement:focus::placeholder { + color: 'transparent'; +} + +.StripeElement:focus { + border-color: transparent; + box-shadow: 0 0 0 2px var(--button-bg); +} diff --git a/components/payment_form/card_input.tsx b/components/payment_form/card_input.tsx new file mode 100644 index 000000000000..801492bbe7d1 --- /dev/null +++ b/components/payment_form/card_input.tsx @@ -0,0 +1,185 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StripeElements, StripeCardElement, StripeCardElementChangeEvent} from '@stripe/stripe-js'; +import {ElementsConsumer, CardElement} from '@stripe/react-stripe-js'; + +import {FormattedMessage} from 'react-intl'; + +import './card_input.css'; +import 'components/input.css'; + +const CARD_ELEMENT_OPTIONS = { + hidePostalCode: true, + style: { + base: { + fontFamily: 'Open Sans', + fontSize: '14px', + opacity: '0.5', + fontSmoothing: 'antialiased', + }, + }, +}; + +type OwnProps = { + error?: string; + required?: boolean; + forwardedRef?: any; + + // Stripe doesn't give type exports + [propName: string]: any; //eslint-disable-line @typescript-eslint/no-explicit-any +} + +type Props = { + elements: StripeElements | null | undefined; +} & OwnProps; + +type State = { + focused: boolean; + error: string; + empty: boolean; + complete: boolean; +} + +const REQUIRED_FIELD_TEXT = 'This field is required'; +const VALID_CARD_TEXT = 'Please enter a valid credit card'; + +export interface CardInputType extends React.PureComponent { + getCard(): StripeCardElement | undefined; +} + +class CardInput extends React.PureComponent { + public constructor(props: Props) { + super(props); + + this.state = { + focused: false, + error: '', + empty: true, + complete: false, + }; + } + + private onFocus = () => { + const {onFocus} = this.props; + + this.setState({focused: true}); + + if (onFocus) { + onFocus(); + } + } + + private onBlur = () => { + const {onBlur} = this.props; + + this.setState({focused: false}); + this.validateInput(); + + if (onBlur) { + onBlur(); + } + } + + private onChange = (event: StripeCardElementChangeEvent) => { + this.setState({error: '', empty: event.empty, complete: event.complete}); + } + + private validateInput = () => { + const {required} = this.props; + const {empty, complete} = this.state; + let error = ''; + + this.setState({error: ''}); + if (required && empty) { + error = REQUIRED_FIELD_TEXT; + } else if (!complete) { + error = VALID_CARD_TEXT; + } + + this.setState({error}); + } + + private renderError(error: string) { + if (!error) { + return null; + } + + let errorMessage; + if (error === REQUIRED_FIELD_TEXT) { + errorMessage = ( + ); + } else if (error === VALID_CARD_TEXT) { + errorMessage = ( + ); + } + + return ( +
+ + {errorMessage} +
+ ); + } + + public getCard(): StripeCardElement | null | undefined { + return this.props.elements?.getElement(CardElement); + } + + public render() { + const {className, error: propError, ...otherProps} = this.props; + const {empty, focused, error: stateError} = this.state; + let fieldsetClass = className ? `Input_fieldset ${className}` : 'Input_fieldset'; + let fieldsetErrorClass = className ? `Input_fieldset Input_fieldset___error ${className}` : 'Input_fieldset Input_fieldset___error'; + const showLegend = Boolean(focused || !empty); + + fieldsetClass = showLegend ? fieldsetClass + ' Input_fieldset___legend' : fieldsetClass; + fieldsetErrorClass = showLegend ? fieldsetErrorClass + ' Input_fieldset___legend' : fieldsetErrorClass; + + const error = propError || stateError; + + return ( +
+
+ + + + +
+ {this.renderError(error)} +
+ ); + } +} + +const InjectedCardInput = (props: OwnProps) => { + return ( + + {({elements}) => ( + + )} + + ); +}; + +export default InjectedCardInput; diff --git a/components/payment_form/payment_form.scss b/components/payment_form/payment_form.scss new file mode 100644 index 000000000000..4dcbcde14d3c --- /dev/null +++ b/components/payment_form/payment_form.scss @@ -0,0 +1,154 @@ +.PaymentForm { + margin: 0 auto; + padding-left: 96px; + padding-right: 96px; + + .form-row { + width: 100%; + margin-bottom: 24px; + display: flex; + } + + .form-row-third-1 { + .DropdownInput { + margin-top: 0px; + z-index: 99999; + } + max-width: 288px; + width: 66%; + margin-right: 16px; + } + + .form-row-third-2 { + max-width: 144px; + width: 34%; + } + + .section-title{ + font-weight: 600; + font-size: 16px; + color: var(--center-channel-text); + text-align: left; + margin-bottom: 24px; + } + + .DropdownInput { + margin-bottom: 24px; + height: 36px; + position: relative; + + z-index: 999999; + + .DropDown__control { + min-height: 0px; + } + } + + .full-width { + width: 100% + } + + Input{ + font-size: 14px; + } + + .Input_fieldset { + background: var(--center-channel-bg); + height: 40px; + padding: 2px 1px; + + .Input_wrapper { + margin: 0; + } + + &.Input_fieldset___legend { + >legend { + margin-left: 11px; + } + } + + &.Input_fieldset:focus-within { + box-shadow: inset 0 0 0 2px var(--button-bg); + color: var(--button-bg); + padding-top: 2px + } + + + &.Input_fieldset___error { + color: var(--error-text); + box-shadow: inset 0 0 0 1px var(--error-text); + padding-top: 1px; + padding-bottom: 1px; + } + + &.Input_fieldset___error:focus-within { + color: var(--error-text); + box-shadow: inset 0 0 0 2px var(--error-text); + } + + .Input { + height: 32px; + &::placeholder{ + color: var(--center-channel-color); + opacity: 0.64; + } + } + } + + + .PaymentForm-saved { + width: 442px; + height: fit-content; + background: #FFFFFF; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + box-sizing: border-box; + border-radius: 4px; + padding: 24px; + margin-bottom: 16px; + } + + .PaymentForm-saved-title { + font-family: Metropolis; + font-weight: 600; + font-size: 14px; + line-height: 18px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: #22406D; + opacity: 0.4; + margin-bottom: 16px; + } + + .PaymentForm-saved-card { + font-family: Source Sans Pro; + font-size: 16px; + line-height: 24px; + color: #22406D; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + padding-bottom: 16px; + } + + .PaymentForm-saved-address { + font-family: Source Sans Pro; + font-size: 16px; + line-height: 24px; + color: #22406D; + margin-top: 16px; + margin-bottom: 16px; + } + + .PaymentForm-change { + font-family: Source Sans Pro; + font-weight: 600; + font-size: 16px; + line-height: 20px; + color: #0058CC; + } +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px var(--center-channel-bg) inset !important; +} diff --git a/components/payment_form/payment_form.tsx b/components/payment_form/payment_form.tsx new file mode 100644 index 000000000000..f6248c6d413d --- /dev/null +++ b/components/payment_form/payment_form.tsx @@ -0,0 +1,331 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {getName} from 'country-list'; +import {FormattedMessage} from 'react-intl'; + +import {PaymentMethod} from 'mattermost-redux/types/cloud'; + +import {BillingDetails} from 'types/cloud/sku'; + +import DropdownInput from 'components/dropdown_input'; +import Input from 'components/input'; +import * as Utils from 'utils/utils'; +import {COUNTRIES} from 'utils/countries'; + +import StateSelector from './state_selector'; +import CardInput, {CardInputType} from './card_input'; +import CardImage from './card_image'; + +import './payment_form.scss'; +import 'components/input.css'; + +type Props = { + className: string; + initialBillingDetails?: BillingDetails; + paymentMethod?: PaymentMethod; + onInputChange?: (billing: BillingDetails) => void; + onInputBlur?: (billing: BillingDetails) => void; + buttonFooter?: JSX.Element; +} + +type State = { + address: string; + address2: string; + city: string; + state: string; + country: string; + postalCode: string; + name: string; + changePaymentMethod: boolean; +} + +export default class PaymentForm extends React.PureComponent { + static defaultProps = { + showSaveCard: false, + className: '', + }; + + cardRef: React.RefObject; + + public constructor(props: Props) { + super(props); + + this.cardRef = React.createRef(); + + this.state = this.getResetState(props); + } + + public componentDidUpdate(prevProps: Props) { + if (prevProps.paymentMethod == null && this.props.paymentMethod != null) { + this.resetState(); + return; + } + + if (prevProps.initialBillingDetails === undefined && this.props.initialBillingDetails !== undefined) { + this.resetState(); + } + } + + private resetState = () => { + this.setState(this.getResetState()); + } + + private getResetState = (props = this.props) => { + const {initialBillingDetails, paymentMethod} = props; + + const billingDetails = initialBillingDetails || {} as BillingDetails; + + return { + address: billingDetails.address, + address2: billingDetails.address2, + city: billingDetails.city, + state: billingDetails.state, + country: getName(billingDetails.country || '') || getName('US') || '', + postalCode: billingDetails.postalCode, + name: billingDetails.name, + changePaymentMethod: paymentMethod == null, + }; + } + + private handleInputChange = (event: React.ChangeEvent | React.ChangeEvent) => { + const target = event.target; + const name = target.name; + const value = target.value; + + const newStateValue = { + [name]: value, + } as unknown as Pick; + + this.setState(newStateValue); + + const {onInputChange} = this.props; + if (onInputChange) { + onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + } + } + + private handleStateChange = (stateValue: string) => { + const newStateValue = { + state: stateValue, + } as unknown as Pick; + this.setState(newStateValue); + + if (this.props.onInputChange) { + this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + } + } + + private handleCountryChange = (option: any) => { + const newStateValue = { + country: option.value, + } as unknown as Pick; + this.setState(newStateValue); + + if (this.props.onInputChange) { + this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + } + } + + private onBlur = () => { + const {onInputBlur} = this.props; + if (onInputBlur) { + onInputBlur({...this.state, card: this.cardRef.current?.getCard()} as BillingDetails); + } + } + + private changePaymentMethod = (event: React.MouseEvent) => { + event.preventDefault(); + this.setState({changePaymentMethod: true}); + } + + public render() { + const {className, paymentMethod, buttonFooter} = this.props; + const {changePaymentMethod} = this.state; + + let paymentDetails: JSX.Element; + if (changePaymentMethod) { + paymentDetails = ( + +
+ +
+
+ +
+
+ +
+ ({value: country.name, label: country.name}))} + legend={Utils.localizeMessage('payment_form.country', 'Country')} + placeholder={Utils.localizeMessage('payment_form.country', 'Country')} + name={'billing_dropdown'} + /> +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ {changePaymentMethod ? buttonFooter : null} +
+ ); + } else { + let cardContent: JSX.Element | null = null; + + if (paymentMethod) { + let cardDetails = ( + + ); + if (paymentMethod.last_four) { + cardDetails = ( + + + {`Card ending in ${paymentMethod.last_four}`} +
+ {`Expires ${paymentMethod.exp_month}/${paymentMethod.exp_year}`} +
+ ); + } + let addressDetails = ( + + + ); + if (this.state.state) { + addressDetails = ( + + {this.state.address} + {this.state.address2} +
+ {`${this.state.city}, ${this.state.state}, ${this.state.country}`} +
+ {this.state.postalCode} +
+ ); + } + + cardContent = ( + +
+ {cardDetails} +
+
+ {addressDetails} +
+
+ ); + } + + paymentDetails = ( +
+
+ +
+ {cardContent} + +
+ ); + } + + return ( +
+
+ +
+ {paymentDetails} +
+ ); + } +} diff --git a/components/payment_form/state_selector.tsx b/components/payment_form/state_selector.tsx new file mode 100644 index 000000000000..e293ad05a675 --- /dev/null +++ b/components/payment_form/state_selector.tsx @@ -0,0 +1,66 @@ + +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {useIntl} from 'react-intl'; +import {getName} from 'country-list'; + +import DropdownInput from 'components/dropdown_input'; + +import Input from 'components/input'; + +import {US_STATES, CA_PROVINCES, StateCode} from 'utils/states'; + +type Props = { + country: string; + state: string; + onChange: (newValue: string) => void; + onBlur?: () => void; +} + +// StateSelector will display a state dropdown for US and Canada. +// Will display a open text input for any other country. +export default function StateSelector(props: Props) { + // Making TS happy here with the react-select event handler + const {formatMessage} = useIntl(); + const onStateSelected = (option: any) => { + props.onChange(option.value); + }; + + let stateList = [] as StateCode[]; + if (props.country === getName('US')) { + stateList = US_STATES; + } else if (props.country === getName('CA')) { + stateList = CA_PROVINCES; + } + + if (stateList.length > 0) { + return ( + ({ + value: stateCode.code, + label: stateCode.name, + }))} + legend={formatMessage({id: 'admin.billing.subscription.stateprovince', defaultMessage: 'State/Province'})} + placeholder={formatMessage({id: 'admin.billing.subscription.stateprovince', defaultMessage: 'State/Province'})} + name={'billing_dropdown'} + /> + ); + } + + return ( + { + props.onChange(e.target.value); + }} + onBlur={props.onBlur} + placeholder={formatMessage({id: 'admin.billing.subscription.stateprovince', defaultMessage: 'State/Province'})} + required={true} + />); +} + diff --git a/components/payment_form/stripe.ts b/components/payment_form/stripe.ts new file mode 100644 index 000000000000..1c4795b869bb --- /dev/null +++ b/components/payment_form/stripe.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { + StripeError, + ConfirmCardSetupData, + ConfirmCardSetupOptions, + SetupIntent, +} from '@stripe/stripe-js'; + +type ConfirmCardSetupType = (clientSecret: string, data?: ConfirmCardSetupData | undefined, options?: ConfirmCardSetupOptions | undefined) => Promise<{ setupIntent?: SetupIntent | undefined; error?: StripeError | undefined }> | undefined; + +function prodConfirmCardSetup(confirmCardSetup: ConfirmCardSetupType): ConfirmCardSetupType { + return confirmCardSetup; +} + +function devConfirmCardSetup(confirmCardSetup: ConfirmCardSetupType): ConfirmCardSetupType { + return async (clientSecret: string, data?: ConfirmCardSetupData | undefined, options?: ConfirmCardSetupOptions | undefined) => { + return {setupIntent: {id: 'testid', status: 'succeeded'} as SetupIntent}; + }; +} + +export const getConfirmCardSetup = (isDevMode: boolean) => (isDevMode ? devConfirmCardSetup : prodConfirmCardSetup); + diff --git a/components/purchase_modal/icon_message.scss b/components/purchase_modal/icon_message.scss new file mode 100644 index 000000000000..3016bbe75c12 --- /dev/null +++ b/components/purchase_modal/icon_message.scss @@ -0,0 +1,94 @@ +.IconMessage { + display: block; + text-align: center; + width: 100%; + height: 100%; + overflow: auto; + margin: auto; + position: absolute; + top: 0px; left: 0; bottom: 0; right: 0; + .content { + &.success { + padding-left: 7px; + padding-bottom: 2px; + } + margin: 0; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + position: absolute; + .IconMessage-h3 { + font-size: 24px; + line-height: 32px; + color: var(--center-channel-color); + margin-top: 43px; + font-weight: 600; + + @media screen and (max-width: 600px) { + font-size: 36px; + line-height: 42px; + letter-spacing: -0.13px; + } + } + + .IconMessage-sub { + &.error { + color: #DB3214; + } + + margin-top: 16px; + font-size: 14px; + font-style: normal; + font-weight: normal; + line-height: 20px; + color: var(--center-channel-color); + } + + .IconMessage-img { + } + + .IconMessage-progress { + margin: 0 auto; + margin-top: 90px; + height: 4px; + width: 387px; + background: rgba(34, 64, 109, 0.12); + border-radius: 4px; + } + + .IconMessage-progress-fill { + height: 100%; + background: #0058CC; + border-radius: 4px; + } + + .IconMessage-button { + margin-left: auto; + margin-right: auto; + margin-top: 16px; + align-self: center; + text-align: center; + font-weight: 600; + font-size: 14px; + line-height: 14px; + height: 40px; + width: 95px; + + &.error { + width: 176px; + } + } + + .IconMessage-link { + margin-top: 21px; + font-weight: 600; + font-size: 14px; + line-height: 14px; + text-align: center; + color: #0058CC; + margin-left: -10px; + } + + } +} + diff --git a/components/purchase_modal/icon_message.tsx b/components/purchase_modal/icon_message.tsx new file mode 100644 index 000000000000..6f3bfbf8aedc --- /dev/null +++ b/components/purchase_modal/icon_message.tsx @@ -0,0 +1,107 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; + +import './icon_message.scss'; + +type Props = { + icon: string; + title: string; + subtitle?: string; + date?: string; + error?: boolean; + buttonText?: string; + buttonHandler?: () => void; + linkText?: string; + linkURL?: string; + footer?: JSX.Element; + className?: string; +} + +export default function IconMessage(props: Props) { + const { + icon, + title, + subtitle, + date, + error, + buttonText, + buttonHandler, + linkText, + linkURL, + footer, + className, + } = props; + + let button = null; + if (buttonText && buttonHandler) { + button = ( +
+ +
+ ); + } + + let link = null; + if (linkText && linkURL) { + link = ( +
+ + + +
+ ); + } + return ( +
+
+ Payment icon +

+ +

+
+ {subtitle ? ( + + ) : null} +
+ {button} + {link} + {footer} +
+
+ ); +} + +IconMessage.defaultProps = { + error: false, + subtitle: '', + date: '', + className: '', +}; diff --git a/components/purchase_modal/index.ts b/components/purchase_modal/index.ts new file mode 100644 index 000000000000..5a5ab8b86851 --- /dev/null +++ b/components/purchase_modal/index.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux'; + +import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions'; +import {Stripe} from '@stripe/stripe-js'; + +import {getCloudProducts} from 'mattermost-redux/actions/cloud'; + +import { + getClientConfig, +} from 'mattermost-redux/actions/general'; + +import {BillingDetails} from 'types/cloud/sku'; + +import {isModalOpen} from 'selectors/views/modals'; +import {ModalIdentifiers} from 'utils/constants'; + +import {closeModal} from 'actions/views/modals'; +import {completeStripeAddPaymentMethod} from 'actions/cloud'; + +import {GlobalState} from 'types/store'; + +import PurchaseModal from './purchase_modal'; + +function mapStateToProps(state: GlobalState) { + return { + show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE), + products: state.entities.cloud!.products, + isDevMode: getConfig(state).EnableDeveloper === 'true', + }; +} +type Actions = { + closeModal: () => void; + getCloudProducts: () => void; + completeStripeAddPaymentMethod: (stripe: Stripe, billingDetails: BillingDetails, isDevMode: boolean) => Promise; + getClientConfig: () => void; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators, Actions>( + { + closeModal: () => closeModal(ModalIdentifiers.CLOUD_PURCHASE), + getCloudProducts, + completeStripeAddPaymentMethod, + getClientConfig, + }, + dispatch, + ), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(PurchaseModal); diff --git a/components/purchase_modal/process_payment.css b/components/purchase_modal/process_payment.css new file mode 100644 index 000000000000..a0deebd9fee6 --- /dev/null +++ b/components/purchase_modal/process_payment.css @@ -0,0 +1,19 @@ +.ProcessPayment-progress { + margin: 0 auto; + margin-top: 60px; + height: 4px; + width: 387px; + background: rgb(34,64,109, 0.12); + border-radius: 4px; +} + +.ProcessPayment-progress-fill { + height: 100%; + background: #0058CC; + border-radius: 4px; +} + +.ProcessPayment-body { + overflow-x: hidden; + overflow-y: hidden; +} diff --git a/components/purchase_modal/process_payment_setup.tsx b/components/purchase_modal/process_payment_setup.tsx new file mode 100644 index 000000000000..bc2b526356fc --- /dev/null +++ b/components/purchase_modal/process_payment_setup.tsx @@ -0,0 +1,170 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Stripe} from '@stripe/stripe-js'; + +import {BillingDetails} from 'types/cloud/sku'; + +import successSvg from 'images/cloud/payment_success.svg'; +import failedSvg from 'images/cloud/payment_fail.svg'; +import {t} from 'utils/i18n'; +import {getNextBillingDate} from 'utils/utils'; + +import processSvg from 'images/cloud/processing_payment.svg'; + +import './process_payment.css'; + +import IconMessage from './icon_message'; + +type Props = { + billingDetails: BillingDetails | null; + stripe: Promise; + isDevMode: boolean; + addPaymentMethod: (stripe: Stripe, billingDetails: BillingDetails, isDevMode: boolean) => Promise; + onBack: () => void; + onClose: () => void; +} + +type State = { + progress: number; + error: boolean; + state: ProcessState; +} + +enum ProcessState { + PROCESSING = 0, + SUCCESS, + FAILED +} + +const MIN_PROCESSING_MILLISECONDS = 5000; +const MAX_FAKE_PROGRESS = 95; + +export default class ProcessPaymentSetup extends React.PureComponent { + intervalId: NodeJS.Timeout; + + public constructor(props: Props) { + super(props); + + this.intervalId = {} as NodeJS.Timeout; + + this.state = { + progress: 0, + error: false, + state: ProcessState.PROCESSING, + }; + } + + public componentDidMount() { + this.savePaymentMethod(); + + this.intervalId = setInterval(this.updateProgress, MIN_PROCESSING_MILLISECONDS / MAX_FAKE_PROGRESS); + } + + public componentWillUnmount() { + clearInterval(this.intervalId); + } + + private updateProgress = () => { + let {progress} = this.state; + + if (progress >= MAX_FAKE_PROGRESS) { + clearInterval(this.intervalId); + return; + } + + progress += 1; + this.setState({progress: progress > MAX_FAKE_PROGRESS ? MAX_FAKE_PROGRESS : progress}); + } + + private savePaymentMethod = async () => { + const start = new Date(); + const {stripe, addPaymentMethod, billingDetails, isDevMode} = this.props; + const success = await addPaymentMethod((await stripe)!, billingDetails!, isDevMode); + + if (!success) { + this.setState({ + error: true, + state: ProcessState.FAILED}); + return; + } + + const end = new Date(); + const millisecondsElapsed = end.valueOf() - start.valueOf(); + if (millisecondsElapsed < MIN_PROCESSING_MILLISECONDS) { + setTimeout(this.completePayment, MIN_PROCESSING_MILLISECONDS - millisecondsElapsed); + return; + } + + this.completePayment(); + } + + private completePayment = () => { + clearInterval(this.intervalId); + this.setState({state: ProcessState.SUCCESS, progress: 100}); + } + + private handleGoBack = () => { + clearInterval(this.intervalId); + this.setState({ + progress: 0, + error: false, + state: ProcessState.PROCESSING, + }); + this.props.onBack(); + } + + public render() { + const {state, progress, error} = this.state; + + const progressBar: JSX.Element | null = ( +
+
+
+ ); + + switch (state) { + case ProcessState.PROCESSING: + return ( + + ); + case ProcessState.SUCCESS: + return ( + + ); + case ProcessState.FAILED: + return ( + + ); + default: + return null; + } + } +} diff --git a/components/purchase_modal/purchase.scss b/components/purchase_modal/purchase.scss new file mode 100644 index 000000000000..0e2fa21ef00e --- /dev/null +++ b/components/purchase_modal/purchase.scss @@ -0,0 +1,153 @@ +.PurchaseModal { + height: 100%; + overflow: hidden; + + >div { + &.processing { + display: none; + } + overflow: hidden; + color: var(--center-channel-color); + font-size: 16px; + font-family: 'Open Sans'; + padding: 77px 107px; + font-weight: 600; + display: flex; + flex-direction: row; + flex-wrap: wrap; + flex-grow: 1; + width: 100%; + height: 100%; + + .footer-text { + font-size: 14px; + } + + .fineprint-text { + margin-top: 24px; + font-size: 12px; + line-height: 16px; + } + + .bold-text { + font-weight: 600; + } + + .normal-text { + font-weight: normal; + } + + .LHS { + width: 25%; + + .title { + font-size: 24px; + } + + .image { + padding: 32px 0px; + } + } + + .central-panel { + width: 50%; + } + + .full-width { + border-color: rgba(var(--center-channel-color-rgb), 0.16) + } + + .RHS { + width: 25%; + position: sticky; + + .price-container { + font-weight: normal; + padding: 24px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + box-sizing: border-box; + box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12); + border-radius: 4px; + background-color: var(--center-channel-bg); + margin-bottom: 24px; + min-width: 270px; + + .footer-text { + font-size: 14px; + margin-bottom: 24px; + } + } + + .price-text { + font-size: 32px; + font-weight: 600; + padding: 16px 0px; + line-height: 1; + } + + .monthly-text { + font-size: 14px; + font-weight: normal; + } + } + + .waves { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: -1; + width: 100%; + stop { + stop-color: var(--button-bg); + } + } + + .blue-dots { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: -1; + } + + .lower-blue-dots { + position: absolute; + right: 0; + bottom: 100px; + z-index: -1; + } + + .logo { + position: absolute; + bottom: 0px; + right: 0px; + } + + button { + background: #0058CC; + width: 100%; + height: 40px; + font-weight: 600; + font-size: 14px; + border-radius: 4px; + background: var(--button-bg); + color: var(--button-color); + border: 0; + + &:disabled { + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.32); + } + } + } +} + +.FullScreenModal { + .close-x { + top: 12px; + right: 12px; + } +} diff --git a/components/purchase_modal/purchase_modal.tsx b/components/purchase_modal/purchase_modal.tsx new file mode 100644 index 000000000000..104b6ce5f1a1 --- /dev/null +++ b/components/purchase_modal/purchase_modal.tsx @@ -0,0 +1,252 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {Stripe, loadStripe} from '@stripe/stripe-js'; +import {Elements} from '@stripe/react-stripe-js'; + +import {Product} from 'mattermost-redux/types/cloud'; +import {Dictionary} from 'mattermost-redux/types/utilities'; + +import upgradeImage from 'images/cloud/upgrade.svg'; +import wavesBackground from 'images/cloud/waves.svg'; +import blueDotes from 'images/cloud/blue.svg'; +import LowerBlueDots from 'images/cloud/blue-lower.svg'; + +import cloudLogo from 'images/cloud/mattermost-cloud.svg'; + +import RootPortal from 'components/root_portal'; +import FullScreenModal from 'components/widgets/modals/full_screen_modal'; +import {areBillingDetailsValid, BillingDetails} from 'types/cloud/sku'; +import {getNextBillingDate} from 'utils/utils'; + +import PaymentForm from '../payment_form/payment_form'; + +import './purchase.scss'; +import 'components/payment_form/payment_form.scss'; +import ProcessPaymentSetup from './process_payment_setup'; + +const STRIPE_CSS_SRC = 'https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i&display=swap'; +const STRIPE_PUBLIC_KEY = 'pk_test_ttEpW6dCHksKyfAFzh6MvgBj'; + +const stripePromise = loadStripe(STRIPE_PUBLIC_KEY); + +type Props = { + show: boolean; + isDevMode: boolean; + products?: Dictionary; + actions: { + closeModal: () => void; + getCloudProducts: () => void; + completeStripeAddPaymentMethod: (stripe: Stripe, billingDetails: BillingDetails, isDevMode: boolean) => Promise; + getClientConfig: () => void; + }; +} + +type State = { + paymentInfoIsValid: boolean; + productPrice: number; + billingDetails: BillingDetails | null; + processing: boolean; +} +export default class PurchaseModal extends React.PureComponent { + modal = React.createRef(); + + public constructor(props: Props) { + super(props); + + this.state = { + paymentInfoIsValid: false, + productPrice: 0, + billingDetails: null, + processing: false, + }; + } + + static getDerivedStateFromProps(props: Props, state: State) { + let productPrice = 0; + if (props.products) { + const keys = Object.keys(props.products); + if (keys.length > 0) { + // Assuming the first and only one for now. + productPrice = props.products[keys[0]].price_per_seat; + } + } + + return {...state, productPrice}; + } + + componentDidMount() { + this.props.actions.getCloudProducts(); + + // this.fetchProductPrice(); + this.props.actions.getClientConfig(); + } + + onPaymentInput = (billing: BillingDetails) => { + this.setState({paymentInfoIsValid: areBillingDetailsValid(billing)}); + this.setState({billingDetails: billing}); + } + + handleSubmitClick = async () => { + this.setState({processing: true, paymentInfoIsValid: false}); + } + + purchaseScreen = () => { + return ( +
+
+
+ +
+ upgrade +
+ +
+ + + +
+
+ +
+
+
+
+ +
+
+ {`$${this.state.productPrice || 0}`} + + + +
+
{`Payment begins: ${getNextBillingDate()}`}
+ +
+ + + + {'\u00A0'} + + + +
+
+
{'Need other billing options?'}
+ + + + +
+ +
+
+
+ ); + } + + render() { + return ( + + + +
+ {this.state.processing ? ( +
+ { + this.setState({processing: false}); + }} + /> +
+ ) : null} + {this.purchaseScreen()} +
+ + + +
+
+
+
+
+ ); + } +} diff --git a/components/user_limit_modal/user_limit_modal.test.tsx b/components/user_limit_modal/user_limit_modal.test.tsx index a8a6fcfa8b0d..0897db0a07b3 100644 --- a/components/user_limit_modal/user_limit_modal.test.tsx +++ b/components/user_limit_modal/user_limit_modal.test.tsx @@ -11,6 +11,7 @@ describe('component/UserLimitModal', () => { show: true, actions: { closeModal: () => { }, + openModal: jest.fn(), }, }; diff --git a/components/user_limit_modal/user_limit_modal.tsx b/components/user_limit_modal/user_limit_modal.tsx index d53edaef319d..c929e2fa7ce8 100644 --- a/components/user_limit_modal/user_limit_modal.tsx +++ b/components/user_limit_modal/user_limit_modal.tsx @@ -5,6 +5,9 @@ import React from 'react'; import {Modal, Button} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; +import {ModalIdentifiers} from 'utils/constants'; +import PurchaseModal from 'components/purchase_modal'; + import UpgradeUserLimitModalSvg from './user_limit_upgrade_svg'; import './user_limit_modal.scss'; @@ -12,13 +15,17 @@ type Props = { show: boolean; actions: { closeModal: () => void; + openModal: (modalData: {modalId: string; dialogType: any; dialogProps?: any}) => void; }; }; export default function UserLimitModal(props: Props) { const onSubmit = () => { - // This does nothing until implementation of the upgrade tier page is complete - // Eventually, we will dispatch props.actions.openModal here + props.actions.closeModal(); + props.actions.openModal({ + modalId: ModalIdentifiers.CLOUD_PURCHASE, + dialogType: PurchaseModal, + }); }; const close = () => { diff --git a/i18n/en.json b/i18n/en.json index 53344410a9cd..ca6217741aa4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -229,6 +229,15 @@ "admin.billing.payment_info_edit.title": "Edit Payment Information", "admin.billing.payment_info.add": "Add Payment Information", "admin.billing.payment_info.title": "Payment Information", + "admin.billing.subscription.disclaimer": "Your total is calculated at the end of the billing cycle based on the number of enabled users. You’ll only be charged if you exceed the free tier limits.", + "admin.billing.subscription.goBackTryAgain": "Go back and try again", + "admin.billing.subscription.howItWorks": "See how billing works", + "admin.billing.subscription.letsGo": "Lets go!", + "admin.billing.subscription.mattermostCloud": "Mattermost Cloud", + "admin.billing.subscription.nextBillingDate": "Starting {date} you will be charged based on the number of enabled users", + "admin.billing.subscription.paymentFailed": "Payment failed. Please try again or contact support.", + "admin.billing.subscription.paymentVerificationFailed": "Sorry, the payment verification failed", + "admin.billing.subscription.perUserPerMonth": " /user/month", "admin.billing.subscription.planDetails.currentPlan": "Current Plan", "admin.billing.subscription.planDetails.endDate": "End Date: ", "admin.billing.subscription.planDetails.features.10GBstoragePerUser": "10 GB storage per user", @@ -249,12 +258,19 @@ "admin.billing.subscription.planDetails.userCount": "{userCount} users", "admin.billing.subscription.planDetails.userCountWithLimit": "{userCount} / {userLimit} users", "admin.billing.subscription.privateCloudCard.contactSales": "Contact Sales", + "admin.billing.subscription.privateCloudCard.contactSupport": "Contact Support", "admin.billing.subscription.privateCloudCard.description": "If you need software with dedicated, single-tenant architecture, Mattermost Private Cloud (Beta) is the solution for high-trust collaboration.", "admin.billing.subscription.privateCloudCard.title": "Looking for a high-trust private cloud?", + "admin.billing.subscription.questions": "Questions?", + "admin.billing.subscription.stateprovince": "State/Province", "admin.billing.subscription.title": "Subscriptions", + "admin.billing.subscription.upgrade": "Upgrade", + "admin.billing.subscription.upgradeCloudSubscription": "Upgrade your Mattermost Cloud Subscription", + "admin.billing.subscription.upgradedSuccess": "Great! You're now upgraded", "admin.billing.subscription.upgradeMattermostCloud.description": "The free tier is **limited to 10 users.** Get access to more users, teams and other great features", "admin.billing.subscription.upgradeMattermostCloud.title": "Need more users?", "admin.billing.subscription.upgradeMattermostCloud.upgradeButton": "Upgrade Mattermost Cloud", + "admin.billing.subscription.verifyPaymentInformation": "Verifying your payment information", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "In the future, this is where your most recent bill summary will show.", "admin.billing.subscriptions.billing_summary.noBillingHistory.link": "See how billing works", "admin.billing.subscriptions.billing_summary.noBillingHistory.title": "No billing history yet", @@ -3305,6 +3321,21 @@ "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "passwordRequirements": "Password Requirements:", + "payment_form.address": "Address", + "payment_form.address_2": "Address 2", + "payment_form.billing_address": "Billing address", + "payment_form.change_payment_method": "Change Payment Method", + "payment_form.city": "City", + "payment_form.country": "Country", + "payment_form.credit_card": "Credit Card", + "payment_form.name_on_card": "Name on Card", + "payment_form.no_billing_address": "No billing address added", + "payment_form.no_credit_card": "No credit card added", + "payment_form.saved_payment_method": "Saved Payment Method", + "payment_form.zipcode": "Zip/Postal Code", + "payment.card_number": "Card Number", + "payment.field_required": "This field is required", + "payment.invalid_card_number": "Please enter a valid credit card", "pdf_preview.max_pages": "Download to read more pages", "pending_post_actions.cancel": "Cancel", "pending_post_actions.retry": "Retry", diff --git a/images/cloud-logos/professional.svg b/images/cloud-logos/professional.svg new file mode 100644 index 000000000000..1dc55f32f30e --- /dev/null +++ b/images/cloud-logos/professional.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/blue-lower.svg b/images/cloud/blue-lower.svg new file mode 100644 index 000000000000..ef3fec6b8f28 --- /dev/null +++ b/images/cloud/blue-lower.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/blue.svg b/images/cloud/blue.svg new file mode 100644 index 000000000000..294411c0e293 --- /dev/null +++ b/images/cloud/blue.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/cards/amex.png b/images/cloud/cards/amex.png new file mode 100644 index 0000000000000000000000000000000000000000..e225b899a4a9c7544527ec6d48ce0ad0e234279a GIT binary patch literal 2200 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%0DH)uBv2!3 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 Y_rD(r34jNTk&O`ezz>;-Kmq~)JGuF$j{pDw literal 0 HcmV?d00001 diff --git a/images/cloud/cards/mastercard.png b/images/cloud/cards/mastercard.png new file mode 100644 index 0000000000000000000000000000000000000000..4ae2259a080d42382feadfa55c44a3b241f7a216 GIT binary patch literal 1005 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 literal 0 HcmV?d00001 diff --git a/images/cloud/mattermost-cloud.svg b/images/cloud/mattermost-cloud.svg new file mode 100644 index 000000000000..bf223e7463b1 --- /dev/null +++ b/images/cloud/mattermost-cloud.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/cloud/payment_fail.svg b/images/cloud/payment_fail.svg new file mode 100755 index 000000000000..5fc847baf10c --- /dev/null +++ b/images/cloud/payment_fail.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/payment_success.svg b/images/cloud/payment_success.svg new file mode 100755 index 000000000000..9cd241ff6f36 --- /dev/null +++ b/images/cloud/payment_success.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/processing_payment.svg b/images/cloud/processing_payment.svg new file mode 100755 index 000000000000..bc122b182d5b --- /dev/null +++ b/images/cloud/processing_payment.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/upgrade.svg b/images/cloud/upgrade.svg new file mode 100644 index 000000000000..3a6da21bc939 --- /dev/null +++ b/images/cloud/upgrade.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/cloud/waves.svg b/images/cloud/waves.svg new file mode 100644 index 000000000000..a52e47dba86e --- /dev/null +++ b/images/cloud/waves.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images/getting-started.svg b/images/getting-started.svg new file mode 100644 index 000000000000..e406c900bd18 --- /dev/null +++ b/images/getting-started.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/onboarding-bg.svg b/images/onboarding-bg.svg new file mode 100644 index 000000000000..c6b036cd3fc7 --- /dev/null +++ b/images/onboarding-bg.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/onboarding-success.svg b/images/onboarding-success.svg new file mode 100644 index 000000000000..042be4962e4b --- /dev/null +++ b/images/onboarding-success.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index a55ae0df9377..2295f208fec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4146,6 +4146,19 @@ } } }, + "@stripe/react-stripe-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.1.2.tgz", + "integrity": "sha512-07hu8RJXwWKGbvdvd1yt1cYvGtDB8jFX+q10f7FQuItUt9rlSo0am3WIx845iMHANiYgxyRb1PS201Yle9xxPQ==", + "requires": { + "prop-types": "^15.7.2" + } + }, + "@stripe/stripe-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.9.0.tgz", + "integrity": "sha512-/8+zfeRHlsEsxj0qmq9qbrnyF3fx+r97sDxfv7kqOyZFUFzC7DBwQwZlmss6XiV3ez5vz1G1QIjEW29PmLmsAw==" + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -4613,6 +4626,11 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/country-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/country-list/-/country-list-2.1.0.tgz", + "integrity": "sha512-aCE6SP9WQJkq9kNFDRdxpUfWhazvreNSPtm1BQG1B4/xSpKbRwfweVAChYx6MMLyTguCKJExGZ1C52Z8DlFSGg==" + }, "@types/enzyme": { "version": "3.10.5", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz", @@ -8879,6 +8897,11 @@ "yaml": "^1.7.2" } }, + "country-list": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/country-list/-/country-list-2.2.0.tgz", + "integrity": "sha512-AS21pllCp72LmUztqXPVKXK3TRyap1XlohGLqN04cXH2rFB9vo7SnH5sMnqltZPnu9ie/x1se3UuCZJbGXErfw==" + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -12559,11 +12582,15 @@ "resolved": "https://registry.npmjs.org/full-icu/-/full-icu-1.3.1.tgz", "integrity": "sha512-VMtK//85QJomhk3cXOCksNwOYaw1KWnYTS37GYGgyf7A3ajdBoPGhaJuJWAH2S2kq8GZeXkdKn+3Mfmgy11cVw==", "dev": true, + "requires": { + "icu4c-data": "0.64.2" + }, "dependencies": { "icu4c-data": { "version": "0.64.2", "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.64.2.tgz", - "integrity": "sha512-BPuTfkRTkplmK1pNrqgyOLJ0qB2UcQ12EotVLwiWh4ErtZR1tEYoRZk/LBLmlDfK5v574/lQYLB4jT9vApBiBQ==" + "integrity": "sha512-BPuTfkRTkplmK1pNrqgyOLJ0qB2UcQ12EotVLwiWh4ErtZR1tEYoRZk/LBLmlDfK5v574/lQYLB4jT9vApBiBQ==", + "dev": true } } }, @@ -13464,12 +13491,6 @@ "postcss": "^7.0.14" } }, - "icu4c-data": { - "version": "0.67.2", - "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.67.2.tgz", - "integrity": "sha512-OIRiop+k1IVf4TBLEOj910duoO9NKwtJLwp++qWT6KT5gRziHNt+5gwhcGuTqRy++RTK2gLoAIbk8KYCNxW++g==", - "dev": true - }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", diff --git a/package.json b/package.json index b9b510c0f436..db7d701ad473 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,14 @@ "dependencies": { "@formatjs/intl-pluralrules": "3.2.3", "@formatjs/intl-relativetimeformat": "6.2.3", + "@stripe/react-stripe-js": "1.1.2", + "@stripe/stripe-js": "1.9.0", + "@types/country-list": "2.1.0", "bootstrap": "3.4.1", "chart.js": "2.9.3", "compass-mixins": "0.12.10", "core-js": "3.6.5", + "country-list": "2.2.0", "css-vars-ponyfill": "2.3.2", "custom-protocol-detection": "github:ismailhabib/custom-protocol-detection", "dynamic-virtualized-list": "github:mattermost/dynamic-virtualized-list#119db968c96643c7106d4d2c965f05b2e251bc83", @@ -233,7 +237,7 @@ "fix": "eslint --ext .js,.jsx,.tsx,.ts . --quiet --fix --cache", "build": "cross-env NODE_ENV=production webpack --display-error-details --verbose", "run": "webpack --progress --watch", - "dev-server": "webpack-dev-server", + "dev-server": "webpack-dev-server -d", "test": "cross-env NODE_ICU_DATA=node_modules/full-icu TZ=Etc/UTC jest --maxWorkers=50%", "test-ci": "cross-env NODE_ICU_DATA=node_modules/full-icu TZ=Etc/UTC jest --ci --maxWorkers=8", "stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json", diff --git a/sass/components/_announcement-bar.scss b/sass/components/_announcement-bar.scss index 2a5a3bb408e7..414ea8b12dc5 100644 --- a/sass/components/_announcement-bar.scss +++ b/sass/components/_announcement-bar.scss @@ -35,6 +35,7 @@ vertical-align: middle; opacity: 0.56; } + z-index: 1029; } background-color: #113166; diff --git a/types/cloud/sku.ts b/types/cloud/sku.ts new file mode 100644 index 000000000000..8a13bbe9fc4f --- /dev/null +++ b/types/cloud/sku.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {StripeCardElement} from '@stripe/stripe-js'; + +export type StripeSetupIntent = { + id: string; + client_secret: string; +}; + +export type BillingDetails = { + address: string; + address2: string; + city: string; + state: string; + country: string; + postalCode: string; + name: string; + card: StripeCardElement; + agreedTerms?: boolean; +}; + +export const areBillingDetailsValid = ( + billingDetails: BillingDetails | null | undefined, +): boolean => { + if (billingDetails == null) { + return false; + } + + return Boolean( + billingDetails.address && + billingDetails.city && + billingDetails.state && + billingDetails.country && + billingDetails.postalCode && + billingDetails.name, + ); +}; diff --git a/utils/constants.jsx b/utils/constants.jsx index 03e643a33dd6..839e41d6a3b1 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -273,6 +273,7 @@ export const ModalIdentifiers = { REMOVE_NEXT_STEPS_MODAL: 'remove_next_steps_modal', MORE_CHANNELS: 'more_channels', NEW_CHANNEL_FLOW: 'new_channel_flow', + CLOUD_PURCHASE: 'cloud_purchase', }; export const UserStatuses = { @@ -1560,3 +1561,4 @@ t('suggestion.archive'); t('suggestion.mention.groups'); export default Constants; + diff --git a/utils/countries.ts b/utils/countries.ts new file mode 100644 index 000000000000..5aadd18af1c6 --- /dev/null +++ b/utils/countries.ts @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {getData} from 'country-list'; + +type Country = { + name: string; + code: string; +} + +export const COUNTRIES = getData().sort((a: Country, b: Country) => (a.name > b.name ? 1 : -1)); diff --git a/utils/states.ts b/utils/states.ts new file mode 100644 index 000000000000..091b4fe46795 --- /dev/null +++ b/utils/states.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export interface StateCode { + code: string; + name: string; +} + +export const US_STATES = [ + {code: 'AL', name: 'Alabama'}, + {code: 'AK', name: 'Alaska'}, + {code: 'AZ', name: 'Arizona'}, + {code: 'AR', name: 'Arkansas'}, + {code: 'CA', name: 'California'}, + {code: 'CO', name: 'Colorado'}, + {code: 'CT', name: 'Connecticut'}, + {code: 'DE', name: 'Delaware'}, + {code: 'DC', name: 'District of Columbia'}, + {code: 'FL', name: 'Florida'}, + {code: 'GA', name: 'Georgia'}, + {code: 'HI', name: 'Hawaii'}, + {code: 'ID', name: 'Idaho'}, + {code: 'IL', name: 'Illinois'}, + {code: 'IN', name: 'Indiana'}, + {code: 'IA', name: 'Iowa'}, + {code: 'KS', name: 'Kansas'}, + {code: 'KY', name: 'Kentucky'}, + {code: 'LA', name: 'Louisiana'}, + {code: 'ME', name: 'Maine'}, + {code: 'MD', name: 'Maryland'}, + {code: 'MA', name: 'Massachusetts'}, + {code: 'MI', name: 'Michigan'}, + {code: 'MN', name: 'Minnesota'}, + {code: 'MS', name: 'Mississippi'}, + {code: 'MO', name: 'Missouri'}, + {code: 'MT', name: 'Montana'}, + {code: 'NE', name: 'Nebraska'}, + {code: 'NV', name: 'Nevada'}, + {code: 'NH', name: 'New Hampshire'}, + {code: 'NJ', name: 'New Jersey'}, + {code: 'NM', name: 'New Mexico'}, + {code: 'NY', name: 'New York'}, + {code: 'NC', name: 'North Carolina'}, + {code: 'ND', name: 'North Dakota'}, + {code: 'OH', name: 'Ohio'}, + {code: 'OK', name: 'Oklahoma'}, + {code: 'OR', name: 'Oregon'}, + {code: 'PA', name: 'Pennsylvania'}, + {code: 'PR', name: 'Puerto Rico'}, + {code: 'RI', name: 'Rhode Island'}, + {code: 'SC', name: 'South Carolina'}, + {code: 'SD', name: 'South Dakota'}, + {code: 'TN', name: 'Tennessee'}, + {code: 'TX', name: 'Texas'}, + {code: 'UT', name: 'Utah'}, + {code: 'VT', name: 'Vermont'}, + {code: 'VA', name: 'Virginia'}, + {code: 'WA', name: 'Washington'}, + {code: 'WV', name: 'West Virginia'}, + {code: 'WI', name: 'Wisconsin'}, + {code: 'WY', name: 'Wyoming'}, +] as StateCode[]; + +export const CA_PROVINCES = [ + {code: 'AB', name: 'Alberta'}, + {code: 'BC', name: 'British Columbia'}, + {code: 'MB', name: 'Manitoba'}, + {code: 'NB', name: 'New Brunswick'}, + {code: 'NL', name: 'Newfoundland and Labrador'}, + {code: 'NT', name: 'Northwest Territories'}, + {code: 'NS', name: 'Nova Scotia'}, + {code: 'NU', name: 'Nunavut'}, + {code: 'ON', name: 'Ontario'}, + {code: 'PE', name: 'Prince Edward Island'}, + {code: 'QC', name: 'Quebec'}, + {code: 'SK', name: 'Saskatchewan'}, + {code: 'YT', name: 'Yukon Territory'}, +] as StateCode[]; diff --git a/utils/utils.jsx b/utils/utils.jsx index 64f6a7626a93..00492a675e4f 100644 --- a/utils/utils.jsx +++ b/utils/utils.jsx @@ -19,6 +19,8 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {getCurrentTeamId, getCurrentRelativeTeamUrl, getTeam} from 'mattermost-redux/selectors/entities/teams'; import cssVars from 'css-vars-ponyfill'; +import moment from 'moment'; + import {browserHistory} from 'utils/browser_history'; import {searchForTerm} from 'actions/post_actions'; import Constants, {FileTypes, UserStatuses} from 'utils/constants.jsx'; @@ -1895,3 +1897,8 @@ export function adjustSelection(inputBox, e) { setSelectionRange(inputBox, selectionStart + 1, selectionEnd - 1); } } + +export function getNextBillingDate() { + const nextBillingDate = moment().add(1, 'months').startOf('month'); + return nextBillingDate.format('MMM D, YYYY'); +} diff --git a/webpack.config.js b/webpack.config.js index 4c0f91078795..dd3203509392 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -275,7 +275,7 @@ var config = { meta: { csp: { 'http-equiv': 'Content-Security-Policy', - content: 'script-src \'self\' cdn.rudderlabs.com/' + CSP_UNSAFE_EVAL_IF_DEV, + content: 'script-src \'self\' cdn.rudderlabs.com/ js.stripe.com/v3 ' + CSP_UNSAFE_EVAL_IF_DEV, }, }, }),