Repository: kiwicom/mobile
Pull request #757 (2018-06-22 14:31:41)

Added DownloadInvoice timeline event; added disabled style to TextBut…
…ton; improved TimelineEventLayout


Patch:
diff --git a/app/MMB/src/navigation/DetailScreen.js b/app/MMB/src/navigation/DetailScreen.js
index df687ba2..f5f9b125 100644
--- a/app/MMB/src/navigation/DetailScreen.js
+++ b/app/MMB/src/navigation/DetailScreen.js
@@ -20,7 +20,7 @@ import TripServices, {
 } from '../scenes/tripServices/TripServices';
 import PassengerDetailContainer from '../scenes/passenger/PassengerDetailContainer';
 import TripOverviewTablet from '../scenes/tripOverview/TripOverviewTablet';
-import Timeline from '../scenes/timeline/Timeline';
+import Timeline, { TimelineSubmenuItems } from '../scenes/timeline/Timeline';
 
 export const MenuComponents = {
   'mmb.trip_overview': {
@@ -111,6 +111,7 @@ export const MenuComponents = {
   ...HelpSubmenuItems,
   ...OtherSubmenuItems,
   ...TicketSceneSubMenus,
+  ...TimelineSubmenuItems,
 };
 
 type Props = {|
diff --git a/app/MMB/src/scenes/timeline/Timeline.js b/app/MMB/src/scenes/timeline/Timeline.js
index 493a8799..76b48c13 100644
--- a/app/MMB/src/scenes/timeline/Timeline.js
+++ b/app/MMB/src/scenes/timeline/Timeline.js
@@ -10,15 +10,19 @@ import type { TimelineQueryResponse } from './__generated__/TimelineQuery.graphq
 import BookingDetailContext from '../../context/BookingDetailContext';
 import BookedFlightTimelineEvent from './events/BookedFlightTimelineEvent';
 import AirportArrivalTimelineEvent from './events/AirportArrivalTimelineEvent';
+import DownloadInvoiceTimelineEvent from './events/DownloadInvoiceTimelineEvent';
 import LeaveForAirportTimelineEvent from './events/LeaveForAirportTimelineEvent';
 import DaySeparator from './DaySeparator';
 
-// @TODO Display day banners only once for each day
+export { TimelineSubmenuItems } from './events/DownloadInvoiceTimelineEvent';
+
 function getValidTimelineEvent(data) {
   if (data && data.__typename) {
     switch (data.__typename) {
       case 'BookedFlightTimelineEvent':
         return <BookedFlightTimelineEvent data={data} />;
+      case 'DownloadInvoiceTimelineEvent':
+        return <DownloadInvoiceTimelineEvent data={data} />;
       case 'AirportArrivalTimelineEvent':
         return <AirportArrivalTimelineEvent data={data} />;
       case 'LeaveForAirportTimelineEvent':
@@ -89,6 +93,9 @@ export default class Timeline extends React.Component<{||}> {
                   ... on AirportArrivalTimelineEvent {
                     ...AirportArrivalTimelineEvent
                   }
+                  ... on DownloadInvoiceTimelineEvent {
+                    ...DownloadInvoiceTimelineEvent
+                  }
                 }
               }
             }
diff --git a/app/MMB/src/scenes/timeline/TimelineEvent.js b/app/MMB/src/scenes/timeline/TimelineEvent.js
index db058895..6eb4a7e7 100644
--- a/app/MMB/src/scenes/timeline/TimelineEvent.js
+++ b/app/MMB/src/scenes/timeline/TimelineEvent.js
@@ -8,6 +8,7 @@ import DateLocation from './TimelineEventDateLocation';
 
 type Props = {|
   +timestamp: ?Date,
+  +displayTime: boolean,
   +iconVertLines: React.Node,
   +mainContent: React.Node,
 |};
@@ -21,7 +22,7 @@ const TimelineEvent = (props: Props) => {
     isPastEvent = new Date() - new Date(timestamp) > 0;
   }
   let dateLocation = null;
-  if (time) {
+  if (time && props.displayTime) {
     dateLocation = <DateLocation time={time} />;
   }
 
@@ -40,4 +41,8 @@ const TimelineEvent = (props: Props) => {
   );
 };
 
+TimelineEvent.defaultProps = {
+  displayTime: true,
+};
+
 export default TimelineEvent;
diff --git a/app/MMB/src/scenes/timeline/TimelineEventDateLocation.js b/app/MMB/src/scenes/timeline/TimelineEventDateLocation.js
index cad09c29..b3eb58cf 100644
--- a/app/MMB/src/scenes/timeline/TimelineEventDateLocation.js
+++ b/app/MMB/src/scenes/timeline/TimelineEventDateLocation.js
@@ -21,8 +21,8 @@ export default function TimelineEventDateLocation(props: Props) {
 
 const styles = StyleSheet.create({
   container: {
-    flex: 1,
-    justifyContent: 'center',
+    paddingTop: 25,
+    justifyContent: 'flex-start',
     alignItems: 'flex-end',
   },
   text: {
diff --git a/app/MMB/src/scenes/timeline/TimelineEventIcon.js b/app/MMB/src/scenes/timeline/TimelineEventIcon.js
index f8eabe07..121768fe 100644
--- a/app/MMB/src/scenes/timeline/TimelineEventIcon.js
+++ b/app/MMB/src/scenes/timeline/TimelineEventIcon.js
@@ -24,12 +24,16 @@ export default function TimelineEventIcon(props: Props) {
   return (
     <View style={styles.container}>
       <View
-        style={[styles.line, props.isFirst && styles.resetBackgroundColor]}
+        style={[
+          styles.line,
+          styles.topLine,
+          props.isFirst && styles.resetBackgroundColor,
+        ]}
       />
       <View style={styles.iconBorder}>
         {props.isPastEvent ? pastEventIcon : icon}
       </View>
-      <View style={styles.line} />
+      <View style={[styles.line, styles.bottomLine]} />
     </View>
   );
 }
@@ -37,18 +41,23 @@ export default function TimelineEventIcon(props: Props) {
 const styles = StyleSheet.create({
   container: {
     alignItems: 'center',
-    justifyContent: 'center',
-    flex: 1,
+    justifyContent: 'flex-start',
+    height: '100%',
   },
   line: {
     backgroundColor: Color.ink.lighter,
-    flex: 1,
-    height: 30,
     width: 1,
   },
   resetBackgroundColor: {
     backgroundColor: 'transparent',
   },
+  topLine: {
+    height: 20,
+  },
+  bottomLine: {
+    minHeight: 10,
+    flex: 1,
+  },
   icon: {
     color: Color.product.normal,
     backgroundColor: Color.white,
diff --git a/app/MMB/src/scenes/timeline/TimelineEventLayout.js b/app/MMB/src/scenes/timeline/TimelineEventLayout.js
index 9a0c12ca..70422f89 100644
--- a/app/MMB/src/scenes/timeline/TimelineEventLayout.js
+++ b/app/MMB/src/scenes/timeline/TimelineEventLayout.js
@@ -48,6 +48,8 @@ const styles = StyleSheet.create({
   mainContent: {
     flex: 1,
     justifyContent: 'center',
-    alignItems: 'flex-start',
+    paddingTop: 20,
+    paddingBottom: 10,
+    paddingEnd: 10,
   },
 });
diff --git a/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js b/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
index 88fb3d09..85ebf790 100644
--- a/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
+++ b/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash d63faacc4932f7d15c167e0b667eee6b
+ * @relayHash 68d81c7f18408e29d969f8d43e9930ee
  */
 
 /* eslint-disable */
@@ -11,6 +11,7 @@
 import type { ConcreteRequest } from 'relay-runtime';
 type AirportArrivalTimelineEvent$ref = any;
 type BookedFlightTimelineEvent$ref = any;
+type DownloadInvoiceTimelineEvent$ref = any;
 type LeaveForAirportTimelineEvent$ref = any;
 export type TimelineQueryVariables = {|
   id: string
@@ -20,7 +21,7 @@ export type TimelineQueryResponse = {|
     +events: ?$ReadOnlyArray<?{|
       +__typename: string,
       +timestamp: ?any,
-      +$fragmentRefs: BookedFlightTimelineEvent$ref & LeaveForAirportTimelineEvent$ref & AirportArrivalTimelineEvent$ref,
+      +$fragmentRefs: BookedFlightTimelineEvent$ref & LeaveForAirportTimelineEvent$ref & AirportArrivalTimelineEvent$ref & DownloadInvoiceTimelineEvent$ref,
     |}>
   |}
 |};
@@ -44,6 +45,9 @@ query TimelineQuery(
       ... on AirportArrivalTimelineEvent {
         ...AirportArrivalTimelineEvent
       }
+      ... on DownloadInvoiceTimelineEvent {
+        ...DownloadInvoiceTimelineEvent
+      }
     }
   }
 }
@@ -73,6 +77,31 @@ fragment AirportArrivalTimelineEvent on AirportArrivalTimelineEvent {
     }
   }
 }
+
+fragment DownloadInvoiceTimelineEvent on DownloadInvoiceTimelineEvent {
+  timestamp
+  invoiceUrl
+  numberPassengers
+  legs {
+    departure {
+      airport {
+        city {
+          name
+        }
+        id
+      }
+    }
+    arrival {
+      airport {
+        city {
+          name
+        }
+        id
+      }
+    }
+    id
+  }
+}
 */
 
 const node/*: ConcreteRequest*/ = (function(){
@@ -119,13 +148,39 @@ v5 = {
   "name": "id",
   "args": null,
   "storageKey": null
-};
+},
+v6 = [
+  {
+    "kind": "LinkedField",
+    "alias": null,
+    "name": "airport",
+    "storageKey": null,
+    "args": null,
+    "concreteType": "Location",
+    "plural": false,
+    "selections": [
+      {
+        "kind": "LinkedField",
+        "alias": null,
+        "name": "city",
+        "storageKey": null,
+        "args": null,
+        "concreteType": "LocationArea",
+        "plural": false,
+        "selections": [
+          v4
+        ]
+      },
+      v5
+    ]
+  }
+];
 return {
   "kind": "Request",
   "operationKind": "query",
   "name": "TimelineQuery",
   "id": null,
-  "text": "query TimelineQuery(\n  $id: ID!\n) {\n  bookingTimeline(id: $id) {\n    events {\n      __typename\n      timestamp\n      ... on BookedFlightTimelineEvent {\n        ...BookedFlightTimelineEvent\n      }\n      ... on LeaveForAirportTimelineEvent {\n        ...LeaveForAirportTimelineEvent\n      }\n      ... on AirportArrivalTimelineEvent {\n        ...AirportArrivalTimelineEvent\n      }\n    }\n  }\n}\n\nfragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {\n  timestamp\n  location {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n}\n\nfragment LeaveForAirportTimelineEvent on LeaveForAirportTimelineEvent {\n  timestamp\n}\n\nfragment AirportArrivalTimelineEvent on AirportArrivalTimelineEvent {\n  timestamp\n  location {\n    airport {\n      name\n      id\n    }\n  }\n}\n",
+  "text": "query TimelineQuery(\n  $id: ID!\n) {\n  bookingTimeline(id: $id) {\n    events {\n      __typename\n      timestamp\n      ... on BookedFlightTimelineEvent {\n        ...BookedFlightTimelineEvent\n      }\n      ... on LeaveForAirportTimelineEvent {\n        ...LeaveForAirportTimelineEvent\n      }\n      ... on AirportArrivalTimelineEvent {\n        ...AirportArrivalTimelineEvent\n      }\n      ... on DownloadInvoiceTimelineEvent {\n        ...DownloadInvoiceTimelineEvent\n      }\n    }\n  }\n}\n\nfragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {\n  timestamp\n  location {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n}\n\nfragment LeaveForAirportTimelineEvent on LeaveForAirportTimelineEvent {\n  timestamp\n}\n\nfragment AirportArrivalTimelineEvent on AirportArrivalTimelineEvent {\n  timestamp\n  location {\n    airport {\n      name\n      id\n    }\n  }\n}\n\nfragment DownloadInvoiceTimelineEvent on DownloadInvoiceTimelineEvent {\n  timestamp\n  invoiceUrl\n  numberPassengers\n  legs {\n    departure {\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    id\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -154,6 +209,17 @@ return {
             "selections": [
               v2,
               v3,
+              {
+                "kind": "InlineFragment",
+                "type": "DownloadInvoiceTimelineEvent",
+                "selections": [
+                  {
+                    "kind": "FragmentSpread",
+                    "name": "DownloadInvoiceTimelineEvent",
+                    "args": null
+                  }
+                ]
+              },
               {
                 "kind": "InlineFragment",
                 "type": "AirportArrivalTimelineEvent",
@@ -220,37 +286,59 @@ return {
               v3,
               {
                 "kind": "InlineFragment",
-                "type": "AirportArrivalTimelineEvent",
+                "type": "DownloadInvoiceTimelineEvent",
                 "selections": [
+                  {
+                    "kind": "ScalarField",
+                    "alias": null,
+                    "name": "invoiceUrl",
+                    "args": null,
+                    "storageKey": null
+                  },
+                  {
+                    "kind": "ScalarField",
+                    "alias": null,
+                    "name": "numberPassengers",
+                    "args": null,
+                    "storageKey": null
+                  },
                   {
                     "kind": "LinkedField",
                     "alias": null,
-                    "name": "location",
+                    "name": "legs",
                     "storageKey": null,
                     "args": null,
-                    "concreteType": "RouteStop",
-                    "plural": false,
+                    "concreteType": "Leg",
+                    "plural": true,
                     "selections": [
                       {
                         "kind": "LinkedField",
                         "alias": null,
-                        "name": "airport",
+                        "name": "departure",
                         "storageKey": null,
                         "args": null,
-                        "concreteType": "Location",
+                        "concreteType": "RouteStop",
                         "plural": false,
-                        "selections": [
-                          v4,
-                          v5
-                        ]
-                      }
+                        "selections": v6
+                      },
+                      {
+                        "kind": "LinkedField",
+                        "alias": null,
+                        "name": "arrival",
+                        "storageKey": null,
+                        "args": null,
+                        "concreteType": "RouteStop",
+                        "plural": false,
+                        "selections": v6
+                      },
+                      v5
                     ]
                   }
                 ]
               },
               {
                 "kind": "InlineFragment",
-                "type": "BookedFlightTimelineEvent",
+                "type": "AirportArrivalTimelineEvent",
                 "selections": [
                   {
                     "kind": "LinkedField",
@@ -270,24 +358,29 @@ return {
                         "concreteType": "Location",
                         "plural": false,
                         "selections": [
-                          {
-                            "kind": "LinkedField",
-                            "alias": null,
-                            "name": "city",
-                            "storageKey": null,
-                            "args": null,
-                            "concreteType": "LocationArea",
-                            "plural": false,
-                            "selections": [
-                              v4
-                            ]
-                          },
+                          v4,
                           v5
                         ]
                       }
                     ]
                   }
                 ]
+              },
+              {
+                "kind": "InlineFragment",
+                "type": "BookedFlightTimelineEvent",
+                "selections": [
+                  {
+                    "kind": "LinkedField",
+                    "alias": null,
+                    "name": "location",
+                    "storageKey": null,
+                    "args": null,
+                    "concreteType": "RouteStop",
+                    "plural": false,
+                    "selections": v6
+                  }
+                ]
               }
             ]
           }
@@ -298,5 +391,5 @@ return {
 };
 })();
 // prettier-ignore
-(node/*: any*/).hash = '68df0368b473e820e3f6203140b515e9';
+(node/*: any*/).hash = 'e3a12768b3eceb23d38c4bbaf24fc881';
 module.exports = node;
diff --git a/app/MMB/src/scenes/timeline/events/DownloadInvoiceTimelineEvent.js b/app/MMB/src/scenes/timeline/events/DownloadInvoiceTimelineEvent.js
new file mode 100644
index 00000000..83ff3f98
--- /dev/null
+++ b/app/MMB/src/scenes/timeline/events/DownloadInvoiceTimelineEvent.js
@@ -0,0 +1,167 @@
+// @flow
+
+import * as React from 'react';
+import idx from 'idx';
+import { View } from 'react-native';
+import { withNavigation } from 'react-navigation';
+import { graphql, createFragmentContainer } from '@kiwicom/mobile-relay';
+import { Text, StyleSheet, Color, TextButton } from '@kiwicom/mobile-shared';
+import {
+  type NavigationType,
+  type RouteNamesType,
+} from '@kiwicom/mobile-navigation';
+import { Translation } from '@kiwicom/mobile-localization';
+
+import Invoice from '../../../components/Invoice';
+import type { DownloadInvoiceTimelineEvent as DownloadInvoiceTimelineEventType } from './__generated__/DownloadInvoiceTimelineEvent.graphql';
+import TimelineEvent from '../TimelineEvent';
+import TimelineEventIcon from '../TimelineEventIcon';
+
+type Props = {|
+  +data: DownloadInvoiceTimelineEventType,
+  +navigation: NavigationType,
+|};
+
+function renderNote(
+  disabled: boolean,
+  numberPassengers: ?number,
+  route: ?string,
+): React.Element<typeof Translation> {
+  if (!disabled) {
+    if (numberPassengers && numberPassengers >= 1) {
+      return (
+        <Translation
+          id="mmb.booking_timeline.event.download_invoice.note_plural"
+          values={{
+            route: route,
+            passengers: numberPassengers,
+          }}
+        />
+      );
+    }
+    return (
+      <Translation
+        id="mmb.booking_timeline.event.download_invoice.note_singular"
+        values={{
+          route: route,
+          passengers: numberPassengers,
+        }}
+      />
+    );
+  }
+  return (
+    <Translation id="mmb.booking_timeline.event.download_invoice.note_not_available" />
+  );
+}
+
+function renderLeg(leg) {
+  const departureCity = idx(leg, _ => _.departure.airport.city.name);
+  const arrivalCity = idx(leg, _ => _.arrival.airport.city.name);
+  if (departureCity && arrivalCity) {
+    return departureCity + ' - ' + arrivalCity;
+  }
+  return '';
+}
+
+function renderLegs(legs) {
+  if (!legs) {
+    return '';
+  }
+  return legs.map(renderLeg).join(', ');
+}
+
+class DownloadInvoiceTimelineEvent extends React.Component<Props> {
+  navigate = (key: RouteNamesType) => {
+    this.props.navigation.navigate({
+      routeName: key,
+      key: `key-${key}`,
+    });
+  };
+
+  handleOpenInvoice = () => {
+    this.navigate('mmb.timeline.invoice');
+  };
+
+  render() {
+    const timestamp = idx(this.props, _ => _.data.timestamp);
+    const invoiceUrl = idx(this.props, _ => _.data.invoiceUrl);
+    const numberPassengers = idx(this.props, _ => _.data.numberPassengers);
+    const legs = idx(this.props, _ => _.data.legs);
+    const disabled = !invoiceUrl;
+
+    const route = renderLegs(legs);
+    const note = renderNote(disabled, numberPassengers, route);
+
+    return (
+      <TimelineEvent
+        timestamp={timestamp}
+        displayTime={false}
+        iconVertLines={<TimelineEventIcon name="ticket-confirmation" />}
+        mainContent={
+          <View style={styles.container}>
+            <Translation id="mmb.booking_timeline.event.download_invoice.title" />
+            <Text style={styles.note}>{note}</Text>
+            <View style={styles.button}>
+              <TextButton
+                disabled={disabled}
+                title={
+                  <Translation id="mmb.booking_timeline.event.download_invoice.button.title" />
+                }
+                onPress={this.handleOpenInvoice}
+              />
+            </View>
+          </View>
+        }
+      />
+    );
+  }
+}
+
+export default createFragmentContainer(
+  withNavigation(DownloadInvoiceTimelineEvent),
+  graphql`
+    fragment DownloadInvoiceTimelineEvent on DownloadInvoiceTimelineEvent {
+      timestamp
+      invoiceUrl
+      numberPassengers
+      legs {
+        departure {
+          airport {
+            city {
+              name
+            }
+          }
+        }
+        arrival {
+          airport {
+            city {
+              name
+            }
+          }
+        }
+      }
+    }
+  `,
+);
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    width: '100%',
+  },
+  note: { fontSize: 13, color: Color.ink.light },
+  button: {
+    flex: 1,
+    marginTop: 10,
+    marginBottom: 10,
+    alignSelf: 'flex-end',
+  },
+});
+
+export const TimelineSubmenuItems = {
+  'mmb.timeline.invoice': {
+    screen: function TimelineSubmenuOpenInvoice() {
+      return <Invoice />;
+    },
+  },
+};
diff --git a/app/MMB/src/scenes/timeline/events/__generated__/DownloadInvoiceTimelineEvent.graphql.js b/app/MMB/src/scenes/timeline/events/__generated__/DownloadInvoiceTimelineEvent.graphql.js
new file mode 100644
index 00000000..135b17f1
--- /dev/null
+++ b/app/MMB/src/scenes/timeline/events/__generated__/DownloadInvoiceTimelineEvent.graphql.js
@@ -0,0 +1,134 @@
+/**
+ * @flow
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ConcreteFragment } from 'relay-runtime';
+import type { FragmentReference } from "relay-runtime";
+declare export opaque type DownloadInvoiceTimelineEvent$ref: FragmentReference;
+export type DownloadInvoiceTimelineEvent = {|
+  +timestamp: ?any,
+  +invoiceUrl: ?string,
+  +numberPassengers: ?number,
+  +legs: ?$ReadOnlyArray<?{|
+    +departure: ?{|
+      +airport: ?{|
+        +city: ?{|
+          +name: ?string
+        |}
+      |}
+    |},
+    +arrival: ?{|
+      +airport: ?{|
+        +city: ?{|
+          +name: ?string
+        |}
+      |}
+    |},
+  |}>,
+  +$refType: DownloadInvoiceTimelineEvent$ref,
+|};
+*/
+
+
+const node/*: ConcreteFragment*/ = (function(){
+var v0 = [
+  {
+    "kind": "LinkedField",
+    "alias": null,
+    "name": "airport",
+    "storageKey": null,
+    "args": null,
+    "concreteType": "Location",
+    "plural": false,
+    "selections": [
+      {
+        "kind": "LinkedField",
+        "alias": null,
+        "name": "city",
+        "storageKey": null,
+        "args": null,
+        "concreteType": "LocationArea",
+        "plural": false,
+        "selections": [
+          {
+            "kind": "ScalarField",
+            "alias": null,
+            "name": "name",
+            "args": null,
+            "storageKey": null
+          }
+        ]
+      }
+    ]
+  }
+];
+return {
+  "kind": "Fragment",
+  "name": "DownloadInvoiceTimelineEvent",
+  "type": "DownloadInvoiceTimelineEvent",
+  "metadata": null,
+  "argumentDefinitions": [],
+  "selections": [
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "timestamp",
+      "args": null,
+      "storageKey": null
+    },
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "invoiceUrl",
+      "args": null,
+      "storageKey": null
+    },
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "numberPassengers",
+      "args": null,
+      "storageKey": null
+    },
+    {
+      "kind": "LinkedField",
+      "alias": null,
+      "name": "legs",
+      "storageKey": null,
+      "args": null,
+      "concreteType": "Leg",
+      "plural": true,
+      "selections": [
+        {
+          "kind": "LinkedField",
+          "alias": null,
+          "name": "departure",
+          "storageKey": null,
+          "args": null,
+          "concreteType": "RouteStop",
+          "plural": false,
+          "selections": v0
+        },
+        {
+          "kind": "LinkedField",
+          "alias": null,
+          "name": "arrival",
+          "storageKey": null,
+          "args": null,
+          "concreteType": "RouteStop",
+          "plural": false,
+          "selections": v0
+        }
+      ]
+    }
+  ]
+};
+})();
+// prettier-ignore
+(node/*: any*/).hash = '2120e3d24995e2db43ed98ec984a22f5';
+module.exports = node;
diff --git a/packages/localization/src/vocabularies/ManageMyBooking.js b/packages/localization/src/vocabularies/ManageMyBooking.js
index 487ebbc5..afd424cd 100644
--- a/packages/localization/src/vocabularies/ManageMyBooking.js
+++ b/packages/localization/src/vocabularies/ManageMyBooking.js
@@ -149,4 +149,14 @@ export default {
   'mmb.booking_timeline.event.leave_for_airport.title': 'Leave for the airport',
   'mmb.booking_timeline.event.leave_for_airport.note':
     "Don't forget your passport/photo ID.",
+
+  'mmb.booking_timeline.event.download_invoice.title': 'Download invoice',
+  'mmb.booking_timeline.event.download_invoice.note_not_available':
+    'The invoice is not yet available.',
+  'mmb.booking_timeline.event.download_invoice.note_singular':
+    '__1_route__, __2_passengers__× traveller.',
+
+  'mmb.booking_timeline.event.download_invoice.note_plural':
+    '__1_route__, __2_passengers__× travellers.',
+  'mmb.booking_timeline.event.download_invoice.button.title': 'Download',
 };
diff --git a/packages/navigation/types/Navigation.js b/packages/navigation/types/Navigation.js
index ae0179fb..2e057a68 100644
--- a/packages/navigation/types/Navigation.js
+++ b/packages/navigation/types/Navigation.js
@@ -37,6 +37,7 @@ type MMBStackNavigatorNames =
   | 'mmb.other.open'
   | 'mmb.other.refund'
   | 'mmb.tickets.e_ticket'
+  | 'mmb.timeline.invoice'
   | 'DetailScreen';
 
 type HomepageStackNavigatorRouteNames = 'Home';
diff --git a/packages/shared/src/buttons/TextButton.js b/packages/shared/src/buttons/TextButton.js
index 50fcfb20..afc9171e 100644
--- a/packages/shared/src/buttons/TextButton.js
+++ b/packages/shared/src/buttons/TextButton.js
@@ -19,8 +19,18 @@ type Props = {|
  */
 export default function TextButton(props: Props) {
   return (
-    <Button onPress={props.onPress} disabled={props.disabled}>
-      <ButtonTitle style={styleSheet.buttonText} text={props.title} />
+    <Button
+      onPress={props.onPress}
+      disabled={props.disabled}
+      style={props.disabled && styleSheet.buttonDisabled}
+    >
+      <ButtonTitle
+        style={[
+          styleSheet.buttonText,
+          props.disabled && styleSheet.buttonTextDisabled,
+        ]}
+        text={props.title}
+      />
     </Button>
   );
 }
@@ -33,4 +43,10 @@ const styleSheet = StyleSheet.create({
   buttonText: {
     color: Color.white,
   },
+  buttonDisabled: {
+    backgroundColor: Color.grey.$300,
+  },
+  buttonTextDisabled: {
+    color: Color.ink.dark,
+  },
 });
diff --git a/schema.graphql b/schema.graphql
index 26a33711..d0a6a12d 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -804,12 +804,18 @@ type DownloadETicketTimelineEvent implements TimelineEvent {
   ticketUrl: String
 }
 
-type DownloadReceiptTimelineEvent implements TimelineEvent {
+type DownloadInvoiceTimelineEvent implements TimelineEvent {
   """Time of appearance in the timeline."""
   timestamp: DateTime
 
-  """URL of the receipt/invoice"""
-  receiptUrl: String
+  """URL of the invoice"""
+  invoiceUrl: String
+
+  """Number of passengers"""
+  numberPassengers: Int
+
+  """Legs of the booking"""
+  legs: [Leg]
 }
 
 input DurationInput {
@@ -880,6 +886,9 @@ type FAQArticle {
   """The ID of an object"""
   id: ID!
 
+  """Original numeric id of the article"""
+  originalId: Int
+
   """Title of the article"""
   title: String
 
@@ -918,6 +927,9 @@ type FAQCategory {
   """The ID of an object"""
   id: ID!
 
+  """Original numeric id of the FAQ category"""
+  originalId: Int
+
   """Title of the FAQ category"""
   title: String
 


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 d499f32a..ca955cac 100644
--- a/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap
+++ b/packages/shared/src/buttons/__tests__/__snapshots__/TextButton.test.js.snap
@@ -4,12 +4,16 @@ exports[`renders button with custom title 1`] = `
 <Button
   disabled={false}
   onPress={[Function]}
+  style={false}
 >
   <ButtonTitle
     style={
-      Object {
-        "color": "#ffffff",
-      }
+      Array [
+        Object {
+          "color": "#ffffff",
+        },
+        false,
+      ]
     }
     text={
       <Translation