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;
}