diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-05-18 17:19:32 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-05-18 17:19:32 +0200 |
| commit | 8a37a091cb03ca6a7b51e930e319378c1f80f45e (patch) | |
| tree | 04d3ea55ffc238a83f8ba6cc39eca5ed33fca43d /gui | |
| parent | 9c3990be25ec6fd28506fccf2b7351ae0737e251 (diff) | |
| parent | b73d46d130f94951a4867cec594118debc35c059 (diff) | |
| download | mullvadvpn-8a37a091cb03ca6a7b51e930e319378c1f80f45e.tar.xz mullvadvpn-8a37a091cb03ca6a7b51e930e319378c1f80f45e.zip | |
Merge branch 'convert-redeem-voucher-to-styled-components'
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/RedeemVoucher.tsx | 245 | ||||
| -rw-r--r-- | gui/src/renderer/components/RedeemVoucherStyles.tsx | 78 | ||||
| -rw-r--r-- | gui/src/renderer/containers/RedeemVoucherContainer.tsx | 19 | ||||
| -rw-r--r-- | gui/src/renderer/context.tsx | 19 | ||||
| -rw-r--r-- | gui/src/renderer/lib/actionsHook.ts | 9 |
6 files changed, 173 insertions, 199 deletions
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index e96b0edd45..5703a33947 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -5,7 +5,7 @@ import { links } from '../../config.json'; import AccountExpiry from '../../shared/account-expiry'; import { AccountToken } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; -import RedeemVoucherContainer from '../containers/RedeemVoucherContainer'; +import { RedeemVoucherContainer } from '../components/RedeemVoucher'; import { LoginState } from '../redux/account/reducers'; import * as AppButton from './AppButton'; import * as Cell from './Cell'; diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx index 4ce00a9a24..39f3e68d3d 100644 --- a/gui/src/renderer/components/RedeemVoucher.tsx +++ b/gui/src/renderer/components/RedeemVoucher.tsx @@ -1,10 +1,19 @@ -import * as React from 'react'; -import { Component, Text, TextInput, View } from 'reactxp'; -import { colors } from '../../config.json'; +import React, { useCallback, useContext, useState } from 'react'; import { VoucherResponse } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import { useAppContext } from '../context'; +import useActions from '../lib/actionsHook'; +import accountActions from '../redux/account/actions'; import * as AppButton from './AppButton'; -import styles, { Spinner } from './RedeemVoucherStyles'; +import { + StyledEmptyResponse, + StyledErrorResponse, + StyledInput, + StyledSpinner, + StyledSuccessResponse, +} from './RedeemVoucherStyles'; + +const MIN_VOUCHER_LENGTH = 16; interface IRedeemVoucherContextValue { onSubmit: () => void; @@ -39,162 +48,126 @@ const RedeemVoucherContext = React.createContext<IRedeemVoucherContextValue>({ }); interface IRedeemVoucherProps { - submitVoucher: (voucherCode: string) => Promise<VoucherResponse>; - updateAccountExpiry: (expiry: string) => void; onSubmit?: () => void; onSuccess?: () => void; onFailure?: () => void; children?: React.ReactNode; } -interface IRedeemVoucherState { - value: string; - submitting: boolean; - response?: VoucherResponse; -} - -export class RedeemVoucher extends Component<IRedeemVoucherProps, IRedeemVoucherState> { - public state = { - value: '', - submitting: false, - response: undefined, - }; +export function RedeemVoucherContainer(props: IRedeemVoucherProps) { + const { onSubmit, onSuccess, onFailure } = props; - public render() { - return ( - <RedeemVoucherContext.Provider - value={{ - onSubmit: this.onSubmit, - value: this.state.value, - setValue: this.setValue, - valueValid: RedeemVoucher.isValueValid(this.state.value), - submitting: this.state.submitting, - response: this.state.response, - }}> - {this.props.children} - </RedeemVoucherContext.Provider> - ); - } + const { submitVoucher } = useAppContext(); + const { updateAccountExpiry } = useActions(accountActions); - private setValue = (value: string) => { - this.setState({ value }); - }; + const [value, setValue] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [response, setResponse] = useState<VoucherResponse>(); - private static isValueValid(value: string): boolean { - return value.length >= 16; - } + const valueValid = value.length >= MIN_VOUCHER_LENGTH; - private onSubmit = async () => { - if (!RedeemVoucher.isValueValid(this.state.value)) { + const onSubmitWrapper = useCallback(async () => { + if (!valueValid) { return; } - this.setState({ submitting: true }); - - if (this.props.onSubmit) { - this.props.onSubmit(); - } - - const response = await this.props.submitVoucher(this.state.value); + setSubmitting(true); + onSubmit?.(); + const response = await submitVoucher(value); + setSubmitting(false); + setResponse(response); if (response.type === 'success') { - this.setState({ value: '', submitting: false, response }); - this.props.updateAccountExpiry(response.new_expiry); - if (this.props.onSuccess) { - this.props.onSuccess(); - } + setValue(''); + updateAccountExpiry(response.new_expiry); + onSuccess?.(); } else { - this.setState({ submitting: false, response }); - if (this.props.onFailure) { - this.props.onFailure(); - } + onFailure?.(); } - }; + }, [value, valueValid, onSubmit, submitVoucher, updateAccountExpiry, onSuccess, onFailure]); + + return ( + <RedeemVoucherContext.Provider + value={{ onSubmit: onSubmitWrapper, value, setValue, valueValid, submitting, response }}> + {props.children} + </RedeemVoucherContext.Provider> + ); } -export class RedeemVoucherInput extends Component { - public render() { - return ( - <RedeemVoucherContext.Consumer> - {(context) => ( - <View> - <TextInput - style={styles.textInput} - value={context.value} - placeholder={'XXXX-XXXX-XXXX-XXXX'} - placeholderTextColor={colors.blue40} - autoCorrect={false} - onChangeText={context.setValue} - onSubmitEditing={context.onSubmit} - /> - </View> - )} - </RedeemVoucherContext.Consumer> - ); - } +export function RedeemVoucherInput() { + const { value, setValue, onSubmit } = useContext(RedeemVoucherContext); + + const onChange = useCallback( + (event: React.ChangeEvent<HTMLInputElement>) => { + setValue(event.target.value); + }, + [setValue], + ); + + const onKeyPress = useCallback( + (event: React.KeyboardEvent<HTMLInputElement>) => { + if (event.key === 'Enter') { + onSubmit(); + } + }, + [onSubmit], + ); + + return ( + <StyledInput + value={value} + placeholder={'XXXX-XXXX-XXXX-XXXX'} + onChange={onChange} + onKeyPress={onKeyPress} + /> + ); } -export class RedeemVoucherResponse extends Component { - public render() { - return ( - <RedeemVoucherContext.Consumer> - {(context) => { - if (context.submitting) { - return <Spinner source="icon-spinner" height={20} width={20} />; - } +export function RedeemVoucherResponse() { + const { response, submitting } = useContext(RedeemVoucherContext); - if (context.response) { - switch (context.response.type) { - case 'success': - return ( - <Text style={styles.redeemVoucherResponseSuccess}> - {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')} - </Text> - ); - case 'invalid': - return ( - <Text style={styles.redeemVoucherResponseError}> - {messages.pgettext('redeem-voucher-view', 'Voucher code is invalid.')} - </Text> - ); - case 'already_used': - return ( - <Text style={styles.redeemVoucherResponseError}> - {messages.pgettext( - 'redeem-voucher-view', - 'Voucher code has already been used.', - )} - </Text> - ); - case 'error': - return ( - <Text style={styles.redeemVoucherResponseError}> - {messages.pgettext('redeem-voucher-view', 'An error occured.')} - </Text> - ); - } - } + if (submitting) { + return <StyledSpinner source="icon-spinner" height={20} width={20} />; + } - return <View style={styles.redeemVoucherResponseEmpty} />; - }} - </RedeemVoucherContext.Consumer> - ); + if (response) { + switch (response.type) { + case 'success': + return ( + <StyledSuccessResponse> + {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')} + </StyledSuccessResponse> + ); + case 'invalid': + return ( + <StyledErrorResponse> + {messages.pgettext('redeem-voucher-view', 'Voucher code is invalid.')} + </StyledErrorResponse> + ); + case 'already_used': + return ( + <StyledErrorResponse> + {messages.pgettext('redeem-voucher-view', 'Voucher code has already been used.')} + </StyledErrorResponse> + ); + case 'error': + return ( + <StyledErrorResponse> + {messages.pgettext('redeem-voucher-view', 'An error occured.')} + </StyledErrorResponse> + ); + } } + + return <StyledEmptyResponse />; } -export class RedeemVoucherSubmitButton extends Component { - public render() { - return ( - <RedeemVoucherContext.Consumer> - {(context) => ( - <AppButton.GreenButton - key="cancel" - disabled={!context.valueValid || context.submitting} - onPress={context.onSubmit}> - {messages.pgettext('redeem-voucher-view', 'Redeem')} - </AppButton.GreenButton> - )} - </RedeemVoucherContext.Consumer> - ); - } +export function RedeemVoucherSubmitButton() { + const { valueValid, onSubmit, submitting } = useContext(RedeemVoucherContext); + + return ( + <AppButton.GreenButton key="cancel" disabled={!valueValid || submitting} onPress={onSubmit}> + {messages.pgettext('redeem-voucher-view', 'Redeem')} + </AppButton.GreenButton> + ); } diff --git a/gui/src/renderer/components/RedeemVoucherStyles.tsx b/gui/src/renderer/components/RedeemVoucherStyles.tsx index b2e462e7cd..6ce463fab9 100644 --- a/gui/src/renderer/components/RedeemVoucherStyles.tsx +++ b/gui/src/renderer/components/RedeemVoucherStyles.tsx @@ -1,46 +1,46 @@ -import { Styles } from 'reactxp'; import styled from 'styled-components'; import { colors } from '../../config.json'; import ImageView from './ImageView'; -export const Spinner = styled(ImageView)({ +export const StyledInput = styled.input({ + flex: 1, + overflow: 'hidden', + padding: '14px', + fontFamily: 'Open Sans', + fontSize: '13px', + fontWeight: 600, + lineHeight: '26px', + color: colors.blue, + backgroundColor: colors.white, + border: 'none', + borderRadius: '4px', + '::placeholder': { + color: colors.blue40, + }, +}); + +export const StyledResponse = styled.span({ marginTop: '8px', + fontFamily: 'Open Sans', + fontSize: '13px', + lineHeight: '20px', +}); + +export const StyledSuccessResponse = styled(StyledResponse)({ + fontWeight: 600, + color: colors.green, +}); + +export const StyledErrorResponse = styled(StyledResponse)({ + fontWeight: 800, + color: colors.red, }); -export default { - textInput: Styles.createTextInputStyle({ - flex: 1, - overflow: 'hidden', - paddingTop: 14, - paddingLeft: 14, - paddingRight: 14, - paddingBottom: 14, - fontFamily: 'Open Sans', - fontSize: 13, - fontWeight: '600', - lineHeight: 26, - color: colors.blue, - backgroundColor: colors.white, - borderRadius: 4, - }), - redeemVoucherResponseSuccess: Styles.createTextStyle({ - marginTop: 8, - fontFamily: 'Open Sans', - fontSize: 13, - fontWeight: '600', - lineHeight: 20, - color: colors.green, - }), - redeemVoucherResponseError: Styles.createTextStyle({ - marginTop: 8, - fontFamily: 'Open Sans', - fontSize: 13, - fontWeight: '800', - lineHeight: 20, - color: colors.red, - }), - redeemVoucherResponseEmpty: Styles.createViewStyle({ - height: 20, - marginTop: 8, - }), -}; +export const StyledEmptyResponse = styled.span({ + height: '20px', + marginTop: '8px', +}); + +export const StyledSpinner = styled(ImageView)({ + marginTop: '8px', +}); diff --git a/gui/src/renderer/containers/RedeemVoucherContainer.tsx b/gui/src/renderer/containers/RedeemVoucherContainer.tsx deleted file mode 100644 index 1aeee8c719..0000000000 --- a/gui/src/renderer/containers/RedeemVoucherContainer.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { RedeemVoucher } from '../components/RedeemVoucher'; -import withAppContext, { IAppContext } from '../context'; -import { ReduxDispatch } from '../redux/store'; -import accountActions from '../redux/account/actions'; - -const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => { - const account = bindActionCreators(accountActions, dispatch); - - return { - submitVoucher: (voucherCode: string) => { - return props.app.submitVoucher(voucherCode); - }, - updateAccountExpiry: account.updateAccountExpiry, - }; -}; - -export default withAppContext(connect(null, mapDispatchToProps)(RedeemVoucher)); diff --git a/gui/src/renderer/context.tsx b/gui/src/renderer/context.tsx index 4a905be6b1..113a29fe71 100644 --- a/gui/src/renderer/context.tsx +++ b/gui/src/renderer/context.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useContext } from 'react'; import App from './app'; export interface IAppContext { @@ -10,6 +10,10 @@ if (process.env.NODE_ENV === 'development') { AppContext.displayName = 'AppContext'; } +const missingContextError = new Error( + 'The context value is empty. Make sure to wrap the component in AppContext.Provider.', +); + export default function withAppContext<Props>(BaseComponent: React.ComponentType<Props>) { // Exclude the IAppContext from props since those are injected props const wrappedComponent = (props: Omit<Props, keyof IAppContext>) => { @@ -23,9 +27,7 @@ export default function withAppContext<Props>(BaseComponent: React.ComponentType return <BaseComponent {...mergedProps} />; } else { - throw new Error( - 'The context value is empty. Make sure to wrap the component in AppContext.Provider.', - ); + throw missingContextError; } }} </AppContext.Consumer> @@ -39,3 +41,12 @@ export default function withAppContext<Props>(BaseComponent: React.ComponentType return wrappedComponent; } + +export function useAppContext(): App { + const appContext = useContext(AppContext); + if (appContext) { + return appContext.app; + } else { + throw missingContextError; + } +} diff --git a/gui/src/renderer/lib/actionsHook.ts b/gui/src/renderer/lib/actionsHook.ts new file mode 100644 index 0000000000..2597aee422 --- /dev/null +++ b/gui/src/renderer/lib/actionsHook.ts @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { ActionCreatorsMapObject, bindActionCreators } from 'redux'; + +export default function useActions<A, M extends ActionCreatorsMapObject<A>>(actionCreator: M) { + const dispatch = useDispatch(); + const actions = useMemo(() => bindActionCreators(actionCreator, dispatch), [dispatch]); + return actions; +} |
