Repository: kiwicom/mobile
Pull request #891 (2018-07-18 07:26:46)

Shared: Add cancellable promise to Price component


Patch:
diff --git a/packages/localization/src/CancellableTranslation.js b/packages/localization/src/CancellableTranslation.js
index 58e9b5c0..ea897025 100644
--- a/packages/localization/src/CancellableTranslation.js
+++ b/packages/localization/src/CancellableTranslation.js
@@ -1,6 +1,7 @@
 // @flow strict
 
 import { NativeModules } from 'react-native';
+import { CancellablePromise } from '@kiwicom/mobile-shared';
 
 export type TranslationPromise = {|
   cancel: () => void,
@@ -8,22 +9,7 @@ export type TranslationPromise = {|
 |};
 
 export function cancellableTranslation(nativeKey: string): TranslationPromise {
-  let resolve;
-  let reject;
-
-  const promise = new Promise((originalResolve, originalReject) => {
-    resolve = originalResolve;
-    reject = originalReject;
-  });
-
-  NativeModules.RNTranslationManager.translateAsync(nativeKey)
-    .then(resolve)
-    .then(reject);
-
-  return {
-    promise,
-    cancel: () => {
-      reject('cancelled');
-    },
-  };
+  return CancellablePromise(
+    NativeModules.RNTranslationManager.translateAsync(nativeKey),
+  );
 }
diff --git a/packages/shared/index.js b/packages/shared/index.js
index 8b107071..b199769b 100644
--- a/packages/shared/index.js
+++ b/packages/shared/index.js
@@ -86,6 +86,7 @@ export { default as StretchedImage } from './src/image/StretchedImage';
  * Non-UI components (components without render output):
  */
 export { default as AppStateChange } from './src/AppStateChange';
+export { default as CancellablePromise } from './src/CancellablePromise';
 export { default as Color } from './src/Color';
 export { default as CurrencyFormatter } from './src/CurrencyFormatter';
 export { default as Device } from './src/Device';
@@ -114,4 +115,6 @@ export type {
   PlatformStyleObjectType,
 } from './types/Styles';
 
+export type { CancellablePromiseType } from './src/CancellablePromise';
+
 export type { DimensionType } from './types/Objects';
diff --git a/packages/shared/src/CancellablePromise.js b/packages/shared/src/CancellablePromise.js
new file mode 100644
index 00000000..e93ef50e
--- /dev/null
+++ b/packages/shared/src/CancellablePromise.js
@@ -0,0 +1,25 @@
+// @flow strict
+
+export type CancellablePromiseType = {|
+  cancel: () => void,
+  promise: Promise<*>,
+|};
+
+export default (incomingPromise: Promise<*>) => {
+  let resolve;
+  let reject;
+
+  const promise = new Promise((originalResolve, originalReject) => {
+    resolve = originalResolve;
+    reject = originalReject;
+  });
+
+  incomingPromise.then(resolve).then(reject);
+
+  return {
+    promise,
+    cancel: () => {
+      reject('cancelled');
+    },
+  };
+};
diff --git a/packages/shared/src/Price.js b/packages/shared/src/Price.js
index 9947e685..5c994875 100644
--- a/packages/shared/src/Price.js
+++ b/packages/shared/src/Price.js
@@ -6,6 +6,9 @@ import { Translation } from '@kiwicom/mobile-localization';
 import CurrencyFormatter from './CurrencyFormatter';
 import Text from './Text';
 import type { StylePropType } from '../types/Styles';
+import CancellablePromise, {
+  type CancellablePromiseType,
+} from './CancellablePromise';
 
 type Props = {|
   +amount: number | null,
@@ -23,6 +26,7 @@ type State = {|
  * all prices should be wrapped in this component so the future changes are easy.
  */
 export default class Price extends React.Component<Props, State> {
+  cancellablePromise: CancellablePromiseType | null = null;
   state = {
     formattedCurrency: '',
   };
@@ -37,13 +41,24 @@ export default class Price extends React.Component<Props, State> {
     }
   };
 
+  componentWillUnmount = () => {
+    if (this.cancellablePromise !== null) {
+      this.cancellablePromise.cancel();
+    }
+  };
+
   formatCurrency = async () => {
     if (this.props.amount != null && this.props.currency != null) {
-      const formattedCurrency = await CurrencyFormatter(
-        this.props.amount,
-        this.props.currency,
-      );
-      this.setState({ formattedCurrency });
+      try {
+        this.cancellablePromise = CancellablePromise(
+          CurrencyFormatter(this.props.amount, this.props.currency),
+        );
+        const formattedCurrency = await this.cancellablePromise.promise;
+        this.setState({ formattedCurrency });
+        this.cancellablePromise = null;
+      } catch (err) {
+        this.cancellablePromise = null;
+      }
     }
   };
 


Test patch:
diff --git a/packages/shared/src/__tests__/Price.test.js b/packages/shared/src/__tests__/Price.test.js
index 5f1d6272..5f22e749 100644
--- a/packages/shared/src/__tests__/Price.test.js
+++ b/packages/shared/src/__tests__/Price.test.js
@@ -8,6 +8,11 @@ import Price from '../Price';
 import CurrencyFormatter from '../CurrencyFormatter';
 
 jest.mock('../CurrencyFormatter', () => jest.fn());
+jest.mock('../CancellablePromise', () => ({
+  promise: new Promise((resolve, reject) => {
+    reject();
+  }),
+}));
 
 const renderer = new ShallowRenderer();
 
@@ -28,4 +33,13 @@ describe('Price', () => {
     testRenderer.create(<Price amount={45} currency="EUR" />);
     expect(CurrencyFormatter).toHaveBeenCalledWith(45, 'EUR');
   });
+
+  it('does not call set state if promise is rejected', async () => {
+    const Component = new Price({ amount: 45, currency: 'EUR' });
+
+    jest.spyOn(Component, 'setState');
+    await Component.formatCurrency();
+
+    expect(Component.setState).not.toHaveBeenCalled();
+  });
 });