summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-05-18 17:19:32 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-05-18 17:19:32 +0200
commit8a37a091cb03ca6a7b51e930e319378c1f80f45e (patch)
tree04d3ea55ffc238a83f8ba6cc39eca5ed33fca43d /gui/src
parent9c3990be25ec6fd28506fccf2b7351ae0737e251 (diff)
parentb73d46d130f94951a4867cec594118debc35c059 (diff)
downloadmullvadvpn-8a37a091cb03ca6a7b51e930e319378c1f80f45e.tar.xz
mullvadvpn-8a37a091cb03ca6a7b51e930e319378c1f80f45e.zip
Merge branch 'convert-redeem-voucher-to-styled-components'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx2
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx245
-rw-r--r--gui/src/renderer/components/RedeemVoucherStyles.tsx78
-rw-r--r--gui/src/renderer/containers/RedeemVoucherContainer.tsx19
-rw-r--r--gui/src/renderer/context.tsx19
-rw-r--r--gui/src/renderer/lib/actionsHook.ts9
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;
+}