* add shipping address to self hosted signup model
* purchase modals allow scroll
* fix z-index issues with payment modal dropdowns
Этот коммит содержится в:
Nathaniel Allred
2023-03-24 14:46:22 -05:00
коммит произвёл GitHub
родитель 379dbb1ca8
Коммит 9105077ee1
15 изменённых файлов: 438 добавлений и 171 удалений

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

@@ -21,10 +21,11 @@ type BootstrapSelfHostedSignupResponseInternal struct {
// email contained in token, so not in the request body.
type SelfHostedCustomerForm struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
BillingAddress *Address `json:"billing_address"`
Organization string `json:"organization"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
BillingAddress *Address `json:"billing_address"`
ShippingAddress *Address `json:"shipping_address"`
Organization string `json:"organization"`
}
type SelfHostedConfirmPaymentMethodRequest struct {

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

@@ -0,0 +1,37 @@
.shipping-address-section {
display: flex;
align-content: flex-start;
padding-bottom: 24px;
font-weight: normal;
button.no-style {
padding-left: 0;
border: none;
background: transparent;
outline: unset;
text-align: left;
&:focus {
outline: unset;
}
}
#address-same-than-billing-address {
width: 17px;
height: 17px;
flex-shrink: 0;
}
.Form-checkbox-label {
padding-left: 12px;
cursor: default;
font-family: 'Open Sans', sans-serif;
vertical-align: middle;
}
.billing_address_btn_text {
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-weight: bold;
}
}

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

@@ -0,0 +1,45 @@
// 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 './choose_different_shipping.scss';
interface Props {
shippingIsSame: boolean;
setShippingIsSame: (different: boolean) => void;
}
export default function ChooseDifferentShipping(props: Props) {
const intl = useIntl();
const toggle = () => props.setShippingIsSame(!props.shippingIsSame);
return (
<div className='shipping-address-section'>
<input
id='address-same-than-billing-address'
className='Form-checkbox-input'
name='terms'
type='checkbox'
checked={props.shippingIsSame}
onChange={toggle}
/>
<span className='Form-checkbox-label'>
<button
onClick={toggle}
type='button'
className='no-style'
>
<span className='billing_address_btn_text'>
{intl.formatMessage({
id: 'admin.billing.subscription.complianceScreenShippingSameAsBilling',
defaultMessage:
'My shipping address is the same as my billing address',
})}
</span>
</button>
</span>
</div>
);
}

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

@@ -1,5 +1,7 @@
$dropdown_input_index: 999999;
.DropdownInput {
z-index: 999999;
z-index: $dropdown_input_index;
&.Input_container {
margin-top: 20px;
@@ -37,7 +39,7 @@
}
.DropdownInput__option > div {
z-index: 999999;
z-index: $dropdown_input_index;
padding: 10px 24px;
cursor: pointer;
line-height: 16px;
@@ -51,3 +53,33 @@
.DropdownInput__option.focused > div {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
.second-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 1;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 1;
}
}
.third-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 2;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 2;
}
}
.fourth-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 3;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 3;
}
}

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

@@ -61,25 +61,27 @@ const AddressForm = (props: AddressFormProps) => {
{...props.title}
/>
</div>
<DropdownInput
onChange={handleCountryChange}
value={
props.address.country ? {value: props.address.country, label: props.address.country} : undefined
}
options={COUNTRIES.map((country) => ({
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'}
/>
<div className='third-dropdown-sibling-wrapper'>
<DropdownInput
onChange={handleCountryChange}
value={
props.address.country ? {value: props.address.country, label: props.address.country} : undefined
}
options={COUNTRIES.map((country) => ({
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'}
/>
</div>
<div className='form-row'>
<Input
name='address'
@@ -122,7 +124,7 @@ const AddressForm = (props: AddressFormProps) => {
/>
</div>
<div className='form-row'>
<div className='form-row-third-1 selector'>
<div className='form-row-third-1 selector fourth-dropdown-sibling-wrapper'>
<StateSelector
country={props.address.country}
state={props.address.state}

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

@@ -12,7 +12,6 @@
.form-row-third-1 {
.DropdownInput {
z-index: 99999;
margin-top: 0;
}
@@ -37,7 +36,6 @@
.DropdownInput {
position: relative;
z-index: 999999;
height: 36px;
margin-bottom: 24px;
font-weight: normal;

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

@@ -251,7 +251,7 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
/>
</div>
<div className='form-row'>
<div className='form-row-third-1 selector'>
<div className='form-row-third-1 selector second-dropdown-sibling-wrapper'>
<StateSelector
country={this.state.country}
state={this.state.state}

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

@@ -2,42 +2,8 @@
overflow: hidden;
height: 100%;
.shipping-address-section {
display: flex;
align-content: center;
padding: 0 96px;
padding-bottom: 28px;
font-weight: normal;
button.no-style {
padding-left: 0;
background: transparent;
outline: unset;
&:focus {
outline: unset;
}
}
#address-same-than-billing-address {
width: 20px;
height: 20px;
margin-top: auto;
margin-bottom: auto;
}
.Form-checkbox-label {
padding-left: 12px;
cursor: default;
font-family: 'Open Sans', sans-serif;
vertical-align: middle;
}
.billing_address_btn_text {
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-weight: normal;
}
& &__purchase-body {
overflow-y: auto;
}
>div {

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

@@ -6,6 +6,7 @@
import React, {ReactNode} from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classnames from 'classnames';
import {Stripe, StripeCardElementChangeEvent} from '@stripe/stripe-js';
import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects
import {Elements} from '@stripe/react-stripe-js';
@@ -812,7 +813,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
}
return (
<div className={this.state.processing ? 'processing' : ''}>
<div className={classnames('PurchaseModal__purchase-body', {processing: this.state.processing})}>
<div className='LHS'>
<h2 className='title'>{title}</h2>
<UpgradeSvg

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

@@ -0,0 +1,132 @@
// 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 classNames from 'classnames';
import {COUNTRIES} from 'utils/countries';
import DropdownInput from 'components/dropdown_input';
import Input from 'components/widgets/inputs/input/input';
import StateSelector from 'components/payment_form/state_selector';
interface Props {
type: 'shipping' | 'billing';
testPrefix?: string;
country: string;
changeCountry: (option: {value: string}) => void;
address: string;
changeAddress: (e: React.ChangeEvent<HTMLInputElement>) => void;
address2: string;
changeAddress2: (e: React.ChangeEvent<HTMLInputElement>) => void;
city: string;
changeCity: (e: React.ChangeEvent<HTMLInputElement>) => void;
state: string;
changeState: (postalCode: string) => void;
postalCode: string;
changePostalCode: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function Address(props: Props) {
const testPrefix = props.testPrefix || 'selfHostedPurchase';
const intl = useIntl();
let countrySelectorId = `${testPrefix}CountrySelector`;
let stateSelectorId = `${testPrefix}StateSelector`;
if (props.type === 'shipping') {
countrySelectorId += '_Shipping';
stateSelectorId += '_Shipping';
}
return (
<>
<div className={classNames({'third-dropdown-sibling-wrapper': props.type === 'shipping'})}>
<DropdownInput
testId={countrySelectorId}
onChange={props.changeCountry}
value={
props.country ? {value: props.country, label: props.country} : undefined
}
options={COUNTRIES.map((country) => ({
value: country.name,
label: country.name,
}))}
legend={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
placeholder={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
name={'billing_dropdown'}
/>
</div>
<div className='form-row'>
<Input
name='address'
type='text'
value={props.address}
onChange={props.changeAddress}
placeholder={intl.formatMessage({
id: 'payment_form.address',
defaultMessage: 'Address',
})}
required={true}
/>
</div>
<div className='form-row'>
<Input
name='address2'
type='text'
value={props.address2}
onChange={props.changeAddress2}
placeholder={intl.formatMessage({
id: 'payment_form.address_2',
defaultMessage: 'Address 2',
})}
/>
</div>
<div className='form-row'>
<Input
name='city'
type='text'
value={props.city}
onChange={props.changeCity}
placeholder={intl.formatMessage({
id: 'payment_form.city',
defaultMessage: 'City',
})}
required={true}
/>
</div>
<div className='form-row'>
<div className={classNames('form-row-third-1', {'second-dropdown-sibling-wrapper': props.type === 'billing', 'fourth-dropdown-sibling-wrapper': props.type === 'shipping'})}>
<StateSelector
testId={stateSelectorId}
country={props.country}
state={props.state}
onChange={props.changeState}
/>
</div>
<div className='form-row-third-2'>
<Input
name='postalCode'
type='text'
value={props.postalCode}
onChange={props.changePostalCode}
placeholder={intl.formatMessage({
id: 'payment_form.zipcode',
defaultMessage: 'Zip/Postal Code',
})}
required={true}
/>
</div>
</div>
</>
);
}

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

@@ -310,6 +310,15 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
state: 'string',
country: 'string',
postalCode: '12345',
shippingSame: true,
shippingAddress: '',
shippingAddress2: '',
shippingCity: '',
shippingState: '',
shippingCountry: '',
shippingPostalCode: '',
cardName: 'string',
organization: 'string',
agreedTerms: true,
@@ -361,6 +370,21 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
expect(canSubmit(state, SelfHostedSignupProgress.CREATED_CUSTOMER)).toBe(false);
expect(canSubmit(state, SelfHostedSignupProgress.CREATED_INTENT)).toBe(false);
});
it('if shipping address different and is not filled, can not submit', () => {
const state = makeHappyPathState();
state.shippingSame = false;
expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(false);
state.shippingAddress = 'more shipping info';
state.shippingAddress2 = 'more shipping info';
state.shippingCity = 'more shipping info';
state.shippingState = 'more shipping info';
state.shippingCountry = 'more shipping info';
state.shippingPostalCode = 'more shipping info';
expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(true);
});
it('if card number missing and card has not been confirmed, can not submit', () => {
const state = makeHappyPathState();
state.cardFilled = false;

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

@@ -26,8 +26,6 @@ import {GlobalState} from 'types/store';
import {isModalOpen} from 'selectors/views/modals';
import {isDevModeEnabled} from 'selectors/general';
import {COUNTRIES} from 'utils/countries';
import {
ModalIdentifiers,
StatTypes,
@@ -35,8 +33,6 @@ import {
} from 'utils/constants';
import CardInput, {CardInputType} from 'components/payment_form/card_input';
import StateSelector from 'components/payment_form/state_selector';
import DropdownInput from 'components/dropdown_input';
import BackgroundSvg from 'components/common/svg_images_components/background_svg';
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
@@ -47,6 +43,7 @@ import RootPortal from 'components/root_portal';
import useLoadStripe from 'components/common/hooks/useLoadStripe';
import useControlSelfHostedPurchaseModal from 'components/common/hooks/useControlSelfHostedPurchaseModal';
import useFetchStandardAnalytics from 'components/common/hooks/useFetchStandardAnalytics';
import ChooseDifferentShipping from 'components/choose_different_shipping';
import {ValueOf} from '@mattermost/types/utilities';
import {UserProfile} from '@mattermost/types/users';
@@ -64,6 +61,7 @@ import SuccessPage from './success_page';
import SelfHostedCard from './self_hosted_card';
import StripeProvider from './stripe_provider';
import Terms from './terms';
import Address from './address';
import useNoEscape from './useNoEscape';
import {SetPrefix, UnionSetActions} from './types';
@@ -73,12 +71,24 @@ import './self_hosted_purchase_modal.scss';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants';
export interface State {
// billing address
address: string;
address2: string;
city: string;
state: string;
country: string;
postalCode: string;
// shipping address
shippingSame: boolean;
shippingAddress: string;
shippingAddress2: string;
shippingCity: string;
shippingState: string;
shippingCountry: string;
shippingPostalCode: string;
cardName: string;
organization: string;
agreedTerms: boolean;
@@ -113,6 +123,15 @@ export function makeInitialState(): State {
state: '',
country: '',
postalCode: '',
shippingSame: true,
shippingAddress: '',
shippingAddress2: '',
shippingCity: '',
shippingState: '',
shippingCountry: '',
shippingPostalCode: '',
cardName: '',
organization: '',
agreedTerms: false,
@@ -170,8 +189,18 @@ const simpleSetters: Array<Extract<keyof State, string>> = [
'address2',
'city',
'country',
'postalCode',
'state',
'postalCode',
// shipping address
'shippingSame',
'shippingAddress',
'shippingAddress2',
'shippingCity',
'shippingState',
'shippingCountry',
'shippingPostalCode',
'agreedTerms',
'cardFilled',
'cardName',
@@ -220,7 +249,7 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
return false;
}
const validAddress = Boolean(
let validAddress = Boolean(
state.organization &&
state.address &&
state.city &&
@@ -228,6 +257,16 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
state.postalCode &&
state.country,
);
if (!state.shippingSame) {
validAddress = validAddress && Boolean(
state.shippingAddress &&
state.shippingCity &&
state.shippingState &&
state.shippingPostalCode &&
state.shippingCountry,
);
}
const validCard = Boolean(
state.cardName &&
state.cardFilled,
@@ -366,16 +405,25 @@ export default function SelfHostedPurchaseModal(props: Props) {
try {
const [firstName, lastName] = inferNames(user, state.cardName);
const billingAddress = {
city: state.city,
country: state.country,
line1: state.address,
line2: state.address2,
postal_code: state.postalCode,
state: state.state,
};
signupCustomerResult = await Client4.createCustomerSelfHostedSignup({
first_name: firstName,
last_name: lastName,
billing_address: {
city: state.city,
country: state.country,
line1: state.address,
line2: state.address2,
postal_code: state.postalCode,
state: state.state,
billing_address: billingAddress,
shipping_address: state.shippingSame ? billingAddress : {
city: state.shippingCity,
country: state.shippingCountry,
line1: state.shippingAddress,
line2: state.shippingAddress2,
postal_code: state.shippingPostalCode,
state: state.shippingState,
},
organization: state.organization,
});
@@ -586,99 +634,76 @@ export default function SelfHostedPurchaseModal(props: Props) {
defaultMessage='Billing address'
/>
</div>
<DropdownInput
testId='selfHostedPurchaseCountrySelector'
onChange={(option: {value: string}) => {
<Address
type='billing'
country={state.country}
changeCountry={(option) => {
dispatch({type: 'set_country', data: option.value});
}}
value={
state.country ? {value: state.country, label: state.country} : undefined
}
options={COUNTRIES.map((country) => ({
value: country.name,
label: country.name,
}))}
legend={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
placeholder={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
name={'billing_dropdown'}
address={state.address}
changeAddress={(e) => {
dispatch({type: 'set_address', data: e.target.value});
}}
address2={state.address2}
changeAddress2={(e) => {
dispatch({type: 'set_address2', data: e.target.value});
}}
city={state.city}
changeCity={(e) => {
dispatch({type: 'set_city', data: e.target.value});
}}
state={state.state}
changeState={(state: string) => {
dispatch({type: 'set_state', data: state});
}}
postalCode={state.postalCode}
changePostalCode={(e) => {
dispatch({type: 'set_postalCode', data: e.target.value});
}}
/>
<div className='form-row'>
<Input
name='address'
type='text'
value={state.address}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: 'set_address', data: e.target.value});
}}
placeholder={intl.formatMessage({
id: 'payment_form.address',
defaultMessage: 'Address',
})}
required={true}
/>
</div>
<div className='form-row'>
<Input
name='address2'
type='text'
value={state.address2}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: 'set_address2', data: e.target.value});
}}
placeholder={intl.formatMessage({
id: 'payment_form.address_2',
defaultMessage: 'Address 2',
})}
/>
</div>
<div className='form-row'>
<Input
name='city'
type='text'
value={state.city}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: 'set_city', data: e.target.value});
}}
placeholder={intl.formatMessage({
id: 'payment_form.city',
defaultMessage: 'City',
})}
required={true}
/>
</div>
<div className='form-row'>
<div className='form-row-third-1'>
<StateSelector
testId='selfHostedPurchaseStateSelector'
country={state.country}
state={state.state}
onChange={(state: string) => {
dispatch({type: 'set_state', data: state});
<ChooseDifferentShipping
shippingIsSame={state.shippingSame}
setShippingIsSame={(val: boolean) => {
dispatch({type: 'set_shippingSame', data: val});
}}
/>
{!state.shippingSame && (
<>
<div className='section-title'>
<FormattedMessage
id='payment_form.shipping_address'
defaultMessage='Shipping Address'
/>
</div>
<Address
type='shipping'
country={state.shippingCountry}
changeCountry={(option) => {
dispatch({type: 'set_shippingCountry', data: option.value});
}}
address={state.shippingAddress}
changeAddress={(e) => {
dispatch({type: 'set_shippingAddress', data: e.target.value});
}}
address2={state.shippingAddress2}
changeAddress2={(e) => {
dispatch({type: 'set_shippingAddress2', data: e.target.value});
}}
city={state.shippingCity}
changeCity={(e) => {
dispatch({type: 'set_shippingCity', data: e.target.value});
}}
state={state.shippingState}
changeState={(state: string) => {
dispatch({type: 'set_shippingState', data: state});
}}
postalCode={state.shippingPostalCode}
changePostalCode={(e) => {
dispatch({type: 'set_shippingPostalCode', data: e.target.value});
}}
/>
</div>
<div className='form-row-third-2'>
<Input
name='postalCode'
type='text'
value={state.postalCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: 'set_postalCode', data: e.target.value});
}}
placeholder={intl.formatMessage({
id: 'payment_form.zipcode',
defaultMessage: 'Zip/Postal Code',
})}
required={true}
/>
</div>
</div>
</>
)}
<Terms
agreed={state.agreedTerms}
setAgreed={(data: boolean) => {

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

@@ -4,19 +4,20 @@
.form-view {
display: flex;
overflow: hidden;
width: 100%;
height: 100%;
flex-direction: row;
flex-grow: 1;
flex-wrap: wrap;
align-content: top;
align-items: flex-start;
justify-content: center;
padding: 77px 107px;
color: var(--center-channel-color);
font-family: "Open Sans";
font-size: 16px;
font-weight: 600;
overflow-x: hidden;
overflow-y: auto;
.title {
font-size: 22px;
@@ -39,14 +40,12 @@
margin-right: 16px;
.DropdownInput {
z-index: 99999;
margin-top: 0;
}
}
.DropdownInput {
position: relative;
z-index: 999999;
height: 36px;
margin-bottom: 24px;
@@ -517,6 +516,9 @@
}
input[type=checkbox] {
width: 17px;
height: 17px;
flex-shrink: 0;
margin-right: 12px;
}

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

@@ -4378,6 +4378,7 @@
"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.shipping_address": "Shipping Address",
"payment_form.zipcode": "Zip/Postal Code",
"payment.card_number": "Card Number",
"payment.field_required": "This field is required",

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

@@ -18,6 +18,7 @@ export interface SelfHostedSignupForm {
first_name: string;
last_name: string;
billing_address: Address;
shipping_address: Address;
organization: string;
}