Repository: kiwicom/mobile
Pull request #1210 (2018-10-10 13:40:34)

MMB: Download and unzip pkpass on android


Patch:
diff --git a/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js b/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js
index dce4621b..7ed78a32 100644
--- a/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js
+++ b/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 87e8e5afc048618c3f90b6e10b304389
+ * @relayHash 670c33404cecf9ae18f8a3b76f75d57f
  */
 
 /* eslint-disable */
@@ -135,6 +135,7 @@ fragment DownloadButton on BoardingPass {
 }
 
 fragment TicketList on BoardingPass {
+  flightNumber
   pkpasses {
     ...WalletPassenger
     passenger {
@@ -147,6 +148,7 @@ fragment WalletPassenger on Pkpass {
   url
   passenger {
     fullName
+    databaseId
   }
 }
 
@@ -373,7 +375,7 @@ return {
   "operationKind": "query",
   "name": "TicketRefetchQuery",
   "id": null,
-  "text": "query TicketRefetchQuery(\n  $id: Int!\n  $authToken: String!\n) {\n  singleBooking(id: $id, authToken: $authToken) {\n    __typename\n    ...TicketRefetch\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  databaseId\n  authToken\n  ...BoardingPasses\n  assets {\n    ...ETicket\n  }\n}\n\nfragment BoardingPasses on Node {\n  __typename\n  ... on BookingReturn {\n    ...BoardingPassReturn\n  }\n  ... on BookingOneWay {\n    ...BoardingPassOneWay\n  }\n  ... on BookingMulticity {\n    ...BoardingPassMultiCity\n  }\n}\n\nfragment ETicket on BookingAssets {\n  ticketUrl\n}\n\nfragment BoardingPassReturn on BookingReturn {\n  outbound {\n    ...FlightSegments\n  }\n  inbound {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassOneWay on BookingOneWay {\n  trip {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassMultiCity on BookingMulticity {\n  trips {\n    arrival {\n      airport {\n        city {\n          name\n        }\n        id\n      }\n    }\n    ...FlightSegments\n  }\n}\n\nfragment FlightSegments on Trip {\n  legs {\n    id\n    ...FlightFromTo\n  }\n}\n\nfragment FlightFromTo on Leg {\n  id\n  airline {\n    logoUrl\n  }\n  departure {\n    localTime\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  arrival {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  boardingPass {\n    ...DownloadButton\n    ...TicketList\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment TicketList on BoardingPass {\n  pkpasses {\n    ...WalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment WalletPassenger on Pkpass {\n  url\n  passenger {\n    fullName\n  }\n}\n\nfragment BoardingPassInformation on BoardingPass {\n  availableAt\n  boardingPassUrl\n  ...FutureBookingInformation\n}\n\nfragment FutureBookingInformation on BoardingPass {\n  boardingPassUrl\n}\n",
+  "text": "query TicketRefetchQuery(\n  $id: Int!\n  $authToken: String!\n) {\n  singleBooking(id: $id, authToken: $authToken) {\n    __typename\n    ...TicketRefetch\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  databaseId\n  authToken\n  ...BoardingPasses\n  assets {\n    ...ETicket\n  }\n}\n\nfragment BoardingPasses on Node {\n  __typename\n  ... on BookingReturn {\n    ...BoardingPassReturn\n  }\n  ... on BookingOneWay {\n    ...BoardingPassOneWay\n  }\n  ... on BookingMulticity {\n    ...BoardingPassMultiCity\n  }\n}\n\nfragment ETicket on BookingAssets {\n  ticketUrl\n}\n\nfragment BoardingPassReturn on BookingReturn {\n  outbound {\n    ...FlightSegments\n  }\n  inbound {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassOneWay on BookingOneWay {\n  trip {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassMultiCity on BookingMulticity {\n  trips {\n    arrival {\n      airport {\n        city {\n          name\n        }\n        id\n      }\n    }\n    ...FlightSegments\n  }\n}\n\nfragment FlightSegments on Trip {\n  legs {\n    id\n    ...FlightFromTo\n  }\n}\n\nfragment FlightFromTo on Leg {\n  id\n  airline {\n    logoUrl\n  }\n  departure {\n    localTime\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  arrival {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  boardingPass {\n    ...DownloadButton\n    ...TicketList\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment TicketList on BoardingPass {\n  flightNumber\n  pkpasses {\n    ...WalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment WalletPassenger on Pkpass {\n  url\n  passenger {\n    fullName\n    databaseId\n  }\n}\n\nfragment BoardingPassInformation on BoardingPass {\n  availableAt\n  boardingPassUrl\n  ...FutureBookingInformation\n}\n\nfragment FutureBookingInformation on BoardingPass {\n  boardingPassUrl\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
diff --git a/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js b/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js
index 2f474bcc..3196bff8 100644
--- a/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js
+++ b/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 8e01113ea4dd997ab2b7bede9e08b17b
+ * @relayHash beb307b416b4c90696ef52df1a4cfafd
  */
 
 /* eslint-disable */
@@ -135,6 +135,7 @@ fragment DownloadButton on BoardingPass {
 }
 
 fragment TicketList on BoardingPass {
+  flightNumber
   pkpasses {
     ...WalletPassenger
     passenger {
@@ -147,6 +148,7 @@ fragment WalletPassenger on Pkpass {
   url
   passenger {
     fullName
+    databaseId
   }
 }
 
@@ -373,7 +375,7 @@ return {
   "operationKind": "query",
   "name": "TicketSceneQuery",
   "id": null,
-  "text": "query TicketSceneQuery(\n  $bookingId: Int!\n  $authToken: String!\n) {\n  singleBooking(id: $bookingId, authToken: $authToken) {\n    __typename\n    ...TicketRefetch\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  databaseId\n  authToken\n  ...BoardingPasses\n  assets {\n    ...ETicket\n  }\n}\n\nfragment BoardingPasses on Node {\n  __typename\n  ... on BookingReturn {\n    ...BoardingPassReturn\n  }\n  ... on BookingOneWay {\n    ...BoardingPassOneWay\n  }\n  ... on BookingMulticity {\n    ...BoardingPassMultiCity\n  }\n}\n\nfragment ETicket on BookingAssets {\n  ticketUrl\n}\n\nfragment BoardingPassReturn on BookingReturn {\n  outbound {\n    ...FlightSegments\n  }\n  inbound {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassOneWay on BookingOneWay {\n  trip {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassMultiCity on BookingMulticity {\n  trips {\n    arrival {\n      airport {\n        city {\n          name\n        }\n        id\n      }\n    }\n    ...FlightSegments\n  }\n}\n\nfragment FlightSegments on Trip {\n  legs {\n    id\n    ...FlightFromTo\n  }\n}\n\nfragment FlightFromTo on Leg {\n  id\n  airline {\n    logoUrl\n  }\n  departure {\n    localTime\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  arrival {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  boardingPass {\n    ...DownloadButton\n    ...TicketList\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment TicketList on BoardingPass {\n  pkpasses {\n    ...WalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment WalletPassenger on Pkpass {\n  url\n  passenger {\n    fullName\n  }\n}\n\nfragment BoardingPassInformation on BoardingPass {\n  availableAt\n  boardingPassUrl\n  ...FutureBookingInformation\n}\n\nfragment FutureBookingInformation on BoardingPass {\n  boardingPassUrl\n}\n",
+  "text": "query TicketSceneQuery(\n  $bookingId: Int!\n  $authToken: String!\n) {\n  singleBooking(id: $bookingId, authToken: $authToken) {\n    __typename\n    ...TicketRefetch\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  databaseId\n  authToken\n  ...BoardingPasses\n  assets {\n    ...ETicket\n  }\n}\n\nfragment BoardingPasses on Node {\n  __typename\n  ... on BookingReturn {\n    ...BoardingPassReturn\n  }\n  ... on BookingOneWay {\n    ...BoardingPassOneWay\n  }\n  ... on BookingMulticity {\n    ...BoardingPassMultiCity\n  }\n}\n\nfragment ETicket on BookingAssets {\n  ticketUrl\n}\n\nfragment BoardingPassReturn on BookingReturn {\n  outbound {\n    ...FlightSegments\n  }\n  inbound {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassOneWay on BookingOneWay {\n  trip {\n    ...FlightSegments\n  }\n}\n\nfragment BoardingPassMultiCity on BookingMulticity {\n  trips {\n    arrival {\n      airport {\n        city {\n          name\n        }\n        id\n      }\n    }\n    ...FlightSegments\n  }\n}\n\nfragment FlightSegments on Trip {\n  legs {\n    id\n    ...FlightFromTo\n  }\n}\n\nfragment FlightFromTo on Leg {\n  id\n  airline {\n    logoUrl\n  }\n  departure {\n    localTime\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  arrival {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n  boardingPass {\n    ...DownloadButton\n    ...TicketList\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment TicketList on BoardingPass {\n  flightNumber\n  pkpasses {\n    ...WalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment WalletPassenger on Pkpass {\n  url\n  passenger {\n    fullName\n    databaseId\n  }\n}\n\nfragment BoardingPassInformation on BoardingPass {\n  availableAt\n  boardingPassUrl\n  ...FutureBookingInformation\n}\n\nfragment FutureBookingInformation on BoardingPass {\n  boardingPassUrl\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/TicketList.js b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/TicketList.js
index 5f7b92be..e0c3449d 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/TicketList.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/TicketList.js
@@ -50,6 +50,7 @@ const TicketList = (props: Props) => {
             key={idx(item, _ => _.passenger.databaseId)}
             data={item}
             segmentId={props.segmentId}
+            flightNumber={idx(props.data, _ => _.flightNumber)}
           />
         ))}
       </View>
@@ -61,6 +62,7 @@ export default createFragmentContainer(
   TicketList,
   graphql`
     fragment TicketList on BoardingPass {
+      flightNumber
       pkpasses {
         ...WalletPassenger
         passenger {
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/WalletPassenger.js b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/WalletPassenger.js
index 008ff587..fed84d62 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/WalletPassenger.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/WalletPassenger.js
@@ -11,12 +11,16 @@ import {
   withNavigation,
 } from '@kiwicom/mobile-navigation';
 import { defaultTokens } from '@kiwicom/mobile-orbit';
+import { AssetsDownloader, unzip } from '@kiwicom/mobile-assets';
+import RNFetchBlob from 'rn-fetch-blob';
 
 import type { AppleWalletPassenger as AppleWalletType } from './__generated__/WalletPassenger.graphql';
 import WalletContext from './../../../../context/WalletContext';
+import BookingDetailContext from '../../../../context/BookingDetailContext';
 
 type PropsWithContext = {|
   ...Props,
+  +bookingId: number,
   +addPkpassData: (
     passengerName: string,
     pkpassUrl: string,
@@ -25,7 +29,25 @@ type PropsWithContext = {|
   ) => void,
 |};
 
-class WalletPassenger extends React.Component<PropsWithContext> {
+type State = {|
+  isDownloaded: boolean,
+|};
+
+class WalletPassenger extends React.Component<PropsWithContext, State> {
+  state = {
+    isDownloaded: false,
+  };
+
+  componentDidMount = async () => {
+    if (Platform.OS === 'android') {
+      const folderPath = `${
+        RNFetchBlob.fs.dirs.DocumentDir
+      }/${this.getFolderPath()}`;
+      const exists = await RNFetchBlob.fs.exists(`${folderPath}`);
+      this.setState({ isDownloaded: exists });
+    }
+  };
+
   onPressIos = () => {
     const name = idx(this.props.data, _ => _.passenger.fullName) || '';
     const url = idx(this.props.data, _ => _.url) || '';
@@ -39,7 +61,40 @@ class WalletPassenger extends React.Component<PropsWithContext> {
   };
 
   onPressAndroid = () => {
-    console.warn('TODO');
+    if (this.state.isDownloaded) {
+      console.warn('TODO');
+    } else {
+      this.downloadPkpass();
+    }
+  };
+
+  getFolderPath = () => {
+    const databaseId = idx(this.props.data, _ => _.passenger.databaseId) || '';
+    const flightNumber =
+      this.props.flightNumber == null ? '' : this.props.flightNumber;
+    return `pkpasses/${this.props.bookingId}/${flightNumber}/${databaseId}`;
+  };
+
+  downloadPkpass = async () => {
+    try {
+      const url = idx(this.props.data, _ => _.url) || '';
+      const databaseId = idx(this.props.data, _ => _.passenger.databaseId);
+
+      if (this.props.flightNumber != null && databaseId != null) {
+        const folderPath = this.getFolderPath();
+        const pkPath = `${folderPath}/pass.pkpass`;
+
+        await AssetsDownloader(pkPath, url, true);
+
+        await unzip(pkPath, folderPath);
+        this.setState({ isDownloaded: true });
+      } else {
+        // TODO: Show alert inform about missing data
+      }
+    } catch (err) {
+      // TODO: Clean up, there is a possability that there has been stored an empty file
+      console.warn(err);
+    }
   };
 
   render = () => {
@@ -55,7 +110,11 @@ class WalletPassenger extends React.Component<PropsWithContext> {
             passThrough={idx(this.props.data, _ => _.passenger.fullName)}
           />
           {Platform.select({
-            android: <TextIcon code="&#xe014;" style={styles.icon} />,
+            android: this.state.isDownloaded ? (
+              <TextIcon code="V" style={styles.icon} />
+            ) : (
+              <TextIcon code="&#xe014;" style={styles.icon} />
+            ),
             ios: <TextIcon code="&#xe01F;" style={styles.icon} />,
           })}
         </React.Fragment>
@@ -68,14 +127,23 @@ type Props = {|
   +data: AppleWalletType,
   +navigation: NavigationType,
   +segmentId: ?string,
+  +flightNumber: ?number,
 |};
 
 const WalletPassengerWithContext = (props: Props) => (
-  <WalletContext.Consumer>
-    {({ actions: { addPkpassData } }) => (
-      <WalletPassenger {...props} addPkpassData={addPkpassData} />
+  <BookingDetailContext.Consumer>
+    {({ bookingId }) => (
+      <WalletContext.Consumer>
+        {({ actions: { addPkpassData } }) => (
+          <WalletPassenger
+            {...props}
+            addPkpassData={addPkpassData}
+            bookingId={bookingId}
+          />
+        )}
+      </WalletContext.Consumer>
     )}
-  </WalletContext.Consumer>
+  </BookingDetailContext.Consumer>
 );
 
 export default createFragmentContainer(
@@ -85,6 +153,7 @@ export default createFragmentContainer(
       url
       passenger {
         fullName
+        databaseId
       }
     }
   `,
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/TicketList.graphql.js b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/TicketList.graphql.js
index 015f8042..6b993ab6 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/TicketList.graphql.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/TicketList.graphql.js
@@ -12,6 +12,7 @@ type WalletPassenger$ref = any;
 import type { FragmentReference } from "relay-runtime";
 declare export opaque type TicketList$ref: FragmentReference;
 export type TicketList = {|
+  +flightNumber: ?string,
   +pkpasses: ?$ReadOnlyArray<?{|
     +passenger: ?{|
       +databaseId: ?number
@@ -30,6 +31,13 @@ const node/*: ConcreteFragment*/ = {
   "metadata": null,
   "argumentDefinitions": [],
   "selections": [
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "flightNumber",
+      "args": null,
+      "storageKey": null
+    },
     {
       "kind": "LinkedField",
       "alias": null,
@@ -67,5 +75,5 @@ const node/*: ConcreteFragment*/ = {
   ]
 };
 // prettier-ignore
-(node/*: any*/).hash = '0304d8d8eba00570c06ba26a38fc4b91';
+(node/*: any*/).hash = '51fd314afde996265961adf6a096204b';
 module.exports = node;
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/WalletPassenger.graphql.js b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/WalletPassenger.graphql.js
index acd5859f..500c2490 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/WalletPassenger.graphql.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/mobileTicket/__generated__/WalletPassenger.graphql.js
@@ -13,7 +13,8 @@ declare export opaque type WalletPassenger$ref: FragmentReference;
 export type WalletPassenger = {|
   +url: ?string,
   +passenger: ?{|
-    +fullName: ?string
+    +fullName: ?string,
+    +databaseId: ?number,
   |},
   +$refType: WalletPassenger$ref,
 |};
@@ -49,11 +50,18 @@ const node/*: ConcreteFragment*/ = {
           "name": "fullName",
           "args": null,
           "storageKey": null
+        },
+        {
+          "kind": "ScalarField",
+          "alias": null,
+          "name": "databaseId",
+          "args": null,
+          "storageKey": null
         }
       ]
     }
   ]
 };
 // prettier-ignore
-(node/*: any*/).hash = 'fd0cebd051bc50e5cf19ba7451755c4c';
+(node/*: any*/).hash = '29db6d6dae2f405a47adfd4a628b6a51';
 module.exports = node;
diff --git a/packages/assets/index.js b/packages/assets/index.js
index 4274473e..aa779efc 100644
--- a/packages/assets/index.js
+++ b/packages/assets/index.js
@@ -4,3 +4,4 @@ import RNFetchBlob from 'rn-fetch-blob';
 
 export const documentDir = `file://${RNFetchBlob.fs.dirs.DocumentDir}`;
 export { default as AssetsDownloader } from './src/AssetsDownloader';
+export { default as unzip } from './src/Unzip';
diff --git a/packages/assets/src/Unzip.js b/packages/assets/src/Unzip.js
new file mode 100644
index 00000000..3bf0443b
--- /dev/null
+++ b/packages/assets/src/Unzip.js
@@ -0,0 +1,23 @@
+// @flow strict
+
+import { unzip } from 'react-native-zip-archive';
+import { Platform } from 'react-native';
+import RNFetchBlob from 'rn-fetch-blob';
+
+export default async (sourcePath: string, targetPath: string) => {
+  if (Platform.OS === 'ios') {
+    throw new Error('Not yet bridged.');
+  }
+
+  const { DocumentDir } = RNFetchBlob.fs.dirs;
+
+  if (DocumentDir === undefined) {
+    throw Error('Cannot access DocumentDir.');
+  }
+
+  const path = await unzip(
+    `${DocumentDir}/${sourcePath}`,
+    `${DocumentDir}/${targetPath}`,
+  );
+  return path;
+};


Test patch:
diff --git a/packages/assets/src/__tests__/Unzip.test.js b/packages/assets/src/__tests__/Unzip.test.js
new file mode 100644
index 00000000..b9ebd533
--- /dev/null
+++ b/packages/assets/src/__tests__/Unzip.test.js
@@ -0,0 +1,38 @@
+// @flow strict
+
+import { unzip } from 'react-native-zip-archive';
+import { Platform } from 'react-native';
+
+import Unzip from '../Unzip';
+
+jest.mock('react-native-zip-archive', () => ({
+  unzip: jest.fn(),
+}));
+
+let OriginalPlatform;
+
+beforeEach(() => {
+  OriginalPlatform = Platform.OS;
+});
+
+afterEach(() => {
+  Platform.OS = OriginalPlatform;
+});
+
+describe('Unzip', () => {
+  it('throws error on ios', async () => {
+    Platform.OS = 'ios';
+    expect.assertions(1);
+    try {
+      await Unzip('', '');
+    } catch (e) {
+      expect(e.message).toBe('Not yet bridged.');
+    }
+  });
+
+  it('calls unzip on android', async () => {
+    Platform.OS = 'android';
+    await Unzip('source', 'target');
+    expect(unzip).toHaveBeenCalledWith('ait/source', 'ait/target');
+  });
+});