Repository: kiwicom/mobile
Pull request #930 (2018-07-27 12:33:46)

MMB: Refactor TravelDocumentModalScreen
Basically moving state to a context, and create smaller components


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 0dc3f795..b27fe62a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -58,6 +58,7 @@ module.exports = {
               'TouchableHighlight', // Touchable
               'TouchableNativeFeedback', // Touchable
               'TouchableOpacity', // Touchable
+              'Switch' //Switch
             ],
             message:
               "Please use '@kiwicom/mobile-shared' package instead.",
diff --git a/app/MMB/index.js b/app/MMB/index.js
index a9940806..9ba1cbd0 100644
--- a/app/MMB/index.js
+++ b/app/MMB/index.js
@@ -12,6 +12,7 @@ import {
 import NavigationStack from './src/navigation/NavigationStack';
 import BookingDetailContext from './src/context/BookingDetailContext';
 import WalletContext from './src/context/WalletContext';
+import TravelDocumentFormContext from './src/scenes/travelDocument/form/TravelDocumentFormContext';
 
 type Props = {|
   currency: string,
@@ -26,9 +27,11 @@ class ManageMyBookingPackage extends React.Component<Props> {
       <AuthContext.Provider accessToken={this.props.accessToken}>
         <WalletContext.Provider>
           <BookingDetailContext.Provider>
-            <NavigationStack
-              onNavigationStateChange={this.props.onNavigationStateChange}
-            />
+            <TravelDocumentFormContext.Provider>
+              <NavigationStack
+                onNavigationStateChange={this.props.onNavigationStateChange}
+              />
+            </TravelDocumentFormContext.Provider>
           </BookingDetailContext.Provider>
         </WalletContext.Provider>
       </AuthContext.Provider>
diff --git a/app/MMB/src/navigation/TravelDocumentModalScreen.js b/app/MMB/src/navigation/TravelDocumentModalScreen.js
index 0b2ee63c..20fc8e95 100644
--- a/app/MMB/src/navigation/TravelDocumentModalScreen.js
+++ b/app/MMB/src/navigation/TravelDocumentModalScreen.js
@@ -8,63 +8,39 @@ import {
   HeaderButton,
 } from '@kiwicom/mobile-navigation';
 import { Translation, DateFormatter } from '@kiwicom/mobile-localization';
-import {
-  StyleSheet,
-  TextIcon,
-  Color,
-  TextInput,
-  Text,
-  DatePicker,
-  Switch,
-  ErrorMessage,
-  IconLoading,
-} from '@kiwicom/mobile-shared';
+import { StyleSheet, Color, IconLoading } from '@kiwicom/mobile-shared';
+import { withAuthContext } from '@kiwicom/mobile-relay';
 
-import TitleTranslation from '../components/TitleTranslation';
 import updatePassengerMutation from './mutation/UpdatePassenger';
+import { withFormContext } from '../scenes/travelDocument/form/TravelDocumentFormContext';
+import TravelDocumentForm from '../scenes/travelDocument/form/TravelDocumentForm';
 
 type Props = {|
   +navigation: NavigationType,
-  +fullName: string,
   +title: string,
-  +idNumber: string | null,
-  +expiryDate: Date | null,
-  +bookingId: string,
+  +fullName: string,
   +passengerId: number,
-  +token: string,
+  +bookingId: string,
+  +accessToken: string,
+  +idNumber: string,
+  +expiryDate: Date | null,
+  +noExpiry: boolean,
+  +onIdNumberChange: (idNumber: string) => void,
+  +onDateChange: (date: Date) => void,
+  +onNoExpiryChange: (noExpiry: boolean) => void,
+  +reset: () => void,
 |};
 
 type State = {|
-  idNumber: string,
-  expiryDate: Date | null,
-  noExpiry: boolean,
   isSubmitting: boolean,
-  error: {|
-    idNumber: boolean,
-    expiryDate: boolean,
-  |},
 |};
 
 const noop = () => {};
 
-export default class TravelDocumentModalScreen extends React.Component<
-  Props,
-  State,
-> {
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      idNumber: props.idNumber || '',
-      expiryDate: props.expiryDate,
-      noExpiry: false,
-      isSubmitting: false,
-      error: {
-        idNumber: false,
-        expiryDate: true,
-      },
-    };
-  }
+export class TravelDocumentModalScreen extends React.Component<Props, State> {
+  state = {
+    isSubmitting: false,
+  };
 
   static navigationOptions = ({ navigation }: Props) => {
     function goBack() {
@@ -98,135 +74,67 @@ export default class TravelDocumentModalScreen extends React.Component<
     this.props.navigation.setParams({ onSave: this.onSave, disabled: true });
   };
 
-  onIdNumberChange = (idNumber: string) => {
-    this.setState(
-      state => ({
-        idNumber,
-        error: {
-          ...state.error,
-          idNumber: idNumber.length > 0 && idNumber.length < 5,
-        },
-      }),
-      this.validate,
-    );
-  };
-
-  onDateChange = (expiryDate: Date) => {
-    this.setState(
-      state => ({
-        expiryDate,
-        error: {
-          ...state.error,
-          expiryDate: false,
-        },
-      }),
-      this.validate,
-    );
+  componentDidUpdate = (prevProps: Props) => {
+    if (
+      prevProps.idNumber !== this.props.idNumber ||
+      prevProps.expiryDate !== this.props.expiryDate ||
+      prevProps.noExpiry !== this.props.noExpiry
+    ) {
+      const expiryError = this.props.noExpiry
+        ? false
+        : this.props.expiryDate === null;
+      const hasError = this.props.idNumber.length < 5 || expiryError;
+
+      this.props.navigation.setParams({
+        disabled: hasError,
+      });
+    }
   };
 
-  onNoExpiryChange = (noExpiry: boolean) => {
-    this.setState(
-      state => ({
-        noExpiry,
-        expiryDate: null,
-        error: {
-          ...state.error,
-          expiryDate: !noExpiry,
-        },
-      }),
-      this.validate,
-    );
+  componentWillUnmount = () => {
+    this.props.reset();
   };
 
   onSave = () => {
+    const { bookingId, passengerId, accessToken } = this.props;
     this.setState({ isSubmitting: true });
     this.props.navigation.setParams({ disabled: true });
     updatePassengerMutation(
       {
-        id: this.props.bookingId,
+        id: bookingId,
         passengers: [
           {
-            passengerId: this.props.passengerId,
+            passengerId,
             documentExpiry:
-              this.state.expiryDate === null
+              this.props.expiryDate === null
                 ? null
-                : DateFormatter(this.state.expiryDate).formatForMachine(),
-            documentNumber: this.state.idNumber,
+                : DateFormatter(this.props.expiryDate).formatForMachine(),
+            documentNumber: this.props.idNumber,
           },
         ],
       },
-      () => {
-        this.props.navigation.goBack();
+      (response, errors) => {
+        if (!errors) {
+          this.props.navigation.goBack();
+        } else {
+          // TODO: Show some error message
+        }
         this.setState({ isSubmitting: false });
         this.props.navigation.setParams({ disabled: false });
       },
-      this.props.token,
+      accessToken,
     );
   };
 
-  validate = () => {
-    // Not using this.state.error.idNumber because if this.state.idNumber.length === 0
-    // we want to disable save button, but not show error message under input
-    const hasError =
-      this.state.idNumber.length < 5 || this.state.error.expiryDate;
-    this.props.navigation.setParams({ disabled: hasError });
-  };
-
-  render = () => (
-    <View style={styles.container}>
-      <View style={styles.row}>
-        <TextIcon code="w" style={styles.icon} />
-        <TitleTranslation title={this.props.title} />
-        <Translation passThrough={' ' + this.props.fullName} />
-      </View>
-      <View style={styles.inputContainer}>
-        <Text style={styles.label}>
-          <Translation id="mmb.missing_informaiton.travel_document_modal_screen.passport_or_id_number" />
-        </Text>
-        <TextInput
-          autoFocus={true}
-          defaultValue={this.state.idNumber}
-          onChangeText={this.onIdNumberChange}
-        />
-        {this.state.error.idNumber && (
-          <ErrorMessage style={styles.error}>
-            <Translation id="mmb.missing_informaiton.travel_document_modal_screen.id_number_error" />
-          </ErrorMessage>
-        )}
+  render = () => {
+    const { title, fullName } = this.props;
+    return (
+      <View style={styles.container}>
+        <TravelDocumentForm fullName={fullName} title={title} />
+        {this.state.isSubmitting && <IconLoading />}
       </View>
-      <View style={styles.inputContainer}>
-        <Text
-          style={[
-            styles.label,
-            this.state.noExpiry ? styles.noExpiry : styles.expiry,
-          ]}
-        >
-          <Translation id="mmb.missing_informaiton.travel_document_modal_screen.passport_or_id_expiry" />
-        </Text>
-        <View style={styles.row}>
-          <View style={styles.datePickerContainer}>
-            <DatePicker
-              date={this.state.expiryDate}
-              onDateChange={this.onDateChange}
-              minDate={new Date()} // TODO: Should probably be the date of the last leg
-              formatFunction="formatToBirthday"
-              disabled={this.state.noExpiry}
-            />
-          </View>
-          <View style={[styles.row, styles.switchContainer]}>
-            <Text style={[styles.label, styles.switchText]}>
-              <Translation id="mmb.missing_informaiton.travel_document_modal_screen.no_expiry" />
-            </Text>
-            <Switch
-              value={this.state.noExpiry}
-              onValueChange={this.onNoExpiryChange}
-            />
-          </View>
-        </View>
-      </View>
-      {this.state.isSubmitting && <IconLoading />}
-    </View>
-  );
+    );
+  };
 }
 
 const styles = StyleSheet.create({
@@ -234,39 +142,6 @@ const styles = StyleSheet.create({
     padding: 15,
     backgroundColor: Color.white,
   },
-  icon: {
-    color: Color.black,
-    fontSize: 16,
-    paddingTop: 2,
-    marginEnd: 5,
-    alignSelf: 'flex-start',
-  },
-  row: {
-    flexDirection: 'row',
-  },
-  inputContainer: {
-    marginTop: 15,
-  },
-  label: {
-    color: Color.textLight,
-    marginBottom: 5,
-  },
-  switchContainer: {
-    alignItems: 'center',
-  },
-  switchText: {
-    marginHorizontal: 5,
-  },
-  datePickerContainer: {
-    flex: 1,
-  },
-  error: {
-    alignSelf: 'flex-end',
-  },
-  noExpiry: {
-    color: Color.labelDisabled,
-  },
-  expiry: {
-    color: Color.textLight,
-  },
 });
+
+export default withFormContext(withAuthContext(TravelDocumentModalScreen));
diff --git a/app/MMB/src/navigation/mutation/UpdatePassenger.js b/app/MMB/src/navigation/mutation/UpdatePassenger.js
index 839138b6..c2931981 100644
--- a/app/MMB/src/navigation/mutation/UpdatePassenger.js
+++ b/app/MMB/src/navigation/mutation/UpdatePassenger.js
@@ -15,9 +15,14 @@ const mutation = graphql`
   }
 `;
 
+type Callback = (
+  response: UpdatePassengerMutationResponse,
+  errors: ?[Error],
+) => void;
+
 export default (
   input: UpdatePassengerMutationVariables,
-  callback: (response: UpdatePassengerMutationResponse) => void,
+  callback: Callback,
   token: string,
 ) => {
   commitMutation(
@@ -26,7 +31,7 @@ export default (
       variables: input,
       updater: store => {
         const payload = store.getRootField('updatePassenger');
-        const success = payload.getValue('success');
+        const success = payload === null ? false : payload.getValue('success');
 
         if (success) {
           const booking = store.get(input.id);
@@ -47,8 +52,8 @@ export default (
           });
         }
       },
-      onCompleted: (response: UpdatePassengerMutationResponse) => {
-        callback(response);
+      onCompleted: (response: UpdatePassengerMutationResponse, errors) => {
+        callback(response, errors);
       },
     },
     token,
diff --git a/app/MMB/src/scenes/travelDocument/form/ExpiryDatePicker.js b/app/MMB/src/scenes/travelDocument/form/ExpiryDatePicker.js
new file mode 100644
index 00000000..8962c937
--- /dev/null
+++ b/app/MMB/src/scenes/travelDocument/form/ExpiryDatePicker.js
@@ -0,0 +1,76 @@
+// @flow strict
+
+import * as React from 'react';
+import { View } from 'react-native';
+import {
+  Text,
+  StyleSheet,
+  Color,
+  DatePicker,
+  Switch,
+} from '@kiwicom/mobile-shared';
+import { Translation } from '@kiwicom/mobile-localization';
+
+import TravelDocumentFormContext from './TravelDocumentFormContext';
+
+export default function ExpiryDatePicker() {
+  return (
+    <TravelDocumentFormContext.Consumer>
+      {({
+        noExpiry,
+        expiryDate,
+        actions: { onDateChange, onNoExpiryChange },
+      }) => (
+        <React.Fragment>
+          <Text
+            style={[styles.label, noExpiry ? styles.noExpiry : styles.expiry]}
+          >
+            <Translation id="mmb.missing_informaiton.travel_document_modal_screen.passport_or_id_expiry" />
+          </Text>
+          <View style={styles.row}>
+            <View style={styles.datePickerContainer}>
+              <DatePicker
+                date={expiryDate}
+                onDateChange={onDateChange}
+                minDate={new Date()} // TODO: Should probably be the date of the last leg
+                formatFunction="formatToBirthday"
+                disabled={noExpiry}
+              />
+            </View>
+            <View style={[styles.row, styles.switchContainer]}>
+              <Text style={[styles.label, styles.switchText]}>
+                <Translation id="mmb.missing_informaiton.travel_document_modal_screen.no_expiry" />
+              </Text>
+              <Switch value={noExpiry} onValueChange={onNoExpiryChange} />
+            </View>
+          </View>
+        </React.Fragment>
+      )}
+    </TravelDocumentFormContext.Consumer>
+  );
+}
+
+const styles = StyleSheet.create({
+  noExpiry: {
+    color: Color.labelDisabled,
+  },
+  expiry: {
+    color: Color.textLight,
+  },
+  label: {
+    color: Color.textLight,
+    marginBottom: 5,
+  },
+  switchContainer: {
+    alignItems: 'center',
+  },
+  switchText: {
+    marginHorizontal: 5,
+  },
+  datePickerContainer: {
+    flex: 1,
+  },
+  row: {
+    flexDirection: 'row',
+  },
+});
diff --git a/app/MMB/src/scenes/travelDocument/form/IdInput.js b/app/MMB/src/scenes/travelDocument/form/IdInput.js
new file mode 100644
index 00000000..e586c17e
--- /dev/null
+++ b/app/MMB/src/scenes/travelDocument/form/IdInput.js
@@ -0,0 +1,51 @@
+// @flow strict
+
+import * as React from 'react';
+import { Translation } from '@kiwicom/mobile-localization';
+import {
+  Text,
+  TextInput,
+  StyleSheet,
+  ErrorMessage,
+  Color,
+} from '@kiwicom/mobile-shared';
+
+import TravelDocumentFormContext from './TravelDocumentFormContext';
+
+const IdInput = () => (
+  <TravelDocumentFormContext.Consumer>
+    {({ idNumber, actions: { onIdNumberChange } }) => {
+      const idNumberLength = idNumber.length;
+      const showInputError = idNumberLength > 0 && idNumberLength < 5;
+      return (
+        <React.Fragment>
+          <Text style={styles.label}>
+            <Translation id="mmb.missing_informaiton.travel_document_modal_screen.passport_or_id_number" />
+          </Text>
+          <TextInput
+            autoFocus={true}
+            defaultValue={idNumber}
+            onChangeText={onIdNumberChange}
+          />
+          {showInputError && (
+            <ErrorMessage style={styles.error}>
+              <Translation id="mmb.missing_informaiton.travel_document_modal_screen.id_number_error" />
+            </ErrorMessage>
+          )}
+        </React.Fragment>
+      );
+    }}
+  </TravelDocumentFormContext.Consumer>
+);
+
+const styles = StyleSheet.create({
+  label: {
+    color: Color.textLight,
+    marginBottom: 5,
+  },
+  error: {
+    alignSelf: 'flex-end',
+  },
+});
+
+export default IdInput;
diff --git a/app/MMB/src/scenes/travelDocument/form/TravelDocumentForm.js b/app/MMB/src/scenes/travelDocument/form/TravelDocumentForm.js
new file mode 100644
index 00000000..4c69702f
--- /dev/null
+++ b/app/MMB/src/scenes/travelDocument/form/TravelDocumentForm.js
@@ -0,0 +1,49 @@
+// @flow strict
+
+import * as React from 'react';
+import { View } from 'react-native';
+import { Translation } from '@kiwicom/mobile-localization';
+import { StyleSheet, TextIcon, Color } from '@kiwicom/mobile-shared';
+
+import TitleTranslation from '../../../components/TitleTranslation';
+import IdInput from './IdInput';
+import ExpiryDatePicker from './ExpiryDatePicker';
+
+type Props = {|
+  +fullName: string,
+  +title: string,
+|};
+
+export default function TravelDocumentForm(props: Props) {
+  return (
+    <React.Fragment>
+      <View style={styles.row}>
+        <TextIcon code="w" style={styles.icon} />
+        <TitleTranslation title={props.title} />
+        <Translation passThrough={' ' + props.fullName} />
+      </View>
+      <View style={styles.inputContainer}>
+        <IdInput />
+      </View>
+      <View style={styles.inputContainer}>
+        <ExpiryDatePicker />
+      </View>
+    </React.Fragment>
+  );
+}
+
+const styles = StyleSheet.create({
+  icon: {
+    color: Color.black,
+    fontSize: 16,
+    paddingTop: 2,
+    marginEnd: 5,
+    alignSelf: 'flex-start',
+  },
+  row: {
+    flexDirection: 'row',
+  },
+  inputContainer: {
+    marginTop: 15,
+  },
+});
diff --git a/app/MMB/src/scenes/travelDocument/form/TravelDocumentFormContext.js b/app/MMB/src/scenes/travelDocument/form/TravelDocumentFormContext.js
new file mode 100644
index 00000000..8beaca76
--- /dev/null
+++ b/app/MMB/src/scenes/travelDocument/form/TravelDocumentFormContext.js
@@ -0,0 +1,92 @@
+// @flow strict
+
+import * as React from 'react';
+
+const defaultState = {
+  idNumber: '',
+  expiryDate: null,
+  noExpiry: false,
+  actions: {
+    onIdNumberChange: () => {},
+    onDateChange: () => {},
+    onNoExpiryChange: () => {},
+    reset: () => {},
+  },
+};
+
+const { Consumer, Provider: ContextProvider } = React.createContext({
+  ...defaultState,
+});
+
+type Props = {|
+  +children: React.Node,
+|};
+
+type State = {|
+  idNumber: string,
+  expiryDate: Date | null,
+  noExpiry: boolean,
+  +actions: {
+    +onIdNumberChange: (idNumber: string) => void,
+    +onDateChange: (date: Date) => void,
+    +onNoExpiryChange: (noExipry: boolean) => void,
+    +reset: () => void,
+  },
+|};
+
+class Provider extends React.Component<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      idNumber: '',
+      expiryDate: null,
+      noExpiry: false,
+      actions: {
+        onIdNumberChange: this.onIdNumberChange,
+        onDateChange: this.onDateChange,
+        onNoExpiryChange: this.onNoExpiryChange,
+        reset: this.reset,
+      },
+    };
+  }
+
+  onIdNumberChange = (idNumber: string) => {
+    this.setState({ idNumber });
+  };
+
+  onDateChange = (expiryDate: Date) => {
+    this.setState({ expiryDate });
+  };
+
+  onNoExpiryChange = (noExpiry: boolean) => {
+    this.setState({ noExpiry, expiryDate: null });
+  };
+
+  reset = () => {
+    this.setState({ idNumber: '', noExpiry: false, expiryDate: null });
+  };
+
+  render = () => (
+    <ContextProvider value={this.state}>{this.props.children}</ContextProvider>
+  );
+}
+
+export default { Consumer, Provider };
+
+type PropsWithContext = {};
+
+export function withFormContext(Component: React.ElementType) {
+  const WithFormContext = (props: PropsWithContext) => (
+    <Consumer>
+      {({ actions, ...rest }) => (
+        <Component {...props} {...rest} {...actions} />
+      )}
+    </Consumer>
+  );
+  // $FlowExpectedError: We need to pass on the navigationOptions if any, flow does not know about it, but a react component might have it
+  if (Component.navigationOptions) {
+    WithFormContext.navigationOptions = Component.navigationOptions;
+  }
+  return WithFormContext;
+}
diff --git a/app/MMB/src/scenes/travelDocument/menuItem/TravelDocumentPassengerMenuItem.js b/app/MMB/src/scenes/travelDocument/menuItem/TravelDocumentPassengerMenuItem.js
index 4b226aee..8ab5872e 100644
--- a/app/MMB/src/scenes/travelDocument/menuItem/TravelDocumentPassengerMenuItem.js
+++ b/app/MMB/src/scenes/travelDocument/menuItem/TravelDocumentPassengerMenuItem.js
@@ -2,11 +2,7 @@
 
 import * as React from 'react';
 import idx from 'idx';
-import {
-  createFragmentContainer,
-  graphql,
-  AuthContext,
-} from '@kiwicom/mobile-relay';
+import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import {
   type NavigationType,
   withNavigation,
@@ -21,7 +17,6 @@ import BookingDetailContext from '../../../context/BookingDetailContext';
 type PropsWithContext = {|
   ...Props,
   +bookingId: string,
-  +token: string,
 |};
 
 const TravelDocumentPassengerMenuItem = (props: PropsWithContext) => {
@@ -35,11 +30,8 @@ const TravelDocumentPassengerMenuItem = (props: PropsWithContext) => {
     props.navigation.navigate('TravelDocumentModalScreen', {
       title,
       fullName,
-      idNumber,
-      expiryDate,
       passengerId,
       bookingId: props.bookingId,
-      token: props.token,
     });
   }
 
@@ -64,15 +56,7 @@ type Props = {|
 const TravelDocumentPassengerMenuItemWithContext = (props: Props) => (
   <BookingDetailContext.Consumer>
     {({ bookingId }) => (
-      <AuthContext.Consumer>
-        {({ accessToken }) => (
-          <TravelDocumentPassengerMenuItem
-            {...props}
-            bookingId={bookingId}
-            token={accessToken || ''}
-          />
-        )}
-      </AuthContext.Consumer>
+      <TravelDocumentPassengerMenuItem {...props} bookingId={bookingId} />
     )}
   </BookingDetailContext.Consumer>
 );
diff --git a/packages/relay/index.js b/packages/relay/index.js
index 3ef2776f..ec5a0cd6 100644
--- a/packages/relay/index.js
+++ b/packages/relay/index.js
@@ -13,7 +13,7 @@ export {
 } from 'react-relay';
 export { default as PublicApiRenderer } from './src/PublicApiRenderer';
 export { default as PrivateApiRenderer } from './src/PrivateApiRenderer';
-export { default as AuthContext } from './src/AuthContext';
+export { default as AuthContext, withAuthContext } from './src/AuthContext';
 
 // just a very simple Flow types - doesn't work very well yet (better than nothing)
 export const createFragmentContainer = (
diff --git a/packages/relay/src/AuthContext.js b/packages/relay/src/AuthContext.js
index d68a079c..f5c6decd 100644
--- a/packages/relay/src/AuthContext.js
+++ b/packages/relay/src/AuthContext.js
@@ -47,3 +47,24 @@ class Provider extends React.Component<Props, State> {
 }
 
 export default { Consumer, Provider };
+
+type PropsWithContext = {};
+
+export function withAuthContext(Component: React.ElementType) {
+  const WithAuthContext = (props: PropsWithContext) => (
+    <Consumer>
+      {({ accessToken, actions: { setAccessToken } }) => (
+        <Component
+          {...props}
+          accessToken={accessToken}
+          setAccessToken={setAccessToken}
+        />
+      )}
+    </Consumer>
+  );
+  // $FlowExpectedError: We need to pass on the navigationOptions if any, flow does not know about it, but a react component might have it
+  if (Component.navigationOptions) {
+    WithAuthContext.navigationOptions = Component.navigationOptions;
+  }
+  return WithAuthContext;
+}
diff --git a/packages/shared/src/forms/Switch.js b/packages/shared/src/forms/Switch.js
index 43d36f89..f4016934 100644
--- a/packages/shared/src/forms/Switch.js
+++ b/packages/shared/src/forms/Switch.js
@@ -1,7 +1,7 @@
 // @flow strict
 
 import * as React from 'react';
-import { Switch as OriginalSwitch } from 'react-native';
+import { Switch as OriginalSwitch } from 'react-native'; // eslint-disable-line no-restricted-imports
 
 import Color from '../Color';
 


Test patch:
diff --git a/app/MMB/src/navigation/__tests__/TravelDocumentModalScreen.test.js b/app/MMB/src/navigation/__tests__/TravelDocumentModalScreen.test.js
index b5e2d201..be6fe7b8 100644
--- a/app/MMB/src/navigation/__tests__/TravelDocumentModalScreen.test.js
+++ b/app/MMB/src/navigation/__tests__/TravelDocumentModalScreen.test.js
@@ -1,84 +1,94 @@
 // @flow strict
 
-import * as React from 'react';
-import renderer from 'react-test-renderer';
-
-import TravelDocumentModalScreen from '../TravelDocumentModalScreen';
-
-jest.mock('../../components/TitleTranslation.js', () => () => null);
-jest.mock('DatePickerIOS');
+import { TravelDocumentModalScreen } from '../TravelDocumentModalScreen';
 
 const navigation = {
   setParams: jest.fn(),
 };
 
-const getInstance = () =>
-  renderer
-    .create(
-      // $FlowExpectedError: All props not needed for this test
-      <TravelDocumentModalScreen expiryDate={null} navigation={navigation} />,
-    )
-    .getInstance();
-
 afterEach(() => {
   jest.resetAllMocks();
 });
 
+const getComponent = props =>
+  // $FlowExpectedError: Passing just props needed to run test
+  new TravelDocumentModalScreen({
+    ...props,
+    navigation,
+  });
+
 describe('TravelDocumentModalScreen', () => {
-  it('should validate onIdNumberChange correctly', () => {
-    const instance = getInstance();
-    expect(instance.state.error.idNumber).toBe(false);
+  it('should call navigation setParams when a idNumber changes', () => {
+    const Component = getComponent({
+      idNumber: 'a',
+    });
+
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({ idNumber: 'ab' });
+    expect(navigation.setParams).toBeCalledWith({ disabled: true });
+  });
 
-    instance.onIdNumberChange('1');
-    expect(instance.state.error.idNumber).toBe(true);
+  it('should call navigation setParams when a expiryDate changes', () => {
+    const Component = getComponent({
+      idNumber: 'a',
+    });
 
-    instance.onIdNumberChange('12345');
-    expect(instance.state.error.idNumber).toBe(false);
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({ expiryDate: new Date() });
+    expect(navigation.setParams).toBeCalledWith({ disabled: true });
   });
 
-  it('should validate onDateChange correctly', () => {
-    const instance = getInstance();
-    expect(instance.state.error.expiryDate).toBe(true);
+  it('should call navigation setParams when a noExpiry changes', () => {
+    const Component = getComponent({
+      noExpiry: false,
+      idNumber: '',
+    });
 
-    instance.onDateChange(new Date());
-    expect(instance.state.error.expiryDate).toBe(false);
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({ noExpiry: true, idNumber: '' });
+    expect(navigation.setParams).toBeCalledWith({ disabled: true });
   });
 
-  it('should validate onNoExpiryChange correctly', () => {
-    const instance = getInstance();
-    expect(instance.state.error.expiryDate).toBe(true);
-
-    instance.onDateChange(new Date());
-    expect(instance.state.error.expiryDate).toBe(false);
+  it('should not call set params if idNumber and expiry date remain unchanged', () => {
+    const Component = getComponent({
+      idNumber: 'a',
+      expiryDate: null,
+    });
+
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({
+      idNumber: 'a',
+      expiryDate: null,
+    });
+    expect(navigation.setParams).not.toHaveBeenCalled();
+  });
 
-    instance.onNoExpiryChange(false);
-    expect(instance.state.error.expiryDate).toBe(true);
+  it('should enable save button if idNumber > 5 && expiryDate is set', () => {
+    const Component = getComponent({
+      idNumber: '123456',
+      expiryDate: new Date(),
+    });
+
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({
+      idNumber: '123456',
+      expiryDate: null,
+    });
+    expect(navigation.setParams).toBeCalledWith({ disabled: false });
   });
 
-  it('should disable header button correctly', () => {
-    const instance = getInstance();
-    jest.resetAllMocks();
-
-    instance.onIdNumberChange('12345');
-    expect(navigation.setParams).toHaveBeenCalledWith({ disabled: true });
-    expect(navigation.setParams).toHaveBeenCalledTimes(1);
-    jest.resetAllMocks();
-
-    instance.onDateChange(new Date());
-    expect(navigation.setParams).toHaveBeenCalledWith({ disabled: false });
-    expect(navigation.setParams).toHaveBeenCalledTimes(1);
-    jest.resetAllMocks();
-
-    instance.onIdNumberChange('2');
-    expect(navigation.setParams).toHaveBeenCalledWith({ disabled: true });
-    expect(navigation.setParams).toHaveBeenCalledTimes(1);
-    jest.resetAllMocks();
-
-    instance.onIdNumberChange('12345');
-    jest.resetAllMocks();
-    instance.onNoExpiryChange(true);
-    expect(navigation.setParams).toHaveBeenCalledWith({ disabled: false });
-    expect(navigation.setParams).toHaveBeenCalledTimes(1);
-    jest.resetAllMocks();
+  it('should enable save button if idNumber > 5 && noExpiry is true', () => {
+    const Component = getComponent({
+      idNumber: '123456',
+      expiryDate: null,
+      noExpiry: true,
+    });
+
+    // $FlowExpectedError: Passing just props needed to run test
+    Component.componentDidUpdate({
+      idNumber: '12345',
+      expiryDate: null,
+    });
+    expect(navigation.setParams).toBeCalledWith({ disabled: false });
   });
 });