From 9105077ee11013e2f26652828f593a4836ac53e6 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Fri, 24 Mar 2023 14:46:22 -0500 Subject: [PATCH] Mm 50735 (#22626) * add shipping address to self hosted signup model * purchase modals allow scroll * fix z-index issues with payment modal dropdowns --- model/hosted_customer.go | 9 +- .../choose_different_shipping.scss | 37 +++ .../choose_different_shipping/index.tsx | 45 ++++ .../src/components/dropdown_input.scss | 36 ++- .../components/payment_form/address_form.tsx | 42 ++-- .../components/payment_form/payment_form.scss | 2 - .../components/payment_form/payment_form.tsx | 2 +- .../components/purchase_modal/purchase.scss | 38 +-- .../purchase_modal/purchase_modal.tsx | 3 +- .../self_hosted_purchase_modal/address.tsx | 132 ++++++++++ .../self_hosted_purchase_modal/index.test.tsx | 24 ++ .../self_hosted_purchase_modal/index.tsx | 227 ++++++++++-------- .../self_hosted_purchase_modal.scss | 10 +- webapp/channels/src/i18n/en.json | 1 + webapp/platform/types/src/hosted_customer.ts | 1 + 15 files changed, 438 insertions(+), 171 deletions(-) create mode 100644 webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss create mode 100644 webapp/channels/src/components/choose_different_shipping/index.tsx create mode 100644 webapp/channels/src/components/self_hosted_purchase_modal/address.tsx diff --git a/model/hosted_customer.go b/model/hosted_customer.go index 4f1917bdaf..543ea12b74 100644 --- a/model/hosted_customer.go +++ b/model/hosted_customer.go @@ -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 { diff --git a/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss new file mode 100644 index 0000000000..322997fc03 --- /dev/null +++ b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss @@ -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; + } +} diff --git a/webapp/channels/src/components/choose_different_shipping/index.tsx b/webapp/channels/src/components/choose_different_shipping/index.tsx new file mode 100644 index 0000000000..43b810f2e8 --- /dev/null +++ b/webapp/channels/src/components/choose_different_shipping/index.tsx @@ -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 ( +
+ + + + +
+ ); +} diff --git a/webapp/channels/src/components/dropdown_input.scss b/webapp/channels/src/components/dropdown_input.scss index e654e78d5c..1420223f21 100644 --- a/webapp/channels/src/components/dropdown_input.scss +++ b/webapp/channels/src/components/dropdown_input.scss @@ -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; + } +} diff --git a/webapp/channels/src/components/payment_form/address_form.tsx b/webapp/channels/src/components/payment_form/address_form.tsx index 651861b8d1..470f45da88 100644 --- a/webapp/channels/src/components/payment_form/address_form.tsx +++ b/webapp/channels/src/components/payment_form/address_form.tsx @@ -61,25 +61,27 @@ const AddressForm = (props: AddressFormProps) => { {...props.title} /> - ({ - 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'} - /> +
+ ({ + 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 { diff --git a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx index bf4abc4804..36b39a3ab0 100644 --- a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx +++ b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx @@ -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 { } return ( -
+

{title}

void; + + address: string; + changeAddress: (e: React.ChangeEvent) => void; + + address2: string; + changeAddress2: (e: React.ChangeEvent) => void; + + city: string; + changeCity: (e: React.ChangeEvent) => void; + + state: string; + changeState: (postalCode: string) => void; + + postalCode: string; + changePostalCode: (e: React.ChangeEvent) => 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 ( + <> +
+ ({ + 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'} + /> +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ + ); +} diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx index 5a3fbf6bda..3ccbb74ce2 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx @@ -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; diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx index 03bfbccddc..af43bfd229 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx @@ -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> = [ '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
- { +
{ 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}); + }} /> -
- ) => { - dispatch({type: 'set_address', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.address', - defaultMessage: 'Address', - })} - required={true} - /> -
-
- ) => { - dispatch({type: 'set_address2', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.address_2', - defaultMessage: 'Address 2', - })} - /> -
-
- ) => { - dispatch({type: 'set_city', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.city', - defaultMessage: 'City', - })} - required={true} - /> -
-
-
- { - dispatch({type: 'set_state', data: state}); + { + dispatch({type: 'set_shippingSame', data: val}); + }} + /> + {!state.shippingSame && ( + <> +
+ +
+
{ + 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}); }} /> -
-
- ) => { - dispatch({type: 'set_postalCode', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.zipcode', - defaultMessage: 'Zip/Postal Code', - })} - required={true} - /> -
-
+ + )} { diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss index e949e76e20..52bac49aec 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss +++ b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss @@ -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; } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c4f6025f80..6c28bff86c 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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", diff --git a/webapp/platform/types/src/hosted_customer.ts b/webapp/platform/types/src/hosted_customer.ts index d81ef15227..fcd5b4e70b 100644 --- a/webapp/platform/types/src/hosted_customer.ts +++ b/webapp/platform/types/src/hosted_customer.ts @@ -18,6 +18,7 @@ export interface SelfHostedSignupForm { first_name: string; last_name: string; billing_address: Address; + shipping_address: Address; organization: string; }