Repository: kiwicom/mobile
Pull request #1042 (2018-08-29 13:49:37)

iOS card module


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 8dd40f4a..b348e73b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -34,6 +34,8 @@ module.exports = {
     'prettier/flowtype',
   ],
   rules: {
+    'react/no-did-update-set-state': ERROR,
+    'prefer-const': ERROR,
     'curly': ERROR,
     'no-console': [ERROR, { allow: ['warn', 'error'] }],
     'no-duplicate-imports': ERROR,
diff --git a/app/MMB/package.json b/app/MMB/package.json
index 1d44f1f3..78e32db9 100644
--- a/app/MMB/package.json
+++ b/app/MMB/package.json
@@ -7,11 +7,12 @@
     "@kiwicom/mobile-assets": "^0",
     "@kiwicom/mobile-localization": "^0",
     "@kiwicom/mobile-navigation": "^0",
+    "@kiwicom/mobile-orbit": "^0",
     "@kiwicom/mobile-pdf": "^0",
     "@kiwicom/mobile-relay": "^0",
     "@kiwicom/mobile-shared": "^0",
-    "@kiwicom/mobile-orbit": "^0",
     "lodash": "^4.17.10",
+    "memoize-one": "^4.0.0",
     "react-native-dash": "^0.0.8",
     "react-native-share": "^1.0.27"
   },
diff --git a/app/MMB/src/scenes/tripOverview/TimelineTitle.js b/app/MMB/src/scenes/tripOverview/TimelineTitle.js
index 471c6362..3d7dc468 100644
--- a/app/MMB/src/scenes/tripOverview/TimelineTitle.js
+++ b/app/MMB/src/scenes/tripOverview/TimelineTitle.js
@@ -12,6 +12,7 @@ import {
 import { StyleSheet, Text, Touchable, TextIcon } from '@kiwicom/mobile-shared';
 import idx from 'idx';
 import { defaultTokens } from '@kiwicom/mobile-orbit';
+import memoize from 'memoize-one';
 
 import TripOverviewContext from './TripOverviewContext';
 import type { TimelineTitle as TimelineTitleDataType } from './__generated__/TimelineTitle.graphql';
@@ -29,38 +30,25 @@ type PropsWithContext = {|
   +warnings: ?(Warning[]),
 |};
 
-type State = {|
-  warning: ?Warning,
-|};
-
-class TimelineTitle extends React.Component<PropsWithContext, State> {
-  constructor(props) {
-    super(props);
-    this.state = {
-      warning: null,
-    };
-  }
-  componentDidUpdate(prevProps) {
-    const { warnings } = this.props;
-    if (prevProps.warnings && warnings) {
-      if (prevProps.warnings.length !== warnings.length) {
-        const data = this.props.data;
-        const localTime = idx(data, _ => _.localTime);
-        const iataCode = idx(data, _ => _.airport.locationId);
-
-        const warning = warnings.find(
-          _warning =>
-            _warning.timelineTitle.localTime == localTime &&
-            _warning.timelineTitle.iataCode == iataCode,
-        );
-        this.setState({ warning });
-      }
-    }
-  }
+class TimelineTitle extends React.Component<PropsWithContext> {
+  getWarning = memoize(
+    (warnings: $ReadOnlyArray<Warning>, localTime: ?Date, iataCode: ?string) =>
+      warnings.find(
+        _warning =>
+          _warning.timelineTitle.localTime == localTime &&
+          _warning.timelineTitle.iataCode == iataCode,
+      ),
+  );
 
   onPress = () => {
-    if (this.state.warning) {
-      Alert.translatedAlert(undefined, this.state.warning.text);
+    const data = this.props.data;
+    const localTime = idx(data, _ => _.localTime);
+    const iataCode = idx(data, _ => _.airport.locationId);
+    const warnings = idx(this.props, _ => _.warnings) || [];
+
+    const warning = this.getWarning(warnings, localTime, iataCode);
+    if (warning) {
+      Alert.translatedAlert(undefined, warning.text);
     }
   };
 
@@ -69,7 +57,10 @@ class TimelineTitle extends React.Component<PropsWithContext, State> {
     const localTime = idx(data, _ => _.localTime);
     const cityName = idx(data, _ => _.airport.city.name);
     const iataCode = idx(data, _ => _.airport.locationId);
-    const warningStyle = this.state.warning ? styleSheet.warningStyle : null;
+    const warnings = idx(this.props, _ => _.warnings) || [];
+    const warning = this.getWarning(warnings, localTime, iataCode);
+
+    const warningStyle = warning != null ? styleSheet.warningStyle : null;
     return (
       <View style={styleSheet.row}>
         <View style={styleSheet.dateTime}>
@@ -90,7 +81,7 @@ class TimelineTitle extends React.Component<PropsWithContext, State> {
           )}
         </View>
 
-        <Touchable onPress={this.onPress} disabled={!this.state.warning}>
+        <Touchable onPress={this.onPress} disabled={warning == null}>
           <View style={styleSheet.warningRow}>
             <Translation passThrough={cityName} />
             <Translation passThrough=" " />
@@ -98,9 +89,9 @@ class TimelineTitle extends React.Component<PropsWithContext, State> {
               <Translation passThrough={iataCode} />
             </Text>
             <Translation passThrough=" " />
-            {this.state.warning ? (
+            {warning != null && (
               <TextIcon code="4" style={styleSheet.warningStyle} />
-            ) : null}
+            )}
           </View>
         </Touchable>
       </View>
diff --git a/app/MMB/src/scenes/tripServices/transportationMenuItem/TransportationMenuItem.js b/app/MMB/src/scenes/tripServices/transportationMenuItem/TransportationMenuItem.js
index a0d052db..f75d0801 100644
--- a/app/MMB/src/scenes/tripServices/transportationMenuItem/TransportationMenuItem.js
+++ b/app/MMB/src/scenes/tripServices/transportationMenuItem/TransportationMenuItem.js
@@ -52,7 +52,7 @@ class TransportationMenuItem extends React.Component<Props, State> {
       const whitelabelURL =
         idx(relevantLocations, _ => _[0].whitelabelURL) || '';
 
-      let location = idx(relevantLocations, _ => _[0].location.location);
+      const location = idx(relevantLocations, _ => _[0].location.location);
 
       if (location != null) {
         this.openTransportationMap({ location, whitelabelURL });
diff --git a/app/hotels/src/allHotels/searchForm/locationPicker/LocationPicker.js b/app/hotels/src/allHotels/searchForm/locationPicker/LocationPicker.js
index 4323f265..37978dbd 100644
--- a/app/hotels/src/allHotels/searchForm/locationPicker/LocationPicker.js
+++ b/app/hotels/src/allHotels/searchForm/locationPicker/LocationPicker.js
@@ -46,7 +46,7 @@ export class LocationPicker extends React.Component<PropsWithContext, State> {
   };
 
   saveRecentSearches = (cityId: string, cityName: string) => {
-    let recentSearches = this.props.storageValue;
+    const recentSearches = this.props.storageValue;
 
     if (recentSearches.find(item => item.id === cityId) === undefined) {
       recentSearches.unshift({ id: cityId, name: cityName });
diff --git a/app/hotels/src/navigation/allHotels/GuestsModalScreen.js b/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
index 0c5685e5..364d84ca 100644
--- a/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
+++ b/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
@@ -34,20 +34,17 @@ export class GuestsModalScreen extends React.Component<
   PropsWithContext,
   State,
 > {
-  state = {
-    guests: {
-      adultsCount: 0,
-      children: [],
-    },
-    isMissingAge: false,
-  };
+  constructor(props: PropsWithContext) {
+    super(props);
 
-  componentDidMount = () => {
-    this.setState({
-      guests: ((this.props.guests: any): UnsavedRoomConfigurationType),
-    });
+    this.state = {
+      guests: (props.guests: any),
+      isMissingAge: false,
+    };
+  }
+  componentDidMount() {
     this.props.navigation.setParams({ onSave: this.onSave });
-  };
+  }
 
   isMissingAge = (children: ChildAge[]) => {
     return children.some(child => child.age === null);
diff --git a/app/hotels/src/search/SearchQueryHelpers.js b/app/hotels/src/search/SearchQueryHelpers.js
index d64e19b8..135fb7f4 100644
--- a/app/hotels/src/search/SearchQueryHelpers.js
+++ b/app/hotels/src/search/SearchQueryHelpers.js
@@ -22,7 +22,7 @@ export const getSearchQueryParams = (
   coordinates: Coordinates | null,
   cityId: ?string,
 ): Object => {
-  let params: Object = {
+  const params: Object = {
     ...search,
     checkin: sanitizeDate(search.checkin),
     checkout: sanitizeDate(search.checkout),
diff --git a/app/hotels/src/singleHotel/roomList/RoomBadges.js b/app/hotels/src/singleHotel/roomList/RoomBadges.js
index fcbb1a20..1e93f1f9 100644
--- a/app/hotels/src/singleHotel/roomList/RoomBadges.js
+++ b/app/hotels/src/singleHotel/roomList/RoomBadges.js
@@ -30,7 +30,7 @@ export const RoomBadges = (props: Props) => {
   );
   const isRefundable = idx(props.availableRoom, _ => _.isRefundable);
 
-  let badges = [];
+  const badges = [];
 
   if (isBreakfastIncluded) {
     badges.push(
diff --git a/ios/Podfile b/ios/Podfile
index c88f32bb..99b32b62 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -40,7 +40,8 @@ def react_native_modules()
     'RNTranslationManager',
     'RNCurrencyManager',
     'RNDeviceInfo',
-    'RNAppleWallet'
+    'RNAppleWallet',
+    'RNCardManager'
   ]
 end
 
@@ -62,5 +63,4 @@ post_install do |installer|
       end
     end
   end
-
 end
diff --git a/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.h b/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.h
new file mode 100644
index 00000000..03df64e1
--- /dev/null
+++ b/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.h
@@ -0,0 +1,14 @@
+#import <Foundation/Foundation.h>
+
+@protocol RNCardContainer <NSObject>
+
+- (void)saveCard:(nonnull NSDictionary *)card;
+- (NSDictionary *)getCard;
+
+@end
+
+@interface RNCardManager : NSObject
+
++ (void)setCardSaver:(nullable id<RNCardContainer>)cardSaver;
+
+@end
diff --git a/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.m b/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.m
new file mode 100644
index 00000000..4a8e4e30
--- /dev/null
+++ b/ios/RNModules/RNCardManager/RNCardManager/RNCardManager.m
@@ -0,0 +1,68 @@
+#import "RNCardManager.h"
+
+#import <React/RCTBridgeModule.h>
+
+@interface RNDummyCard: NSObject <RNCardContainer>
+@end
+
+@implementation RNDummyCard
+
+- (void)saveCard:(NSDictionary *)card {
+    [[NSUserDefaults standardUserDefaults] setValue:card
+                                             forKey:@"userCard"];
+}
+
+- (NSDictionary *)getCard {
+    return [[NSUserDefaults standardUserDefaults] valueForKey:@"userCard"];
+}
+
+@end
+
+@interface RNCardManager() <RCTBridgeModule>
+
+@property (nonatomic, strong, nonnull) id<RNCardContainer> cardSaver;
+
+@end
+
+@implementation RNCardManager
+
+- (instancetype)init {
+    static dispatch_once_t pred = 0;
+    static id _sharedObject = nil;
+    dispatch_once(&pred, ^{
+        _sharedObject = [super init];
+    });
+    return _sharedObject;
+}
+
++ (void)setCardSaver:(id<RNCardContainer>)cardSaver {
+    [[self new] setCardSaver:cardSaver];
+}
+
+- (BOOL)requiresMainQueueSetup {
+    return YES;
+}
+
+RCT_EXPORT_MODULE();
+
+- (id<RNCardContainer>)cardSaver {
+    if (!_cardSaver) {
+        return [[RNDummyCard alloc] init];
+    }
+    return _cardSaver;
+}
+
+RCT_EXPORT_METHOD(saveCard:(NSDictionary *)card
+                  resolver:(RCTPromiseResolveBlock)resolve
+                  rejecter:(RCTPromiseRejectBlock)reject) {
+   [self.cardSaver saveCard:card];
+}
+
+RCT_EXPORT_METHOD(getCard:(RCTPromiseResolveBlock)resolve
+                  rejecter:(RCTPromiseRejectBlock)reject) {
+    NSDictionary *userCard = [self.cardSaver getCard];
+    resolve(userCard);
+}
+
+@end
+
diff --git a/ios/RNModules/RNModules.podspec b/ios/RNModules/RNModules.podspec
index 2da36714..68b329f4 100644
--- a/ios/RNModules/RNModules.podspec
+++ b/ios/RNModules/RNModules.podspec
@@ -35,4 +35,8 @@ Pod::Spec.new do |s|
   s.subspec "RNAppleWallet" do |ss|
     ss.source_files   = 'RNAppleWallet/RNAppleWallet/*.{h,m}'
   end
+  
+  s.subspec "RNCardManager" do |ss|
+    ss.source_files   = 'RNCardManager/RNCardManager/*.{h,m}'
+  end
 end
diff --git a/packages/localization/package.json b/packages/localization/package.json
index 5b08d8ec..b64c6b85 100644
--- a/packages/localization/package.json
+++ b/packages/localization/package.json
@@ -5,6 +5,7 @@
   "main": "index.js",
   "dependencies": {
     "@kiwicom/mobile-shared": "^0",
+    "@kiwicom/rnmodules": "^0",
     "intl": "^1.2.5"
   },
   "devDependencies": {
diff --git a/packages/localization/src/Alert.js b/packages/localization/src/Alert.js
index 2092307c..ec97b154 100644
--- a/packages/localization/src/Alert.js
+++ b/packages/localization/src/Alert.js
@@ -1,10 +1,13 @@
 // @flow
 
 import { Alert as AlertNative } from 'react-native'; // eslint-disable-line no-restricted-imports
+import { translate } from '@kiwicom/rnmodules';
 
 import { type TranslationKeys } from './DefaultVocabulary';
-import { type TranslationPromise } from './CancellableTranslation';
-import { setTranslatedString, replaceValues } from './TranslationHelpers';
+import {
+  replaceValues,
+  getTranslation as prepareTranslation,
+} from './TranslationHelpers';
 
 export type Translation =
   | {|
@@ -37,12 +40,13 @@ type AlertButtonStyle = 'default' | 'cancel' | 'destructive';
 type AlertType = 'default' | 'plain-text' | 'secure-text' | 'login-password';
 
 const getTranslation = async (translation: ?Translation) => {
-  let translatePromise: TranslationPromise | null = null;
   if (translation && translation.id) {
-    const translatedTitle = await setTranslatedString(
+    const nativeKey = `mobile.${translation.id}`;
+    const translatedString = await translate(nativeKey);
+
+    const translatedTitle = prepareTranslation(
+      translatedString,
       translation.id,
-      translatePromise,
-      translatedString => translatedString,
     );
     if (translatedTitle) {
       return replaceValues(translatedTitle, translation.values);
@@ -64,7 +68,7 @@ export default class Alert extends AlertNative {
     const translatedTitleWithValues = await getTranslation(title);
     const translatedMessageWithValues = await getTranslation(message);
 
-    let translatedButtons: NativeAlertButtons = [];
+    const translatedButtons: NativeAlertButtons = [];
     if (buttons && buttons.length >= 1) {
       await Promise.all(
         buttons.map(async button => {
diff --git a/packages/localization/src/Translation.js b/packages/localization/src/Translation.js
index 73e091ed..a46ee7a4 100644
--- a/packages/localization/src/Translation.js
+++ b/packages/localization/src/Translation.js
@@ -7,8 +7,11 @@ import { type TranslationKeys } from './DefaultVocabulary';
 import CaseTransform, {
   type SupportedTransformationsType,
 } from './transformations/CaseTransform';
-import { type TranslationPromise } from './CancellableTranslation';
-import { setTranslatedString, replaceValues } from './TranslationHelpers';
+import {
+  type TranslationPromise,
+  cancellableTranslation,
+} from './CancellableTranslation';
+import { getTranslation, replaceValues } from './TranslationHelpers';
 
 type CommonProps = {|
   testID?: string,
@@ -76,13 +79,22 @@ export default class Translation extends React.Component<Props, State> {
     }
   };
 
-  setTranslatedString = () => {
+  setTranslatedString = async () => {
     if (this.props.id) {
-      setTranslatedString(
-        this.props.id,
-        this.translatePromise,
-        translatedString => this.setState({ translatedString }),
-      );
+      const key = this.props.id;
+      const nativeKey = 'mobile.' + key;
+      try {
+        this.translatePromise = cancellableTranslation(nativeKey);
+        const translatedString = await this.translatePromise.promise;
+
+        this.setState({
+          translatedString: getTranslation(translatedString, key),
+        });
+
+        this.translatePromise = null;
+      } catch (err) {
+        this.translatePromise = null;
+      }
     }
   };
 
diff --git a/packages/localization/src/TranslationHelpers.js b/packages/localization/src/TranslationHelpers.js
index 44a94503..b88b13bb 100644
--- a/packages/localization/src/TranslationHelpers.js
+++ b/packages/localization/src/TranslationHelpers.js
@@ -1,37 +1,24 @@
 //@flow
 
 import DefaultVocabulary, { type TranslationKeys } from './DefaultVocabulary';
-import {
-  cancellableTranslation,
-  type TranslationPromise,
-} from './CancellableTranslation';
 
-export const setTranslatedString = async (
-  id: TranslationKeys,
-  translatePromiseRef: TranslationPromise | null = null,
-  callback: string => void | string,
-) => {
-  if (id) {
-    const nativeKey = 'mobile.' + id;
-    try {
-      translatePromiseRef = cancellableTranslation(nativeKey);
-      let translatedString = await translatePromiseRef.promise;
-      if (translatedString === nativeKey) {
-        // fallback to our dummy vocabulary because native code didn't provide translation
-        translatedString = DefaultVocabulary[id];
-      }
-      if (translatedString === undefined) {
-        // Everything failed - native translation even our fallback. This usually
-        // means we are using key that does not exist. In this case the only thing
-        // we can actually do is to use original key as a text.
-        translatedString = id;
-      }
-      translatePromiseRef = null;
-      return callback(translatedString);
-    } catch (err) {
-      translatePromiseRef = null;
-    }
+export const getTranslation = (
+  translatedString: ?string,
+  translationId: TranslationKeys,
+): string => {
+  const nativeKey = `mobile.${translationId}`;
+
+  if (translatedString === nativeKey) {
+    // fallback to our dummy vocabulary because native code didn't provide translation
+    return DefaultVocabulary[translationId];
+  }
+  if (translatedString == null) {
+    // Everything failed - native translation even our fallback. This usually
+    // means we are using key that does not exist. In this case the only thing
+    // we can actually do is to use original key as a text.
+    return translationId;
   }
+  return translatedString;
 };
 
 export function replaceValues(text: string, values?: Object): string {
diff --git a/packages/rnmodules/card/CardManager.js b/packages/rnmodules/card/CardManager.js
index 44742d73..0609f0ee 100644
--- a/packages/rnmodules/card/CardManager.js
+++ b/packages/rnmodules/card/CardManager.js
@@ -2,12 +2,12 @@
 
 import { NativeModules } from 'react-native';
 
-type CardType = {
-  cardholder: string,
-  expiryMonth: string,
-  expiryYear: string,
-  number: string,
-};
+type CardType = {|
+  +cardholder: string,
+  +expiryMonth: string,
+  +expiryYear: string,
+  +number: string,
+|};
 
 export const getCard = (): Promise<CardType> | null => {
   return NativeModules.RNCardManager.getCard();
diff --git a/packages/rnmodules/translation/TranslationManager.js b/packages/rnmodules/translation/TranslationManager.js
index cd3ccfe0..b65b73dd 100644
--- a/packages/rnmodules/translation/TranslationManager.js
+++ b/packages/rnmodules/translation/TranslationManager.js
@@ -2,6 +2,6 @@
 
 import { NativeModules } from 'react-native';
 
-export const translate = (key: string): Promise<String> => {
+export const translate = (key: string): Promise<string> => {
   return NativeModules.RNTranslationManager.translate(key);
 };
diff --git a/packages/shared/src/PlatformStyleSheet.js b/packages/shared/src/PlatformStyleSheet.js
index 9c61e489..021ce2c2 100644
--- a/packages/shared/src/PlatformStyleSheet.js
+++ b/packages/shared/src/PlatformStyleSheet.js
@@ -37,7 +37,7 @@ export default {
   create(styles: PlatformStyleObjectType): StyleObjectType {
     const platformStyles = {};
     Object.keys(styles).forEach(name => {
-      let { ios, android, ...style } = { ...styles[name] };
+      let { ios, android, ...style } = { ...styles[name] }; // eslint-disable-line prefer-const
       if (ios && Platform.OS === 'ios') {
         style = { ...style, ...ios };
       }
diff --git a/packages/shared/src/forms/datePicker/DatePicker.ios.js b/packages/shared/src/forms/datePicker/DatePicker.ios.js
index c2e28826..a41bf403 100644
--- a/packages/shared/src/forms/datePicker/DatePicker.ios.js
+++ b/packages/shared/src/forms/datePicker/DatePicker.ios.js
@@ -18,10 +18,14 @@ type State = {|
 |};
 
 export default class DatePicker extends React.Component<Props, State> {
-  state = {
-    isPickerOpen: false,
-    date: new Date(),
-  };
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      isPickerOpen: false,
+      date: props.date || DateUtils.getUTCToday(),
+    };
+  }
 
   static getDerivedStateFromProps = ({ date }: Props) => ({ date });
 
@@ -29,10 +33,6 @@ export default class DatePicker extends React.Component<Props, State> {
     disabled: false,
   };
 
-  componentDidMount = () => {
-    this.setState({ date: this.props.date || DateUtils.getUTCToday() });
-  };
-
   togglePopup = () => {
     this.setState(state => ({
       isPickerOpen: !state.isPickerOpen,
diff --git a/scripts/releaseFramework.js b/scripts/releaseFramework.js
index 894793ae..73df43ff 100755
--- a/scripts/releaseFramework.js
+++ b/scripts/releaseFramework.js
@@ -14,7 +14,7 @@ const child_process = require('child_process');
 const fetch = require('node-fetch');
 const path = require('path');
 
-let version = child_process
+const version = child_process
   .execSync('git rev-parse --short HEAD')
   .toString()
   .trim();
diff --git a/yarn.lock b/yarn.lock
index c607a877..8c9157ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4794,6 +4794,10 @@ mem@^1.1.0:
   dependencies:
     mimic-fn "^1.0.0"
 
+memoize-one@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.0.tgz#fc5e2f1427a216676a62ec652cf7398cfad123db"
+
 merge-stream@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"


Test patch:
diff --git a/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/GuestsModalScreen.test.js.snap b/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/GuestsModalScreen.test.js.snap
index 16aa64ad..f8a507ae 100644
--- a/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/GuestsModalScreen.test.js.snap
+++ b/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/GuestsModalScreen.test.js.snap
@@ -4,7 +4,7 @@ exports[`GuestsModalScreen Renders correctly 1`] = `
 <GuestsModal
   guests={
     Object {
-      "adultsCount": 0,
+      "adultsCount": 3,
       "children": Array [],
     }
   }
diff --git a/packages/localization/src/__tests__/Translation.test.js b/packages/localization/src/__tests__/Translation.test.js
index 120dfa43..60f97c09 100644
--- a/packages/localization/src/__tests__/Translation.test.js
+++ b/packages/localization/src/__tests__/Translation.test.js
@@ -90,7 +90,7 @@ it('works with pass through translations and text transformations', () => {
 });
 
 it('does not call set state if promise is rejected', async () => {
-  let originalFunction = CancellableTranslation.cancellableTranslation;
+  const originalFunction = CancellableTranslation.cancellableTranslation;
   // $FlowExpectedError: Intentionally overwriting function to test outcome
   CancellableTranslation.cancellableTranslation = jest.fn(() => ({
     promise: new Promise((resolve, reject) => {