[CLD-6219] Add Company Name field to Purchase Modal (#24428)

Automatic Merge
Этот коммит содержится в:
Nick Misasi
2023-09-06 16:27:35 -04:00
коммит произвёл GitHub
родитель b540ed5d30
Коммит fcfcbd9909
8 изменённых файлов: 259 добавлений и 286 удалений

Просмотреть файл

@@ -270,6 +270,7 @@ type SubscriptionChange struct {
Seats int `json:"seats"` Seats int `json:"seats"`
Feedback *Feedback `json:"downgrade_feedback"` Feedback *Feedback `json:"downgrade_feedback"`
ShippingAddress *Address `json:"shipping_address"` ShippingAddress *Address `json:"shipping_address"`
Customer *CloudCustomerInfo `json:"customer"`
} }
type FilesLimits struct { type FilesLimits struct {

Просмотреть файл

@@ -4,7 +4,7 @@
import type {Stripe} from '@stripe/stripe-js'; import type {Stripe} from '@stripe/stripe-js';
import {getCode} from 'country-list'; 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 {CloudTypes} from 'mattermost-redux/action_types';
import {getCloudCustomer, getCloudProducts, getCloudSubscription, getInvoices} from 'mattermost-redux/actions/cloud'; import {getCloudCustomer, getCloudProducts, getCloudSubscription, getInvoices} from 'mattermost-redux/actions/cloud';
@@ -89,6 +89,7 @@ export function subscribeCloudSubscription(
shippingAddress: Address = getBlankAddressWithCountry(), shippingAddress: Address = getBlankAddressWithCountry(),
seats = 0, seats = 0,
downgradeFeedback?: Feedback, downgradeFeedback?: Feedback,
customerPatch?: CloudCustomerPatch,
) { ) {
return async () => { return async () => {
try { try {
@@ -97,6 +98,7 @@ export function subscribeCloudSubscription(
shippingAddress, shippingAddress,
seats, seats,
downgradeFeedback, downgradeFeedback,
customerPatch,
); );
return {data: subscription}; return {data: subscription};

Просмотреть файл

@@ -4,11 +4,10 @@
import type { import type {
StripeCardElementChangeEvent, StripeCardElementChangeEvent,
} from '@stripe/stripe-js'; } from '@stripe/stripe-js';
import {getName} from 'country-list'; import React, {useRef} from 'react';
import React from 'react'; import {FormattedMessage, useIntl} from 'react-intl';
import {FormattedMessage} 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'; 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 Input from 'components/widgets/inputs/input/input';
import {COUNTRIES} from 'utils/countries'; import {COUNTRIES} from 'utils/countries';
import * as Utils from 'utils/utils';
import type {BillingDetails} from 'types/cloud/sku'; import type {BillingDetails} from 'types/cloud/sku';
@@ -37,6 +35,7 @@ type Props = {
onInputChange?: (billing: BillingDetails) => void; onInputChange?: (billing: BillingDetails) => void;
onInputBlur?: (billing: BillingDetails) => void; onInputBlur?: (billing: BillingDetails) => void;
buttonFooter?: JSX.Element; buttonFooter?: JSX.Element;
customer?: CloudCustomer | undefined;
}; };
type State = { type State = {
@@ -48,57 +47,27 @@ type State = {
postalCode: string; postalCode: string;
name: string; name: string;
changePaymentMethod: boolean; changePaymentMethod: boolean;
company_name: string;
} }
export default class PaymentForm extends React.PureComponent<Props, State> { const PaymentForm: React.FC<Props> = (props: Props) => {
static defaultProps = { const {className, paymentMethod, buttonFooter, theme} = props;
showSaveCard: false, const {formatMessage} = useIntl();
className: '', const cardRef = useRef<CardInputType>(null);
};
cardRef: React.RefObject<CardInputType>; const [state, setState] = React.useState<State>({
address: '',
public constructor(props: Props) { address2: '',
super(props); city: '',
state: '',
this.cardRef = React.createRef<CardInputType>(); country: '',
postalCode: '',
this.state = this.getResetState(props); name: '',
}
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, changePaymentMethod: paymentMethod == null,
}; company_name: props.customer?.name || '',
}; });
private handleInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => {
const target = event.target; const target = event.target;
const name = target.name; const name = target.name;
const value = target.value; const value = target.value;
@@ -107,68 +76,75 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
[name]: value, [name]: value,
} as unknown as Pick<State, keyof State>; } as unknown as Pick<State, keyof State>;
this.setState(newStateValue); setState({...state, ...newStateValue});
const {onInputChange} = this.props; const {onInputChange} = props;
if (onInputChange) { 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) => { const handleCardInputChange = (event: StripeCardElementChangeEvent) => {
if (this.props.onCardInputChange) { if (props.onCardInputChange) {
this.props.onCardInputChange(event); props.onCardInputChange(event);
} }
}; };
private handleStateChange = (stateValue: string) => { const handleStateChange = (stateValue: string) => {
const newStateValue = { const newStateValue = {
state: stateValue, state: stateValue,
} as unknown as Pick<State, keyof State>; } as unknown as Pick<State, keyof State>;
this.setState(newStateValue); setState({...state, ...newStateValue});
if (this.props.onInputChange) { if (props.onInputChange) {
this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); props.onInputChange({...state, ...newStateValue, card: cardRef.current?.getCard()} as BillingDetails);
} }
}; };
private handleCountryChange = (option: any) => { const handleCountryChange = (option: any) => {
const newStateValue = { const newStateValue = {
country: option.value, country: option.value,
} as unknown as Pick<State, keyof State>; } as unknown as Pick<State, keyof State>;
this.setState(newStateValue); setState({...state, ...newStateValue});
if (this.props.onInputChange) { if (props.onInputChange) {
this.props.onInputChange({...this.state, ...newStateValue, card: this.cardRef.current?.getCard()} as BillingDetails); props.onInputChange({...state, ...newStateValue, card: cardRef.current?.getCard()} as BillingDetails);
} }
}; };
private onBlur = () => { const onBlur = () => {
const {onInputBlur} = this.props; const {onInputBlur} = props;
if (onInputBlur) { 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<HTMLElement>) => { const changePaymentMethod = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault(); 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; let paymentDetails: JSX.Element;
if (changePaymentMethod) { if (state.changePaymentMethod) {
paymentDetails = ( paymentDetails = (
<React.Fragment> <React.Fragment>
<div className='form-row'> <div className='form-row'>
<CardInput <Input
forwardedRef={this.cardRef} name='company_name'
type='text'
value={state.company_name}
onChange={handleInputChange}
onBlur={onBlur}
placeholder={formatMessage({id: 'payment_form.company_name', defaultMessage: 'Company Name'})}
required={true} required={true}
onBlur={this.onBlur} />
onCardInputChange={this.handleCardInputChange} </div>
<div className='form-row'>
<CardInput
forwardedRef={cardRef}
required={true}
onBlur={onBlur}
onCardInputChange={handleCardInputChange}
theme={theme} theme={theme}
/> />
</div> </div>
@@ -176,13 +152,10 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
<Input <Input
name='name' name='name'
type='text' type='text'
value={this.state.name} value={state.name}
onChange={this.handleInputChange} onChange={handleInputChange}
onBlur={this.onBlur} onBlur={onBlur}
placeholder={Utils.localizeMessage( placeholder={formatMessage({id: 'payment_form.name_on_card', defaultMessage: 'Name on Card'})}
'payment_form.name_on_card',
'Name on Card',
)}
required={true} required={true}
/> />
</div> </div>
@@ -193,35 +166,26 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
/> />
</div> </div>
<DropdownInput <DropdownInput
onChange={this.handleCountryChange} onChange={handleCountryChange}
value={ value={
this.state.country ? {value: this.state.country, label: this.state.country} : undefined state.country ? {value: state.country, label: state.country} : undefined
} }
options={COUNTRIES.map((country) => ({ options={COUNTRIES.map((country) => ({
value: country.name, value: country.name,
label: country.name, label: country.name,
}))} }))}
legend={Utils.localizeMessage( legend={formatMessage({id: 'payment_form.country', defaultMessage: 'Country'})}
'payment_form.country', placeholder={formatMessage({id: 'payment_form.country', defaultMessage: 'Country'})}
'Country',
)}
placeholder={Utils.localizeMessage(
'payment_form.country',
'Country',
)}
name={'billing_dropdown'} name={'billing_dropdown'}
/> />
<div className='form-row'> <div className='form-row'>
<Input <Input
name='address' name='address'
type='text' type='text'
value={this.state.address} value={state.address}
onChange={this.handleInputChange} onChange={handleInputChange}
onBlur={this.onBlur} onBlur={onBlur}
placeholder={Utils.localizeMessage( placeholder={formatMessage({id: 'payment_form.address', defaultMessage: 'Address'})}
'payment_form.address',
'Address',
)}
required={true} required={true}
/> />
</div> </div>
@@ -229,54 +193,45 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
<Input <Input
name='address2' name='address2'
type='text' type='text'
value={this.state.address2} value={state.address2}
onChange={this.handleInputChange} onChange={handleInputChange}
onBlur={this.onBlur} onBlur={onBlur}
placeholder={Utils.localizeMessage( placeholder={formatMessage({id: 'payment_form.address_2', defaultMessage: 'Address 2'})}
'payment_form.address_2',
'Address 2',
)}
/> />
</div> </div>
<div className='form-row'> <div className='form-row'>
<Input <Input
name='city' name='city'
type='text' type='text'
value={this.state.city} value={state.city}
onChange={this.handleInputChange} onChange={handleInputChange}
onBlur={this.onBlur} onBlur={onBlur}
placeholder={Utils.localizeMessage( placeholder={formatMessage({id: 'payment_form.city', defaultMessage: 'City'})}
'payment_form.city',
'City',
)}
required={true} required={true}
/> />
</div> </div>
<div className='form-row'> <div className='form-row'>
<div className='form-row-third-1 selector second-dropdown-sibling-wrapper'> <div className='form-row-third-1 selector second-dropdown-sibling-wrapper'>
<StateSelector <StateSelector
country={this.state.country} country={state.country}
state={this.state.state} state={state.state}
onChange={this.handleStateChange} onChange={handleStateChange}
onBlur={this.onBlur} onBlur={onBlur}
/> />
</div> </div>
<div className='form-row-third-2'> <div className='form-row-third-2'>
<Input <Input
name='postalCode' name='postalCode'
type='text' type='text'
value={this.state.postalCode} value={state.postalCode}
onChange={this.handleInputChange} onChange={handleInputChange}
onBlur={this.onBlur} onBlur={onBlur}
placeholder={Utils.localizeMessage( placeholder={formatMessage({id: 'payment_form.zipcode', defaultMessage: 'Zip/Postal Code'})}
'payment_form.zipcode',
'Zip/Postal Code',
)}
required={true} required={true}
/> />
</div> </div>
</div> </div>
{changePaymentMethod ? buttonFooter : null} {state.changePaymentMethod ? buttonFooter : null}
</React.Fragment> </React.Fragment>
); );
} else { } else {
@@ -306,15 +261,15 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
defaultMessage='No billing address added' defaultMessage='No billing address added'
/> />
</i>); </i>);
if (this.state.state) { if (state.state) {
addressDetails = ( addressDetails = (
<React.Fragment> <React.Fragment>
{this.state.address} {state.address}
{this.state.address2} {state.address2}
<br/> <br/>
{`${this.state.city}, ${this.state.state}, ${this.state.country}`} {`${state.city}, ${state.state}, ${state.country}`}
<br/> <br/>
{this.state.postalCode} {state.postalCode}
</React.Fragment> </React.Fragment>
); );
} }
@@ -345,7 +300,7 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
{cardContent} {cardContent}
<button <button
className='Form-btn-link PaymentForm-change' className='Form-btn-link PaymentForm-change'
onClick={this.changePaymentMethod} onClick={changePaymentMethod}
> >
<FormattedMessage <FormattedMessage
id='payment_form.change_payment_method' id='payment_form.change_payment_method'
@@ -379,5 +334,10 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
{paymentDetails} {paymentDetails}
</form> </form>
); );
} };
}
PaymentForm.defaultProps = {
className: '',
};
export default PaymentForm;

Просмотреть файл

@@ -8,7 +8,7 @@ import type {IntlShape} from 'react-intl';
import {withRouter} from 'react-router-dom'; import {withRouter} from 'react-router-dom';
import type {RouteComponentProps} 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 {Team} from '@mattermost/types/teams';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -48,7 +48,7 @@ type Props = RouteComponentProps & {
cwsMockMode: boolean cwsMockMode: boolean
) => Promise<boolean | null>; ) => Promise<boolean | null>;
subscribeCloudSubscription: subscribeCloudSubscription:
| ((productId: string, shippingAddress: Address, seats?: number, downgradeFeedback?: Feedback) => Promise<ActionResult<Subscription, ComplianceError>>) | ((productId: string, shippingAddress: Address, seats?: number, downgradeFeedback?: Feedback, customerPatch?: CloudCustomerPatch) => Promise<ActionResult<Subscription, ComplianceError>>)
| null; | null;
onBack: () => void; onBack: () => void;
onClose: () => void; onClose: () => void;
@@ -139,7 +139,10 @@ class ProcessPaymentSetup extends React.PureComponent<Props, State> {
} }
if (subscribeCloudSubscription) { 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 // the action subscribeCloudSubscription returns a true boolean when successful and an error when it fails
if (result.error) { if (result.error) {

Просмотреть файл

@@ -836,6 +836,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
onCardInputChange={this.handleCardInputChange} onCardInputChange={this.handleCardInputChange}
initialBillingDetails={initialBillingDetails} initialBillingDetails={initialBillingDetails}
theme={this.props.theme} theme={this.props.theme}
customer={this.props.customer}
/> />
) : ( ) : (
<div className='PaymentDetails'> <div className='PaymentDetails'>

Просмотреть файл

@@ -4248,6 +4248,7 @@
"payment_form.billing_address": "Billing address", "payment_form.billing_address": "Billing address",
"payment_form.change_payment_method": "Change Payment Method", "payment_form.change_payment_method": "Change Payment Method",
"payment_form.city": "City", "payment_form.city": "City",
"payment_form.company_name": "Company Name",
"payment_form.country": "Country", "payment_form.country": "Country",
"payment_form.credit_card": "Credit Card", "payment_form.credit_card": "Credit Card",
"payment_form.gather_wire_transfer_intent": "Looking for other payment options?", "payment_form.gather_wire_transfer_intent": "Looking for other payment options?",

Просмотреть файл

@@ -18,6 +18,7 @@ export type BillingDetails = {
name: string; name: string;
card: StripeCardElement; card: StripeCardElement;
agreedTerms?: boolean; agreedTerms?: boolean;
company_name?: string;
}; };
export const areBillingDetailsValid = ( export const areBillingDetailsValid = (

Просмотреть файл

@@ -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 = { const body = {
product_id: productId, product_id: productId,
seats, seats,
@@ -3879,6 +3879,10 @@ export default class Client4 {
if (shippingAddress) { if (shippingAddress) {
body.shipping_address = shippingAddress; body.shipping_address = shippingAddress;
} }
if (customerPatch) {
body.customer = customerPatch;
}
return this.doFetch<Subscription>( return this.doFetch<Subscription>(
`${this.getCloudRoute()}/subscription`, `${this.getCloudRoute()}/subscription`,
{method: 'put', body: JSON.stringify(body)}, {method: 'put', body: JSON.stringify(body)},