diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-06-28 12:12:35 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-06-28 12:12:35 +0200 |
| commit | 4d95c0a019c2d7bfb2c3ca4951febfded93e9758 (patch) | |
| tree | 1cb7b54c080a865e597102e16b1b9b32e4ca3505 /gui/src/renderer/components | |
| parent | 09a3e447f47591643032fbf988dd9017db629b25 (diff) | |
| parent | b51b561baabebf3210f936eddc90390488e8819b (diff) | |
| download | mullvadvpn-4d95c0a019c2d7bfb2c3ca4951febfded93e9758.tar.xz mullvadvpn-4d95c0a019c2d7bfb2c3ca4951febfded93e9758.zip | |
Merge branch 'new-account-flow'
Diffstat (limited to 'gui/src/renderer/components')
| -rw-r--r-- | gui/src/renderer/components/AppButton.tsx | 23 | ||||
| -rw-r--r-- | gui/src/renderer/components/Connect.tsx | 44 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountAddTime.tsx | 274 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 90 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/components/HeaderBar.tsx | 33 | ||||
| -rw-r--r-- | gui/src/renderer/components/MainView.tsx | 28 | ||||
| -rw-r--r-- | gui/src/renderer/components/RedeemVoucher.tsx | 42 |
8 files changed, 436 insertions, 111 deletions
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx index 352d70d9e9..d424e8b54e 100644 --- a/gui/src/renderer/components/AppButton.tsx +++ b/gui/src/renderer/components/AppButton.tsx @@ -191,3 +191,26 @@ export const RedTransparentButton = styled(BaseButton)({ backgroundColor: colors.red80, }, }); + +const StyledButtonWrapper = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 0, + ':not(:last-child)': { + marginBottom: '18px', + }, +}); + +interface IButtonGroupProps { + children: React.ReactElement[]; +} + +export function ButtonGroup(props: IButtonGroupProps) { + return ( + <> + {React.Children.map(props.children, (button, index) => ( + <StyledButtonWrapper key={index}>{button}</StyledButtonWrapper> + ))} + </> + ); +} diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx index a6654ad8c0..e48abf8d96 100644 --- a/gui/src/renderer/components/Connect.tsx +++ b/gui/src/renderer/components/Connect.tsx @@ -1,15 +1,13 @@ import * as React from 'react'; import styled from 'styled-components'; import { hasExpired } from '../../shared/account-expiry'; -import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer'; import NotificationArea from '../components/NotificationArea'; import { AuthFailureKind, parseAuthFailure } from '../../shared/auth-failure'; import { LoginState } from '../redux/account/reducers'; import { IConnectionReduxState } from '../redux/connection/reducers'; -import { FocusFallback } from './Focus'; -import { Brand, HeaderBarStyle, HeaderBarSettingsButton } from './HeaderBar'; +import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar'; import ImageView from './ImageView'; -import { Container, Header, Layout } from './Layout'; +import { Container, Layout } from './Layout'; import Map, { MarkerStyle, ZoomLevel } from './Map'; import { ModalContainer } from './Modal'; import TunnelControl from './TunnelControl'; @@ -89,21 +87,8 @@ export default class Connect extends React.Component<IProps, IState> { return ( <ModalContainer> <Layout> - <Header barStyle={this.headerBarStyle()}> - <FocusFallback> - <Brand /> - </FocusFallback> - <HeaderBarSettingsButton /> - </Header> - <StyledContainer> - {this.state.isAccountExpired || - (this.props.loginState.type === 'ok' && - this.props.loginState.method === 'new_account') ? ( - <ExpiredAccountErrorViewContainer /> - ) : ( - this.renderMap() - )} - </StyledContainer> + <DefaultHeaderBar barStyle={calculateHeaderBarStyle(this.props.connection.status)} /> + <StyledContainer>{this.renderMap()}</StyledContainer> </Layout> </ModalContainer> ); @@ -171,27 +156,6 @@ export default class Connect extends React.Component<IProps, IState> { ); } - private headerBarStyle(): HeaderBarStyle { - const { status } = this.props.connection; - switch (status.state) { - case 'disconnected': - return HeaderBarStyle.error; - case 'connecting': - case 'connected': - return HeaderBarStyle.success; - case 'error': - return !status.details.blockFailure ? HeaderBarStyle.success : HeaderBarStyle.error; - case 'disconnecting': - switch (status.details) { - case 'block': - case 'reconnect': - return HeaderBarStyle.success; - case 'nothing': - return HeaderBarStyle.error; - } - } - } - private getMapProps(): Map['props'] { const { longitude, diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx new file mode 100644 index 0000000000..9831430d4e --- /dev/null +++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx @@ -0,0 +1,274 @@ +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; +import { sprintf } from 'sprintf-js'; +import styled from 'styled-components'; +import { links, colors } from '../../config.json'; +import { formatRelativeDate } from '../../shared/date-helper'; +import { messages } from '../../shared/gettext'; +import { useAppContext } from '../context'; +import useActions from '../lib/actionsHook'; +import History from '../lib/history'; +import account from '../redux/account/actions'; +import { IReduxState } from '../redux/store'; +import * as AppButton from './AppButton'; +import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; +import { bigText } from './common-styles'; +import CustomScrollbars from './CustomScrollbars'; +import { calculateHeaderBarStyle, DefaultHeaderBar, HeaderBarStyle } from './HeaderBar'; +import ImageView from './ImageView'; +import { Container, Layout } from './Layout'; +import { + RedeemVoucherContainer, + RedeemVoucherInput, + RedeemVoucherResponse, + RedeemVoucherSubmitButton, +} from './RedeemVoucher'; + +export const StyledHeader = styled(DefaultHeaderBar)({ + flex: 0, +}); + +export const StyledCustomScrollbars = styled(CustomScrollbars)({ + flex: 1, +}); + +export const StyledContainer = styled(Container)({ + paddingTop: '22px', + minHeight: '100%', + backgroundColor: colors.darkBlue, +}); + +export const StyledBody = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, + padding: '0 22px', + paddingBottom: 'auto', +}); + +export const StyledFooter = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 0, + padding: '18px 22px 22px', +}); + +export const StyledTitle = styled.span(bigText, { + lineHeight: '38px', + marginBottom: '8px', +}); + +export const StyledLabel = styled.span({ + fontFamily: 'Open Sans', + fontSize: '13px', + fontWeight: 600, + lineHeight: '20px', + color: colors.white, + marginBottom: '9px', +}); + +export const StyledRedeemVoucherInput = styled(RedeemVoucherInput)({ + flex: 0, +}); + +export const StyledStatusIcon = styled.div({ + alignSelf: 'center', + width: '60px', + height: '60px', + marginBottom: '18px', +}); + +export function VoucherInput() { + const history = useHistory(); + + const onSuccess = useCallback(() => { + history.push('/main/voucher/success'); + }, [history]); + + const navigateBack = useCallback(() => { + history.goBack(); + }, [history]); + + return ( + <Layout> + <HeaderBar /> + <StyledCustomScrollbars fillContainer> + <StyledContainer> + <RedeemVoucherContainer onSuccess={onSuccess}> + <StyledBody> + <StyledTitle>{messages.pgettext('connect-view', 'Redeem voucher')}</StyledTitle> + <StyledLabel>{messages.pgettext('connect-view', 'Enter voucher code')}</StyledLabel> + <StyledRedeemVoucherInput /> + <RedeemVoucherResponse disableSuccessMessage /> + </StyledBody> + + <StyledFooter> + <AppButton.ButtonGroup> + <RedeemVoucherSubmitButton /> + <AppButton.BlueButton onClick={navigateBack}> + {messages.gettext('Cancel')} + </AppButton.BlueButton> + </AppButton.ButtonGroup> + </StyledFooter> + </RedeemVoucherContainer> + </StyledContainer> + </StyledCustomScrollbars> + </Layout> + ); +} + +export function VoucherVerificationSuccess() { + return ( + <TimeAdded title={messages.pgettext('connect-view', 'Voucher was successfully redeemed')} /> + ); +} + +interface ITimeAddedProps { + title?: string; +} + +export function TimeAdded(props: ITimeAddedProps) { + const history = useHistory(); + const finish = useFinishedCallback(); + const accountData = useSelector((state: IReduxState) => state.account); + const isNewAccount = useSelector( + (state: IReduxState) => + state.account.status.type === 'ok' && state.account.status.method === 'new_account', + ); + + const navigateToSetupFinished = useCallback(() => { + if (isNewAccount) { + history.push('/main/setup-finished'); + } else { + finish(); + } + }, [history, finish]); + + const duration = + (accountData.expiry && + accountData.previousExpiry && + formatRelativeDate(accountData.expiry, accountData.previousExpiry)) ?? + ''; + + return ( + <Layout> + <HeaderBar /> + <StyledCustomScrollbars fillContainer> + <StyledContainer> + <StyledBody> + <StyledStatusIcon> + <ImageView source="icon-success" height={60} width={60} /> + </StyledStatusIcon> + <StyledTitle> + {props.title ?? messages.pgettext('connect-view', 'Time was successfully added')} + </StyledTitle> + <StyledLabel> + {sprintf( + messages.pgettext('connect-view', '%(duration)s was added to your account.'), + { duration }, + )} + </StyledLabel> + </StyledBody> + + <StyledFooter> + <AppButton.BlueButton onClick={navigateToSetupFinished}> + {messages.gettext('Next')} + </AppButton.BlueButton> + </StyledFooter> + </StyledContainer> + </StyledCustomScrollbars> + </Layout> + ); +} + +export function SetupFinished() { + const finish = useFinishedCallback(); + const { openUrl } = useAppContext(); + + const openPrivacyLink = useCallback(() => openUrl(links.privacyGuide), [openUrl]); + + return ( + <Layout> + <HeaderBar /> + <StyledCustomScrollbars fillContainer> + <StyledContainer> + <StyledBody> + <StyledTitle>{messages.pgettext('connect-view', "You're all set!")}</StyledTitle> + <StyledLabel> + {messages.pgettext( + 'connect-view', + 'Go ahead and start using the app to begin reclaiming your online privacy.', + )} + </StyledLabel> + <StyledLabel> + {messages.pgettext( + 'connect-view', + 'To continue your journey as a privacy ninja, visit our website to pick up other privacy-friendly habits and tools.', + )} + </StyledLabel> + </StyledBody> + + <StyledFooter> + <AppButton.ButtonGroup> + <AriaDescriptionGroup> + <AriaDescribed> + <AppButton.BlueButton onClick={openPrivacyLink}> + <AppButton.Label> + {messages.pgettext('connect-view', 'Learn about privacy')} + </AppButton.Label> + <AriaDescription> + <AppButton.Icon + height={16} + width={16} + source="icon-extLink" + aria-label={messages.pgettext('accessibility', 'Opens externally')} + /> + </AriaDescription> + </AppButton.BlueButton> + </AriaDescribed> + </AriaDescriptionGroup> + <AppButton.GreenButton onClick={finish}> + {messages.pgettext('connect-view', 'Start using the app')} + </AppButton.GreenButton> + </AppButton.ButtonGroup> + </StyledFooter> + </StyledContainer> + </StyledCustomScrollbars> + </Layout> + ); +} + +function HeaderBar() { + const isNewAccount = useSelector( + (state: IReduxState) => + state.account.status.type === 'ok' && state.account.status.method === 'new_account', + ); + const tunnelState = useSelector((state: IReduxState) => state.connection.status); + const headerBarStyle = isNewAccount + ? HeaderBarStyle.default + : calculateHeaderBarStyle(tunnelState); + + return <StyledHeader barStyle={headerBarStyle} />; +} + +function useFinishedCallback() { + const { loggedIn } = useActions(account); + + const history = useHistory() as History; + const isNewAccount = useSelector( + (state: IReduxState) => + state.account.status.type === 'ok' && state.account.status.method === 'new_account', + ); + + const callback = useCallback(() => { + // Changes login method from "new_account" to "existing_account" + if (isNewAccount) { + loggedIn(); + } + + history.resetWith('/main'); + }, [isNewAccount, loggedIn, history]); + + return callback; +} diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index a01993787b..04d9f96156 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { sprintf } from 'sprintf-js'; import { links } from '../../config.json'; -import { hasExpired } from '../../shared/account-expiry'; -import { AccountToken } from '../../shared/daemon-rpc-types'; +import { AccountToken, TunnelState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { LoginState } from '../redux/account/reducers'; import * as AppButton from './AppButton'; @@ -18,14 +17,16 @@ import { StyledCustomScrollbars, StyledDisconnectButton, StyledFooter, + StyledHeader, StyledMessage, StyledModalCellContainer, StyledStatusIcon, StyledTitle, } from './ExpiredAccountErrorViewStyles'; +import { calculateHeaderBarStyle, HeaderBarStyle } from './HeaderBar'; import ImageView from './ImageView'; -import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; -import { RedeemVoucherContainer, RedeemVoucherAlert } from './RedeemVoucher'; +import { Layout } from './Layout'; +import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal'; export enum RecoveryAction { openBrowser, @@ -37,17 +38,16 @@ interface IExpiredAccountErrorViewProps { isBlocked: boolean; blockWhenDisconnected: boolean; accountToken?: AccountToken; - accountExpiry?: string; loginState: LoginState; - hideWelcomeView: () => void; + tunnelState: TunnelState; onExternalLinkWithAuth: (url: string) => Promise<void>; onDisconnect: () => Promise<void>; setBlockWhenDisconnected: (value: boolean) => void; + navigateToRedeemVoucher: () => void; } interface IExpiredAccountErrorViewState { showBlockWhenDisconnectedAlert: boolean; - showRedeemVoucherAlert: boolean; } export default class ExpiredAccountErrorView extends React.Component< @@ -56,43 +56,45 @@ export default class ExpiredAccountErrorView extends React.Component< > { public state: IExpiredAccountErrorViewState = { showBlockWhenDisconnectedAlert: false, - showRedeemVoucherAlert: false, }; - public componentDidUpdate() { - if (this.props.accountExpiry && !hasExpired(this.props.accountExpiry)) { - this.props.hideWelcomeView(); - } - } - public render() { + const headerBarStyle = + this.props.loginState.type === 'ok' && this.props.loginState.method === 'new_account' + ? HeaderBarStyle.default + : calculateHeaderBarStyle(this.props.tunnelState); + return ( - <StyledCustomScrollbars fillContainer> - <StyledContainer> - <StyledBody>{this.renderContent()}</StyledBody> + <ModalContainer> + <Layout> + <StyledHeader barStyle={headerBarStyle} /> + <StyledCustomScrollbars fillContainer> + <StyledContainer> + <StyledBody>{this.renderContent()}</StyledBody> - <StyledFooter> - {this.getRecoveryAction() === RecoveryAction.disconnect && ( - <AppButton.BlockingButton onClick={this.props.onDisconnect}> - <StyledDisconnectButton> - {messages.pgettext('connect-view', 'Disconnect')} - </StyledDisconnectButton> - </AppButton.BlockingButton> - )} + <StyledFooter> + {this.getRecoveryAction() === RecoveryAction.disconnect && ( + <AppButton.BlockingButton onClick={this.props.onDisconnect}> + <StyledDisconnectButton> + {messages.pgettext('connect-view', 'Disconnect')} + </StyledDisconnectButton> + </AppButton.BlockingButton> + )} - {this.renderExternalPaymentButton()} + {this.renderExternalPaymentButton()} - <AppButton.GreenButton - disabled={this.getRecoveryAction() === RecoveryAction.disconnect} - onClick={this.onOpenRedeemVoucherAlert}> - {messages.pgettext('connect-view', 'Redeem voucher')} - </AppButton.GreenButton> - </StyledFooter> + <AppButton.GreenButton + disabled={this.getRecoveryAction() === RecoveryAction.disconnect} + onClick={this.props.navigateToRedeemVoucher}> + {messages.pgettext('connect-view', 'Redeem voucher')} + </AppButton.GreenButton> + </StyledFooter> - {this.state.showRedeemVoucherAlert && this.renderRedeemVoucherAlert()} - {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()} - </StyledContainer> - </StyledCustomScrollbars> + {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()} + </StyledContainer> + </StyledCustomScrollbars> + </Layout> + </ModalContainer> ); } @@ -188,14 +190,6 @@ export default class ExpiredAccountErrorView extends React.Component< ); } - private renderRedeemVoucherAlert() { - return ( - <RedeemVoucherContainer onSuccess={this.props.hideWelcomeView}> - <RedeemVoucherAlert onClose={this.onCloseRedeemVoucherAlert} /> - </RedeemVoucherContainer> - ); - } - private renderBlockWhenDisconnectedAlert() { return ( <ModalAlert @@ -255,14 +249,6 @@ export default class ExpiredAccountErrorView extends React.Component< } } - private onOpenRedeemVoucherAlert = () => { - this.setState({ showRedeemVoucherAlert: true }); - }; - - private onCloseRedeemVoucherAlert = () => { - this.setState({ showRedeemVoucherAlert: false }); - }; - private onCloseBlockWhenDisconnectedInstructions = () => { this.setState({ showBlockWhenDisconnectedAlert: false }); }; diff --git a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx index 3559d3b8ad..8872bbb312 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx @@ -5,6 +5,12 @@ import * as AppButton from './AppButton'; import * as Cell from './cell'; import { bigText, smallText } from './common-styles'; import CustomScrollbars from './CustomScrollbars'; +import { DefaultHeaderBar } from './HeaderBar'; +import { Container } from './Layout'; + +export const StyledHeader = styled(DefaultHeaderBar)({ + flex: 0, +}); export const StyledAccountTokenLabel = styled(AccountTokenLabel)({ fontFamily: 'Open Sans', @@ -31,12 +37,10 @@ export const StyledCustomScrollbars = styled(CustomScrollbars)({ flex: 1, }); -export const StyledContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - flex: 1, +export const StyledContainer = styled(Container)({ paddingTop: '22px', minHeight: '100%', + backgroundColor: colors.darkBlue, }); export const StyledBody = styled.div({ @@ -51,7 +55,6 @@ export const StyledFooter = styled.div({ flexDirection: 'column', flex: 0, padding: '18px 22px 22px', - backgroundColor: colors.darkBlue, }); export const StyledTitle = styled.span(bigText, { diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx index bdf7d51924..b89811913e 100644 --- a/gui/src/renderer/components/HeaderBar.tsx +++ b/gui/src/renderer/components/HeaderBar.tsx @@ -3,8 +3,10 @@ import { useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import styled from 'styled-components'; import { colors } from '../../config.json'; +import { TunnelState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IReduxState } from '../redux/store'; +import { FocusFallback } from './Focus'; import ImageView from './ImageView'; export enum HeaderBarStyle { @@ -118,3 +120,34 @@ export function HeaderBarSettingsButton() { </HeaderBarSettingsButtonContainer> ); } + +export function DefaultHeaderBar(props: IHeaderBarProps) { + return ( + <HeaderBar {...props}> + <FocusFallback> + <Brand /> + </FocusFallback> + <HeaderBarSettingsButton /> + </HeaderBar> + ); +} + +export function calculateHeaderBarStyle(tunnelState: TunnelState): HeaderBarStyle { + switch (tunnelState.state) { + case 'disconnected': + return HeaderBarStyle.error; + case 'connecting': + case 'connected': + return HeaderBarStyle.success; + case 'error': + return !tunnelState.details.blockFailure ? HeaderBarStyle.success : HeaderBarStyle.error; + case 'disconnecting': + switch (tunnelState.details) { + case 'block': + case 'reconnect': + return HeaderBarStyle.success; + case 'nothing': + return HeaderBarStyle.error; + } + } +} diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx new file mode 100644 index 0000000000..9698321940 --- /dev/null +++ b/gui/src/renderer/components/MainView.tsx @@ -0,0 +1,28 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; +import { hasExpired } from '../../shared/account-expiry'; +import { IReduxState } from '../redux/store'; +import ConnectPage from '../containers/ConnectPage'; +import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer'; + +export default function MainView() { + const history = useHistory(); + const accountExpiry = useSelector((state: IReduxState) => state.account.expiry); + const accountHasExpired = accountExpiry && hasExpired(accountExpiry); + const isNewAccount = useSelector( + (state: IReduxState) => + state.account.status.type === 'ok' && state.account.status.method === 'new_account', + ); + const [showAccountExpired, setShowAccountExpired] = useState(isNewAccount || accountHasExpired); + + useEffect(() => { + if (accountHasExpired) { + setShowAccountExpired(true); + } else if (showAccountExpired && !accountHasExpired) { + history.push('/main/time-added'); + } + }, [showAccountExpired, accountHasExpired]); + + return showAccountExpired ? <ExpiredAccountErrorViewContainer /> : <ConnectPage />; +} diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx index f8c0d19b83..b878a94c4f 100644 --- a/gui/src/renderer/components/RedeemVoucher.tsx +++ b/gui/src/renderer/components/RedeemVoucher.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useContext, useState } from 'react'; import { VoucherResponse } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; -import { useScheduler } from '../../shared/scheduler'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; import accountActions from '../redux/account/actions'; @@ -60,7 +59,6 @@ interface IRedeemVoucherProps { export function RedeemVoucherContainer(props: IRedeemVoucherProps) { const { onSubmit, onSuccess, onFailure } = props; - const closeScheduler = useScheduler(); const { submitVoucher } = useAppContext(); const { updateAccountExpiry } = useActions(accountActions); @@ -75,18 +73,21 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) { return; } + const submitTimestamp = Date.now(); setSubmitting(true); onSubmit?.(); const response = await submitVoucher(value); + // Show the spinner for at least half a second if it isn't successful. + const submitDuration = Date.now() - submitTimestamp; + if (response.type !== 'success' && submitDuration < 500) { + await new Promise((resolve) => setTimeout(resolve, 500 - submitDuration)); + } + setSubmitting(false); setResponse(response); if (response.type === 'success') { - setValue(''); - closeScheduler.schedule(() => { - updateAccountExpiry(response.newExpiry); - onSuccess?.(); - }, 1000); + onSuccess?.(); } else { onFailure?.(); } @@ -100,7 +101,11 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) { ); } -export function RedeemVoucherInput() { +interface IRedeemVoucherInputProps { + className?: string; +} + +export function RedeemVoucherInput(props: IRedeemVoucherInputProps) { const { value, setValue, onSubmit, submitting, response } = useContext(RedeemVoucherContext); const disabled = submitting || response?.type === 'success'; @@ -122,6 +127,7 @@ export function RedeemVoucherInput() { return ( <StyledInput + className={props.className} allowedCharacters="[A-Z0-9]" separator="-" uppercaseOnly @@ -137,7 +143,11 @@ export function RedeemVoucherInput() { ); } -export function RedeemVoucherResponse() { +interface IRedeemVoucherResponseProps { + disableSuccessMessage?: boolean; +} + +export function RedeemVoucherResponse(props: IRedeemVoucherResponseProps) { const { response, submitting } = useContext(RedeemVoucherContext); if (submitting) { @@ -147,11 +157,15 @@ export function RedeemVoucherResponse() { if (response) { switch (response.type) { case 'success': - return ( - <StyledSuccessResponse> - {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')} - </StyledSuccessResponse> - ); + if (props.disableSuccessMessage) { + return <StyledEmptyResponse />; + } else { + return ( + <StyledSuccessResponse> + {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')} + </StyledSuccessResponse> + ); + } case 'invalid': return ( <StyledErrorResponse> |
