diff --git a/server/public/model/cloud.go b/server/public/model/cloud.go index 97b27bcd32..b81bf91f03 100644 --- a/server/public/model/cloud.go +++ b/server/public/model/cloud.go @@ -266,10 +266,11 @@ type CloudWorkspaceOwner struct { } type SubscriptionChange struct { - ProductID string `json:"product_id"` - Seats int `json:"seats"` - Feedback *Feedback `json:"downgrade_feedback"` - ShippingAddress *Address `json:"shipping_address"` + ProductID string `json:"product_id"` + Seats int `json:"seats"` + Feedback *Feedback `json:"downgrade_feedback"` + ShippingAddress *Address `json:"shipping_address"` + Customer *CloudCustomerInfo `json:"customer"` } type FilesLimits struct { diff --git a/webapp/channels/src/actions/cloud.tsx b/webapp/channels/src/actions/cloud.tsx index 6a5d779721..79489ce5f9 100644 --- a/webapp/channels/src/actions/cloud.tsx +++ b/webapp/channels/src/actions/cloud.tsx @@ -4,7 +4,7 @@ import type {Stripe} from '@stripe/stripe-js'; import {getCode} from 'country-list'; -import type {Address, Feedback, WorkspaceDeletionRequest} from '@mattermost/types/cloud'; +import type {Address, CloudCustomerPatch, Feedback, WorkspaceDeletionRequest} from '@mattermost/types/cloud'; import {CloudTypes} from 'mattermost-redux/action_types'; import {getCloudCustomer, getCloudProducts, getCloudSubscription, getInvoices} from 'mattermost-redux/actions/cloud'; @@ -89,6 +89,7 @@ export function subscribeCloudSubscription( shippingAddress: Address = getBlankAddressWithCountry(), seats = 0, downgradeFeedback?: Feedback, + customerPatch?: CloudCustomerPatch, ) { return async () => { try { @@ -97,6 +98,7 @@ export function subscribeCloudSubscription( shippingAddress, seats, downgradeFeedback, + customerPatch, ); return {data: subscription}; diff --git a/webapp/channels/src/components/payment_form/payment_form.tsx b/webapp/channels/src/components/payment_form/payment_form.tsx index 49794d27b1..146e59fdc9 100644 --- a/webapp/channels/src/components/payment_form/payment_form.tsx +++ b/webapp/channels/src/components/payment_form/payment_form.tsx @@ -4,11 +4,10 @@ import type { StripeCardElementChangeEvent, } from '@stripe/stripe-js'; -import {getName} from 'country-list'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import React, {useRef} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; -import type {PaymentMethod} from '@mattermost/types/cloud'; +import type {CloudCustomer, PaymentMethod} from '@mattermost/types/cloud'; import type {Theme} from 'mattermost-redux/selectors/entities/preferences'; @@ -16,7 +15,6 @@ import DropdownInput from 'components/dropdown_input'; import Input from 'components/widgets/inputs/input/input'; import {COUNTRIES} from 'utils/countries'; -import * as Utils from 'utils/utils'; import type {BillingDetails} from 'types/cloud/sku'; @@ -37,6 +35,7 @@ type Props = { onInputChange?: (billing: BillingDetails) => void; onInputBlur?: (billing: BillingDetails) => void; buttonFooter?: JSX.Element; + customer?: CloudCustomer | undefined; }; type State = { @@ -48,57 +47,27 @@ type State = { postalCode: string; name: string; changePaymentMethod: boolean; + company_name: string; } -export default class PaymentForm extends React.PureComponent { - static defaultProps = { - showSaveCard: false, - className: '', - }; +const PaymentForm: React.FC = (props: Props) => { + const {className, paymentMethod, buttonFooter, theme} = props; + const {formatMessage} = useIntl(); + const cardRef = useRef(null); - cardRef: React.RefObject; + const [state, setState] = React.useState({ + address: '', + address2: '', + city: '', + state: '', + country: '', + postalCode: '', + name: '', + changePaymentMethod: paymentMethod == null, + company_name: props.customer?.name || '', + }); - 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 handleInputChange = (event: React.ChangeEvent | React.ChangeEvent) => { const target = event.target; const name = target.name; const value = target.value; @@ -107,277 +76,268 @@ export default class PaymentForm extends React.PureComponent { [name]: value, } as unknown as Pick; - this.setState(newStateValue); + setState({...state, ...newStateValue}); - const {onInputChange} = this.props; + const {onInputChange} = props; if (onInputChange) { - onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + onInputChange({...state, ...newStateValue, card: cardRef.current?.getCard()} as BillingDetails); } }; - private handleCardInputChange = (event: StripeCardElementChangeEvent) => { - if (this.props.onCardInputChange) { - this.props.onCardInputChange(event); + const handleCardInputChange = (event: StripeCardElementChangeEvent) => { + if (props.onCardInputChange) { + props.onCardInputChange(event); } }; - private handleStateChange = (stateValue: string) => { + const handleStateChange = (stateValue: string) => { const newStateValue = { state: stateValue, } as unknown as Pick; - this.setState(newStateValue); + setState({...state, ...newStateValue}); - if (this.props.onInputChange) { - this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + if (props.onInputChange) { + props.onInputChange({...state, ...newStateValue, card: cardRef.current?.getCard()} as BillingDetails); } }; - private handleCountryChange = (option: any) => { + const handleCountryChange = (option: any) => { const newStateValue = { country: option.value, } as unknown as Pick; - this.setState(newStateValue); + setState({...state, ...newStateValue}); - if (this.props.onInputChange) { - this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); + if (props.onInputChange) { + props.onInputChange({...state, ...newStateValue, card: cardRef.current?.getCard()} as BillingDetails); } }; - private onBlur = () => { - const {onInputBlur} = this.props; + const onBlur = () => { + const {onInputBlur} = props; if (onInputBlur) { - onInputBlur({...this.state, card: this.cardRef.current?.getCard()} as BillingDetails); + onInputBlur({...state, card: cardRef.current?.getCard()} as BillingDetails); } }; - private changePaymentMethod = (event: React.MouseEvent) => { + const changePaymentMethod = (event: React.MouseEvent) => { event.preventDefault(); - this.setState({changePaymentMethod: true}); + setState({...state, changePaymentMethod: true}); }; - public render() { - const {className, paymentMethod, buttonFooter, theme} = 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'} + let paymentDetails: JSX.Element; + if (state.changePaymentMethod) { + paymentDetails = ( + +
+ -
- -
-
- -
-
- -
-
-
- -
-
- -
-
- {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 = ( - - + ({ + value: country.name, + label: country.name, + }))} + legend={formatMessage({id: 'payment_form.country', defaultMessage: 'Country'})} + placeholder={formatMessage({id: 'payment_form.country', defaultMessage: 'Country'})} + name={'billing_dropdown'} + /> +
+ +
+
+ +
+
+ +
+
+
+ - ); - if (this.state.state) { - addressDetails = ( - - {this.state.address} - {this.state.address2} -
- {`${this.state.city}, ${this.state.state}, ${this.state.country}`} -
- {this.state.postalCode} -
- ); - } +
+
+ +
+
+ {state.changePaymentMethod ? buttonFooter : null} + + ); + } else { + let cardContent: JSX.Element | null = null; - cardContent = ( + if (paymentMethod) { + let cardDetails = ( + + ); + if (paymentMethod.last_four) { + cardDetails = ( -
- {cardDetails} -
-
- {addressDetails} -
+ + {`Card ending in ${paymentMethod.last_four}`} +
+ {`Expires ${paymentMethod.exp_month}/${paymentMethod.exp_year}`} +
+ ); + } + let addressDetails = ( + + + ); + if (state.state) { + addressDetails = ( + + {state.address} + {state.address2} +
+ {`${state.city}, ${state.state}, ${state.country}`} +
+ {state.postalCode}
); } - paymentDetails = ( -
-
- + cardContent = ( + +
+ {cardDetails}
- {cardContent} - -
+
+ {addressDetails} +
+ ); } - return ( -
- } - /> -
+
- {paymentDetails} - + {cardContent} + +
); } -} + + return ( +
+ } + /> +
+ +
+ {paymentDetails} + + ); +}; + +PaymentForm.defaultProps = { + className: '', +}; + +export default PaymentForm; diff --git a/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx b/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx index 77773c22e1..e0396864f4 100644 --- a/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx +++ b/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx @@ -8,7 +8,7 @@ import type {IntlShape} from 'react-intl'; import {withRouter} from 'react-router-dom'; import type {RouteComponentProps} from 'react-router-dom'; -import type {Address, Feedback, Product} from '@mattermost/types/cloud'; +import type {Address, CloudCustomerPatch, Feedback, Product} from '@mattermost/types/cloud'; import type {Team} from '@mattermost/types/teams'; import type {ActionResult} from 'mattermost-redux/types/actions'; @@ -48,7 +48,7 @@ type Props = RouteComponentProps & { cwsMockMode: boolean ) => Promise; subscribeCloudSubscription: - | ((productId: string, shippingAddress: Address, seats?: number, downgradeFeedback?: Feedback) => Promise>) + | ((productId: string, shippingAddress: Address, seats?: number, downgradeFeedback?: Feedback, customerPatch?: CloudCustomerPatch) => Promise>) | null; onBack: () => void; onClose: () => void; @@ -139,7 +139,10 @@ class ProcessPaymentSetup extends React.PureComponent { } if (subscribeCloudSubscription) { - const result = await subscribeCloudSubscription(this.props.selectedProduct?.id as string, this.props.shippingAddress as Address, this.props.usersCount); + const customerPatch = { + name: billingDetails?.company_name, + } as CloudCustomerPatch; + const result = await subscribeCloudSubscription(this.props.selectedProduct?.id as string, this.props.shippingAddress as Address, this.props.usersCount, undefined, customerPatch); // the action subscribeCloudSubscription returns a true boolean when successful and an error when it fails if (result.error) { diff --git a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx index 88b879e003..b952e04ce2 100644 --- a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx +++ b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx @@ -836,6 +836,7 @@ class PurchaseModal extends React.PureComponent { onCardInputChange={this.handleCardInputChange} initialBillingDetails={initialBillingDetails} theme={this.props.theme} + customer={this.props.customer} /> ) : (
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 2cfad05244..89e4baa3e8 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4248,6 +4248,7 @@ "payment_form.billing_address": "Billing address", "payment_form.change_payment_method": "Change Payment Method", "payment_form.city": "City", + "payment_form.company_name": "Company Name", "payment_form.country": "Country", "payment_form.credit_card": "Credit Card", "payment_form.gather_wire_transfer_intent": "Looking for other payment options?", diff --git a/webapp/channels/src/types/cloud/sku.ts b/webapp/channels/src/types/cloud/sku.ts index 797c329c1f..bbe754bce6 100644 --- a/webapp/channels/src/types/cloud/sku.ts +++ b/webapp/channels/src/types/cloud/sku.ts @@ -18,6 +18,7 @@ export type BillingDetails = { name: string; card: StripeCardElement; agreedTerms?: boolean; + company_name?: string; }; export const areBillingDetailsValid = ( diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 5d7f09bcde..69047ff848 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -3870,7 +3870,7 @@ export default class Client4 { ); } - subscribeCloudProduct = (productId: string, shippingAddress?: Address, seats = 0, downgradeFeedback?: Feedback) => { + subscribeCloudProduct = (productId: string, shippingAddress?: Address, seats = 0, downgradeFeedback?: Feedback, customerPatch?: CloudCustomerPatch) => { const body = { product_id: productId, seats, @@ -3879,6 +3879,10 @@ export default class Client4 { if (shippingAddress) { body.shipping_address = shippingAddress; } + + if (customerPatch) { + body.customer = customerPatch; + } return this.doFetch( `${this.getCloudRoute()}/subscription`, {method: 'put', body: JSON.stringify(body)},