Repository: kiwicom/mobile
Pull request #703 (2018-06-05 09:19:12)

MMB: Tickets screen, add E-Ticket component

tbergq: @mrtnzlml 👀 

Patch:
diff --git a/app/MMB/src/navigation/DetailScreen.js b/app/MMB/src/navigation/DetailScreen.js
index ea54e167..b92b4470 100644
--- a/app/MMB/src/navigation/DetailScreen.js
+++ b/app/MMB/src/navigation/DetailScreen.js
@@ -10,6 +10,7 @@ import Layout from '../components/Layout';
 import MainMenuContainer from '../MainMenuContainer';
 import HelpSubmenu, { HelpSubmenuItems } from '../scenes/help';
 import OtherSubmenu, { OtherSubmenuItems } from '../scenes/Other';
+import TicketScene from '../scenes/tickets/TicketScene';
 import FlightServices, {
   FlightServicesSubmenuItems,
 } from '../scenes/flightServices/FlightServices';
@@ -70,7 +71,7 @@ export const MenuComponents = {
     },
   },
   'mmb.tickets': {
-    screen: () => null,
+    screen: TicketScene,
     headerTitle: function TicketsSubmenuHeaderTitle() {
       return (
         <HeaderTitle>
diff --git a/app/MMB/src/scenes/tickets/ETicket.js b/app/MMB/src/scenes/tickets/ETicket.js
new file mode 100644
index 00000000..32936297
--- /dev/null
+++ b/app/MMB/src/scenes/tickets/ETicket.js
@@ -0,0 +1,105 @@
+// @flow
+
+import * as React from 'react';
+import { View } from 'react-native';
+import {
+  TextIcon,
+  SimpleCard,
+  StyleSheet,
+  Text,
+  Color,
+  Button,
+} from '@kiwicom/mobile-shared';
+import { graphql, createFragmentContainer } from '@kiwicom/mobile-relay';
+import { Translation } from '@kiwicom/mobile-localization';
+import idx from 'idx';
+
+import type { ETicket as AssetsType } from './__generated__/ETicket.graphql';
+
+type Props = {|
+  +data: AssetsType,
+|};
+
+class ETicket extends React.Component<Props> {
+  openDocument = () => {
+    console.warn('TODO:');
+  };
+
+  render = () => {
+    const isTicketConfirmed = Boolean(idx(this.props.data, _ => _.ticketUrl));
+    return (
+      <SimpleCard>
+        <View style={styles.header}>
+          <TextIcon code="J" style={styles.icon} />
+          <Text style={styles.headerText}>
+            <Translation id="mmb.tickets.e_ticket" />
+          </Text>
+        </View>
+        <Translation
+          id={
+            isTicketConfirmed
+              ? 'mmb.tickets.print_eticket_and_bring_to_airport'
+              : 'mmb.tickets.available_when_booking_confirmed'
+          }
+        />
+        <View style={styles.buttonWrapper}>
+          <Button
+            onPress={this.openDocument}
+            disabled={!isTicketConfirmed}
+            style={[styles.button, !isTicketConfirmed && styles.disabled]}
+          >
+            <Text style={styles.buttonText}>
+              <Translation
+                id={
+                  isTicketConfirmed
+                    ? 'mmb.tickets.open'
+                    : 'mmb.tickets.not_available_yet'
+                }
+              />
+            </Text>
+          </Button>
+        </View>
+      </SimpleCard>
+    );
+  };
+}
+
+export default createFragmentContainer(
+  ETicket,
+  graphql`
+    fragment ETicket on BookingAssets {
+      ticketUrl
+    }
+  `,
+);
+
+const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    marginBottom: 12,
+    paddingTop: 4,
+  },
+  icon: {
+    color: Color.textDark,
+    alignSelf: 'center',
+    marginEnd: 7,
+  },
+  headerText: {
+    color: Color.textDark,
+    fontSize: 16,
+  },
+  buttonWrapper: {
+    marginTop: 21,
+  },
+  button: {
+    backgroundColor: Color.brand,
+    height: 44,
+    borderRadius: 4,
+  },
+  disabled: {
+    backgroundColor: '#b8c1cc',
+  },
+  buttonText: {
+    color: Color.white,
+  },
+});
diff --git a/app/MMB/src/scenes/tickets/TicketScene.js b/app/MMB/src/scenes/tickets/TicketScene.js
new file mode 100644
index 00000000..b1d42648
--- /dev/null
+++ b/app/MMB/src/scenes/tickets/TicketScene.js
@@ -0,0 +1,43 @@
+// @flow
+
+import * as React from 'react';
+import { View } from 'react-native';
+import { PrivateApiRenderer, graphql } from '@kiwicom/mobile-relay';
+import idx from 'idx';
+
+import ETicket from './ETicket';
+import BookingDetailContext from '../../context/BookingDetailContext';
+import type { TicketSceneQueryResponse } from './__generated__/TicketSceneQuery.graphql';
+
+type Props = {|
+  +data: TicketSceneQueryResponse,
+|};
+
+export default class TicketScene extends React.Component<Props> {
+  renderInner = (renderProps: TicketSceneQueryResponse) => (
+    <View style={{ paddingTop: 20 }}>
+      {/* Using inline style because this view is temporary, will be replaced by scroll view and refetch possabilities */}
+      <ETicket data={idx(renderProps.booking, _ => _.assets)} />
+    </View>
+  );
+
+  render = () => (
+    <BookingDetailContext.Consumer>
+      {({ bookingId }) => (
+        <PrivateApiRenderer
+          query={graphql`
+            query TicketSceneQuery($bookingId: ID!) {
+              booking(id: $bookingId) {
+                assets {
+                  ...ETicket
+                }
+              }
+            }
+          `}
+          variables={{ bookingId }}
+          render={this.renderInner}
+        />
+      )}
+    </BookingDetailContext.Consumer>
+  );
+}
diff --git a/app/MMB/src/scenes/tickets/__generated__/ETicket.graphql.js b/app/MMB/src/scenes/tickets/__generated__/ETicket.graphql.js
new file mode 100644
index 00000000..00dac6d9
--- /dev/null
+++ b/app/MMB/src/scenes/tickets/__generated__/ETicket.graphql.js
@@ -0,0 +1,38 @@
+/**
+ * @flow
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ConcreteFragment } from 'relay-runtime';
+import type { FragmentReference } from "relay-runtime";
+declare export opaque type ETicket$ref: FragmentReference;
+export type ETicket = {|
+  +ticketUrl: ?string,
+  +$refType: ETicket$ref,
+|};
+*/
+
+
+const node/*: ConcreteFragment*/ = {
+  "kind": "Fragment",
+  "name": "ETicket",
+  "type": "BookingAssets",
+  "metadata": null,
+  "argumentDefinitions": [],
+  "selections": [
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "ticketUrl",
+      "args": null,
+      "storageKey": null
+    }
+  ]
+};
+// prettier-ignore
+(node/*: any*/).hash = '2303159707413bc5c636e495584f32eb';
+module.exports = node;
diff --git a/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js b/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js
new file mode 100644
index 00000000..b3588b12
--- /dev/null
+++ b/app/MMB/src/scenes/tickets/__generated__/TicketSceneQuery.graphql.js
@@ -0,0 +1,150 @@
+/**
+ * @flow
+ * @relayHash ffc433851f7047b74d31c42eeb68a0ff
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ConcreteRequest } from 'relay-runtime';
+type ETicket$ref = any;
+export type TicketSceneQueryVariables = {|
+  bookingId: string
+|};
+export type TicketSceneQueryResponse = {|
+  +booking: ?{|
+    +assets: ?{|
+      +$fragmentRefs: ETicket$ref
+    |}
+  |}
+|};
+*/
+
+
+/*
+query TicketSceneQuery(
+  $bookingId: ID!
+) {
+  booking(id: $bookingId) {
+    assets {
+      ...ETicket
+    }
+    id
+  }
+}
+
+fragment ETicket on BookingAssets {
+  ticketUrl
+}
+*/
+
+const node/*: ConcreteRequest*/ = (function(){
+var v0 = [
+  {
+    "kind": "LocalArgument",
+    "name": "bookingId",
+    "type": "ID!",
+    "defaultValue": null
+  }
+],
+v1 = [
+  {
+    "kind": "Variable",
+    "name": "id",
+    "variableName": "bookingId",
+    "type": "ID!"
+  }
+];
+return {
+  "kind": "Request",
+  "operationKind": "query",
+  "name": "TicketSceneQuery",
+  "id": null,
+  "text": "query TicketSceneQuery(\n  $bookingId: ID!\n) {\n  booking(id: $bookingId) {\n    assets {\n      ...ETicket\n    }\n    id\n  }\n}\n\nfragment ETicket on BookingAssets {\n  ticketUrl\n}\n",
+  "metadata": {},
+  "fragment": {
+    "kind": "Fragment",
+    "name": "TicketSceneQuery",
+    "type": "RootQuery",
+    "metadata": null,
+    "argumentDefinitions": v0,
+    "selections": [
+      {
+        "kind": "LinkedField",
+        "alias": null,
+        "name": "booking",
+        "storageKey": null,
+        "args": v1,
+        "concreteType": "Booking",
+        "plural": false,
+        "selections": [
+          {
+            "kind": "LinkedField",
+            "alias": null,
+            "name": "assets",
+            "storageKey": null,
+            "args": null,
+            "concreteType": "BookingAssets",
+            "plural": false,
+            "selections": [
+              {
+                "kind": "FragmentSpread",
+                "name": "ETicket",
+                "args": null
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  "operation": {
+    "kind": "Operation",
+    "name": "TicketSceneQuery",
+    "argumentDefinitions": v0,
+    "selections": [
+      {
+        "kind": "LinkedField",
+        "alias": null,
+        "name": "booking",
+        "storageKey": null,
+        "args": v1,
+        "concreteType": "Booking",
+        "plural": false,
+        "selections": [
+          {
+            "kind": "LinkedField",
+            "alias": null,
+            "name": "assets",
+            "storageKey": null,
+            "args": null,
+            "concreteType": "BookingAssets",
+            "plural": false,
+            "selections": [
+              {
+                "kind": "ScalarField",
+                "alias": null,
+                "name": "ticketUrl",
+                "args": null,
+                "storageKey": null
+              }
+            ]
+          },
+          {
+            "kind": "ScalarField",
+            "alias": null,
+            "name": "id",
+            "args": null,
+            "storageKey": null
+          }
+        ]
+      }
+    ]
+  }
+};
+})();
+// prettier-ignore
+(node/*: any*/).hash = '47718bc74ee07416c3d9ff038e6a8dc1';
+module.exports = node;
diff --git a/packages/localization/src/vocabularies/ManageMyBooking.js b/packages/localization/src/vocabularies/ManageMyBooking.js
index 4f025e94..0c0a7c8b 100644
--- a/packages/localization/src/vocabularies/ManageMyBooking.js
+++ b/packages/localization/src/vocabularies/ManageMyBooking.js
@@ -36,6 +36,16 @@ export default {
   'mmb.flight_services.title': 'Services',
   'mmb.flight_services.title_ordered': 'Ordered',
 
+  // MMB Tickets
+
+  'mmb.tickets.available_when_booking_confirmed':
+    'Your document will be ready as soon as your booking is confirmed.',
+  'mmb.tickets.print_eticket_and_bring_to_airport':
+    'Print your e-ticket and bring it to the airport',
+  'mmb.tickets.not_available_yet': 'Not available yet',
+  'mmb.tickets.open': 'Open Document',
+  'mmb.tickets.e_ticket': 'E-Ticket',
+
   // MMB Trip info
   'mmb.title.trip_info': 'Trip info',
   'mmb.button.tickets': 'Tickets',
diff --git a/packages/shared/src/buttons/Button.js b/packages/shared/src/buttons/Button.js
index 99a7d358..e818105b 100644
--- a/packages/shared/src/buttons/Button.js
+++ b/packages/shared/src/buttons/Button.js
@@ -8,9 +8,10 @@ import StyleSheet from '../PlatformStyleSheet';
 import type { StylePropType } from '../../index';
 
 type Props = {|
-  children: React.Node,
-  onPress?: () => void,
-  style?: StylePropType,
+  +children: React.Node,
+  +onPress?: () => void,
+  +style?: StylePropType,
+  +disabled?: boolean,
 |};
 
 /**
@@ -23,7 +24,7 @@ export default function Button(props: Props) {
 
   return (
     <Touchable
-      disabled={!props.onPress}
+      disabled={!props.onPress || props.disabled}
       onPress={onPressHandler}
       style={[styleSheet.buttonWrapper, props.style]}
       accessibilityComponentType="button"
@@ -33,6 +34,10 @@ export default function Button(props: Props) {
   );
 }
 
+Button.defaultProps = {
+  disabled: false,
+};
+
 const styleSheet = StyleSheet.create({
   buttonWrapper: {
     flexGrow: 1,


Test patch:
diff --git a/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap b/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap
index 3008facc..d499f32a 100644
--- a/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap
+++ b/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap
@@ -2,6 +2,7 @@
 
 exports[`renders button with custom title 1`] = `
 <Button
+  disabled={false}
   onPress={[Function]}
 >
   <ButtonTitle