Repository: kiwicom/mobile
Pull request #1036 (2018-08-28 14:22:29)

Android card module Native<->JS protocol
- Prepared to be implemented by native apps that consume it
- Provides a mock implementation using shared preferences


Patch:
diff --git a/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/Card.kt b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/Card.kt
new file mode 100644
index 00000000..e4ba7f32
--- /dev/null
+++ b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/Card.kt
@@ -0,0 +1,8 @@
+package com.skypicker.reactnative.nativemodules.card
+
+class Card {
+  var number: String = ""
+  var expiryMonth: String = ""
+  var expiryYear: String = ""
+  var cardholder: String = ""
+}
\ No newline at end of file
diff --git a/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/CardCallback.kt b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/CardCallback.kt
new file mode 100644
index 00000000..be92d4d7
--- /dev/null
+++ b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/CardCallback.kt
@@ -0,0 +1,6 @@
+package com.skypicker.reactnative.nativemodules.card
+
+interface CardCallback {
+    fun getCard(): Card?
+    fun saveCard(card: Card)
+}
\ No newline at end of file
diff --git a/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManager.kt b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManager.kt
new file mode 100644
index 00000000..306500e8
--- /dev/null
+++ b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManager.kt
@@ -0,0 +1,45 @@
+package com.skypicker.reactnative.nativemodules.card
+
+import com.facebook.react.bridge.*
+
+class RNCardManager(
+  reactContext: ReactApplicationContext,
+  listener: CardCallback
+) :
+  ReactContextBaseJavaModule(reactContext) {
+
+  private object Constant {
+    const val TAG = "RNCardManager"
+  }
+
+  private val cardListener = listener
+
+  override fun getName(): String = Constant.TAG
+
+  @ReactMethod
+  fun getCard(promise: Promise) {
+    val card = cardListener.getCard()
+    if (card != null) {
+      val map = Arguments.createMap()
+      map.putString("number", card.number)
+      map.putString("expiryMonth", card.expiryMonth)
+      map.putString("expiryYear", card.expiryYear)
+      map.putString("cardholder", card.cardholder)
+      promise.resolve(map)
+      return
+    }
+    promise.resolve(null)
+  }
+
+  @ReactMethod
+  fun saveCard(map: ReadableMap) {
+    // Mock implementation
+    val card = Card()
+    card.number = map.getString("number")
+    card.expiryMonth = map.getString("expiryMonth")
+    card.expiryYear = map.getString("expiryYear")
+    card.cardholder = map.getString("cardholder")
+
+    cardListener.saveCard(card)
+  }
+}
diff --git a/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManagerPackage.kt b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManagerPackage.kt
new file mode 100644
index 00000000..dac06c66
--- /dev/null
+++ b/RNAndroidPlayground/react-native-native-modules/src/main/java/com/skypicker/reactnative/nativemodules/card/RNCardManagerPackage.kt
@@ -0,0 +1,49 @@
+package com.skypicker.reactnative.nativemodules.card
+
+import android.content.Context
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.*
+import com.facebook.react.uimanager.ViewManager
+import com.google.gson.Gson
+import java.util.*
+
+class RNCardManagerPackage(private val listener: CardCallback? = null) : ReactPackage {
+
+  private fun createMock(reactContext: ReactApplicationContext) = object : CardCallback {
+    val PREF_KEY = "PREFS_SAVED_CARD"
+
+    override fun getCard(): Card? {
+      // Mock implementation
+      val prefs = reactContext.getSharedPreferences("prefs", Context.MODE_PRIVATE)
+      val cardString = prefs.getString(PREF_KEY, null)
+
+      val gson = Gson()
+      val card = if (cardString != null) gson.fromJson(cardString, Card::class.java) else null
+
+      if (card != null) {
+        return card
+      }
+
+      return null
+    }
+
+    override fun saveCard(card: Card) {
+      // Mock implementation
+      val gson = Gson()
+      val json = gson.toJson(card)
+      val prefs = reactContext.getSharedPreferences("prefs", Context.MODE_PRIVATE)
+
+      prefs.edit().putString(PREF_KEY, json).apply()
+    }
+  }
+
+  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
+    return emptyList()
+  }
+
+  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
+    val modules = ArrayList<NativeModule>()
+    modules.add(RNCardManager(reactContext, listener ?: createMock(reactContext)))
+    return modules
+  }
+}
diff --git a/android/app/src/main/java/com/reactnativeapp/MainApplication.java b/android/app/src/main/java/com/reactnativeapp/MainApplication.java
index 31f10934..73adf0c2 100644
--- a/android/app/src/main/java/com/reactnativeapp/MainApplication.java
+++ b/android/app/src/main/java/com/reactnativeapp/MainApplication.java
@@ -11,6 +11,7 @@ import com.facebook.react.shell.MainReactPackage;
 import com.facebook.soloader.SoLoader;
 import com.oblador.vectoricons.VectorIconsPackage;
 import com.reactlibrary.RNTooltipsPackage;
+import com.skypicker.reactnative.nativemodules.card.RNCardManagerPackage;
 import com.skypicker.reactnative.nativemodules.currency.RNCurrencyManagerPackage;
 import com.skypicker.reactnative.nativemodules.device.RNDeviceInfoPackage;
 import com.skypicker.reactnative.nativemodules.logging.RNLoggingPackage;
@@ -45,7 +46,8 @@ public class MainApplication extends MultiDexApplication implements ShareApplica
                     new RNLoggingPackage(),
                     new RNTranslationManagerPackage(),
                     new RNCurrencyManagerPackage(),
-                    new RNDeviceInfoPackage()
+                    new RNDeviceInfoPackage(),
+                    new RNCardManagerPackage()
             );
         }
 
diff --git a/app/MMB/src/navigation/AddressPickerScreen.js b/app/MMB/src/navigation/AddressPickerScreen.js
index 33f2682b..83de4695 100644
--- a/app/MMB/src/navigation/AddressPickerScreen.js
+++ b/app/MMB/src/navigation/AddressPickerScreen.js
@@ -5,6 +5,8 @@ import { View } from 'react-native';
 import { Translation } from '@kiwicom/mobile-localization';
 import { HeaderButton, type NavigationType } from '@kiwicom/mobile-navigation';
 
+import AddressLocationInput from '../scenes/tripServices/transportation/AddressLocationInput';
+
 type Props = {|
   +navigation: NavigationType,
   +currentAddress: ?string,
@@ -16,6 +18,10 @@ export default class AddressPickerScreen extends React.Component<Props> {
       props.navigation.goBack();
     }
 
+    function todo() {
+      console.warn('todo');
+    }
+
     return {
       headerLeft: (
         <HeaderButton.CloseModal
@@ -25,13 +31,11 @@ export default class AddressPickerScreen extends React.Component<Props> {
           }
         />
       ),
-      headerTitle: (
-        <View>
-          <Translation passThrough="TODO" />
-        </View>
-      ),
+      headerTitle: <AddressLocationInput onChangeText={todo} />,
+      headerStyle: { paddingBottom: 5 },
     };
   };
+
   render() {
     return (
       <View>
diff --git a/app/MMB/src/navigation/NavigationStack.js b/app/MMB/src/navigation/NavigationStack.js
index 02cf8b3e..d2cab1d8 100644
--- a/app/MMB/src/navigation/NavigationStack.js
+++ b/app/MMB/src/navigation/NavigationStack.js
@@ -1,5 +1,6 @@
 // @flow
 
+import * as React from 'react';
 import {
   StackNavigator,
   StackNavigatorOptions,
@@ -24,14 +25,16 @@ Object.entries(MenuComponents).forEach(
   ([routeName, { screen, headerTitle, headerRight, headerStyle }]) => {
     Screens[routeName] = {
       screen: withMappedProps(screen),
-      navigationOptions: {
+      navigationOptions: (props: Object) => ({
         headerTitle,
-        headerRight,
+        headerRight:
+          headerRight == null ? null : React.cloneElement(headerRight, props),
         headerStyle: {
+          ...StackNavigatorOptions.navigationOptions.headerStyle,
           ...headerStyle,
           backgroundColor: defaultTokens.paletteWhite,
         },
-      },
+      }),
     };
   },
 );
diff --git a/app/MMB/src/scenes/tickets/TicketDeleteButton.js b/app/MMB/src/scenes/tickets/TicketDeleteButton.js
index 5c1dc890..eeeb7f27 100644
--- a/app/MMB/src/scenes/tickets/TicketDeleteButton.js
+++ b/app/MMB/src/scenes/tickets/TicketDeleteButton.js
@@ -65,7 +65,7 @@ export class TicketDeleteButton extends React.Component<
   deleteFilesPromise = async () => {
     try {
       await RNFetchBlob.fs.unlink(this.getEticketPath());
-      await this.deleteBoardingPasses();
+      await RNFetchBlob.fs.unlink(this.getBoardingPassPath());
       this.setState({ hasLocalFiles: false });
     } catch (error) {
       Alert.translatedAlert(null, {
@@ -75,34 +75,12 @@ export class TicketDeleteButton extends React.Component<
     }
   };
 
-  deleteBoardingPasses = async () => {
-    try {
-      const boardingPasses = await RNFetchBlob.fs.ls(
-        this.getBoardingPassPath(),
-      );
-      boardingPasses.forEach(async boardingPass => {
-        if (boardingPass.includes(`-bookingId:${this.props.bookingId}`)) {
-          await RNFetchBlob.fs.unlink(
-            `${this.getBoardingPassPath()}/${boardingPass}`,
-          );
-        }
-      });
-    } catch (error) {
-      throw error;
-    }
-  };
-
   hasLocalFiles = async () => {
     const existsEticket = await RNFetchBlob.fs.exists(this.getEticketPath());
-    const existsBoardingPasses = await this.hasLocalBoardingPasses();
-    this.setState({ hasLocalFiles: existsEticket || existsBoardingPasses });
-  };
-
-  hasLocalBoardingPasses = async () => {
-    const boardingPasses = await RNFetchBlob.fs.ls(this.getBoardingPassPath());
-    return boardingPasses.some(file =>
-      file.includes(`-bookingId:${this.props.bookingId}`),
+    const existsBoardingPasses = await RNFetchBlob.fs.exists(
+      this.getBoardingPassPath(),
     );
+    this.setState({ hasLocalFiles: existsEticket || existsBoardingPasses });
   };
 
   getEticketPath = () => {
@@ -114,7 +92,7 @@ export class TicketDeleteButton extends React.Component<
 
   getBoardingPassPath = () => {
     const { DocumentDir } = RNFetchBlob.fs.dirs;
-    return `${DocumentDir}/boardingPasses`;
+    return `${DocumentDir}/boardingPasses/${this.props.bookingId}`;
   };
 
   render = () => {
diff --git a/app/MMB/src/scenes/tickets/TicketScene.js b/app/MMB/src/scenes/tickets/TicketScene.js
index 458ddaee..20a0a26a 100644
--- a/app/MMB/src/scenes/tickets/TicketScene.js
+++ b/app/MMB/src/scenes/tickets/TicketScene.js
@@ -6,8 +6,12 @@ import { PublicApiRenderer, graphql } from '@kiwicom/mobile-relay';
 import BookingDetailContext from '../../context/BookingDetailContext';
 import type { TicketSceneQueryResponse } from './__generated__/TicketSceneQuery.graphql';
 import TicketRefetch from './TicketRefetch';
-import ETicketPdfScreen from './eTicketPdf/ETicketPdfScreen';
-import BoardingPassPdfScreen from './boardingPassesPdf/BoardingPassPdfScreen';
+import ETicketPdfScreen, {
+  ShareButtonWithContext,
+} from './eTicketPdf/ETicketPdfScreen';
+import BoardingPassPdfScreen, {
+  BoardingPassShareButtonWithContext,
+} from './boardingPassesPdf/BoardingPassPdfScreen';
 
 type Props = {|
   +data: TicketSceneQueryResponse,
@@ -42,8 +46,10 @@ export default class TicketScene extends React.Component<Props> {
 export const TicketSceneSubMenus = {
   'mmb.tickets.e_ticket': {
     screen: ETicketPdfScreen,
+    headerRight: <ShareButtonWithContext />,
   },
   'mmb.tickets.boarding_pass': {
     screen: BoardingPassPdfScreen,
+    headerRight: <BoardingPassShareButtonWithContext />,
   },
 };
diff --git a/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js b/app/MMB/src/scenes/tickets/__generated__/TicketRefetchQuery.graphql.js
index 95b45271..c1ce2834 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 4863ae16b1d47d7a471ba914acce5401
+ * @relayHash a55f1532ef590edf9a3d2e0c43241894
  */
 
 /* eslint-disable */
@@ -128,6 +128,7 @@ fragment FlightFromTo on Leg {
 }
 
 fragment DownloadButton on BoardingPass {
+  flightNumber
   boardingPassUrl
   ...BoardingPassInformation
 }
@@ -282,6 +283,13 @@ v5 = {
       "concreteType": "BoardingPass",
       "plural": false,
       "selections": [
+        {
+          "kind": "ScalarField",
+          "alias": null,
+          "name": "flightNumber",
+          "args": null,
+          "storageKey": null
+        },
         {
           "kind": "ScalarField",
           "alias": null,
@@ -351,7 +359,7 @@ return {
   "operationKind": "query",
   "name": "TicketRefetchQuery",
   "id": null,
-  "text": "query TicketRefetchQuery(\n  $id: ID!\n) {\n  node(id: $id) {\n    __typename\n    ... on BookingInterface {\n      ...TicketRefetch\n    }\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  id\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    ...AppleWallet\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment AppleWallet on BoardingPass {\n  pkpasses {\n    ...AppleWalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment AppleWalletPassenger 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: ID!\n) {\n  node(id: $id) {\n    __typename\n    ... on BookingInterface {\n      ...TicketRefetch\n    }\n    id\n  }\n}\n\nfragment TicketRefetch on BookingInterface {\n  id\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    ...AppleWallet\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment AppleWallet on BoardingPass {\n  pkpasses {\n    ...AppleWalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment AppleWalletPassenger 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",
   "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 e69a4dc6..ddcfa572 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 f6f71a6bb47819ddb1448373ab2c2611
+ * @relayHash c81a8202223402b6ccca75459ebd17db
  */
 
 /* eslint-disable */
@@ -128,6 +128,7 @@ fragment FlightFromTo on Leg {
 }
 
 fragment DownloadButton on BoardingPass {
+  flightNumber
   boardingPassUrl
   ...BoardingPassInformation
 }
@@ -294,6 +295,13 @@ v5 = {
       "concreteType": "BoardingPass",
       "plural": false,
       "selections": [
+        {
+          "kind": "ScalarField",
+          "alias": null,
+          "name": "flightNumber",
+          "args": null,
+          "storageKey": null
+        },
         {
           "kind": "ScalarField",
           "alias": null,
@@ -363,7 +371,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  id\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    ...AppleWallet\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment AppleWallet on BoardingPass {\n  pkpasses {\n    ...AppleWalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment AppleWalletPassenger 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  id\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    ...AppleWallet\n  }\n}\n\nfragment DownloadButton on BoardingPass {\n  flightNumber\n  boardingPassUrl\n  ...BoardingPassInformation\n}\n\nfragment AppleWallet on BoardingPass {\n  pkpasses {\n    ...AppleWalletPassenger\n    passenger {\n      databaseId\n    }\n  }\n}\n\nfragment AppleWalletPassenger 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",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/DownloadButton.js b/app/MMB/src/scenes/tickets/boardingPasses/DownloadButton.js
index a7b78755..6114c740 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/DownloadButton.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/DownloadButton.js
@@ -23,6 +23,7 @@ class DownloadButton extends React.Component<Props> {
   navigateToBoardingPass = () => {
     this.props.navigation.navigate('mmb.tickets.boarding_pass', {
       boardingPassUrl: idx(this.props, _ => _.data.boardingPassUrl),
+      flightNumber: idx(this.props, _ => _.data.flightNumber),
     });
   };
 
@@ -49,6 +50,7 @@ export default createFragmentContainer(
   withNavigation(DownloadButton),
   graphql`
     fragment DownloadButton on BoardingPass {
+      flightNumber
       boardingPassUrl
       ...BoardingPassInformation
     }
diff --git a/app/MMB/src/scenes/tickets/boardingPasses/__generated__/DownloadButton.graphql.js b/app/MMB/src/scenes/tickets/boardingPasses/__generated__/DownloadButton.graphql.js
index 1281a933..a33025ab 100644
--- a/app/MMB/src/scenes/tickets/boardingPasses/__generated__/DownloadButton.graphql.js
+++ b/app/MMB/src/scenes/tickets/boardingPasses/__generated__/DownloadButton.graphql.js
@@ -12,6 +12,7 @@ type BoardingPassInformation$ref = any;
 import type { FragmentReference } from "relay-runtime";
 declare export opaque type DownloadButton$ref: FragmentReference;
 export type DownloadButton = {|
+  +flightNumber: ?string,
   +boardingPassUrl: ?string,
   +$fragmentRefs: BoardingPassInformation$ref,
   +$refType: DownloadButton$ref,
@@ -26,6 +27,13 @@ const node/*: ConcreteFragment*/ = {
   "metadata": null,
   "argumentDefinitions": [],
   "selections": [
+    {
+      "kind": "ScalarField",
+      "alias": null,
+      "name": "flightNumber",
+      "args": null,
+      "storageKey": null
+    },
     {
       "kind": "ScalarField",
       "alias": null,
@@ -41,5 +49,5 @@ const node/*: ConcreteFragment*/ = {
   ]
 };
 // prettier-ignore
-(node/*: any*/).hash = '2083b291900c2bf503115ebe519df49f';
+(node/*: any*/).hash = '377dd4859b8f63343575db0175eb213c';
 module.exports = node;
diff --git a/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdf.js b/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdf.js
index e655747f..f2d774b7 100644
--- a/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdf.js
+++ b/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdf.js
@@ -7,18 +7,6 @@ import { PdfViewAndStore } from '@kiwicom/mobile-pdf';
 
 import BookingDetailContext from '../../../context/BookingDetailContext';
 
-export const getPdfFilenameFromUrl = (url: string, bookingId: number) => {
-  const filename = url.slice(url.lastIndexOf('/') + 1);
-  const isPdf = filename.includes('.pdf');
-  if (!isPdf) {
-    return null;
-  }
-  return `${filename.slice(
-    0,
-    filename.indexOf('.pdf'),
-  )}-bookingId:${bookingId}`;
-};
-
 type PropsWithContext = {|
   ...Props,
   +bookingId: number,
@@ -27,19 +15,17 @@ type PropsWithContext = {|
 export const BoardingPassPdf = ({
   boardingPassUrl,
   bookingId,
+  flightNumber,
 }: PropsWithContext) => {
-  if (boardingPassUrl) {
-    const filename = getPdfFilenameFromUrl(boardingPassUrl, bookingId);
-
-    if (filename) {
-      return (
-        <PdfViewAndStore
-          fileName={`boardingPasses/${filename}.pdf`}
-          url={boardingPassUrl}
-        />
-      );
-    }
+  if (bookingId && flightNumber && boardingPassUrl) {
+    return (
+      <PdfViewAndStore
+        fileName={`boardingPasses/${bookingId}/${flightNumber}.pdf`}
+        url={boardingPassUrl}
+      />
+    );
   }
+
   return (
     <GeneralError
       errorMessage={<Translation id="mmb.boarding_passes.not_available" />}
@@ -49,6 +35,7 @@ export const BoardingPassPdf = ({
 
 type Props = {|
   +boardingPassUrl: ?string,
+  +flightNumber: string,
 |};
 
 export default function BoardingPassPdfWithContext(props: Props) {
diff --git a/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdfScreen.js b/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdfScreen.js
index d65561d6..4d311a66 100644
--- a/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdfScreen.js
+++ b/app/MMB/src/scenes/tickets/boardingPassesPdf/BoardingPassPdfScreen.js
@@ -1,15 +1,82 @@
 // @flow strict
 
 import * as React from 'react';
+import Share from 'react-native-share';
+import { documentDir } from '@kiwicom/mobile-assets';
+import type { NavigationType } from '@kiwicom/mobile-navigation';
+import idx from 'idx';
 
 import BoardingPassesPdf from './BoardingPassPdf';
+import ShareButton from '../../../components/ShareButton';
+import BookingDetailContext from '../../../context/BookingDetailContext';
+
+type ShareButtonPropsWithContext = {|
+  +boardingPassUrl: string,
+  +bookingId: number,
+  +flightNumber: string,
+|};
+
+class BoardingPassShareButton extends React.Component<
+  ShareButtonPropsWithContext,
+> {
+  onPress = () => {
+    this.sharePromise();
+  };
+
+  sharePromise = async () => {
+    try {
+      const boardingPassUrl = `${documentDir}/boardingPasses/${
+        this.props.bookingId
+      }/${this.props.flightNumber}.pdf`;
+
+      await Share.open({ url: boardingPassUrl });
+    } catch (err) {
+      // No worries, user just canceled share
+      return;
+    }
+  };
+
+  render = () => <ShareButton onPress={this.onPress} />;
+}
+
+type ShareButtonProps = {|
+  +navigation?: NavigationType,
+|};
+
+export function BoardingPassShareButtonWithContext(props: ShareButtonProps) {
+  const boardingPassUrl = idx(
+    props,
+    _ => _.navigation.state.params.boardingPassUrl,
+  );
+  const flightNumber = idx(props, _ => _.navigation.state.params.flightNumber);
+
+  if (boardingPassUrl == null || flightNumber == null) {
+    return null;
+  }
+
+  return (
+    <BookingDetailContext.Consumer>
+      {({ bookingId }) => (
+        <BoardingPassShareButton
+          boardingPassUrl={boardingPassUrl}
+          bookingId={bookingId}
+          flightNumber={flightNumber}
+        />
+      )}
+    </BookingDetailContext.Consumer>
+  );
+}
 
 type Props = {|
   +boardingPassUrl: ?string,
+  +flightNumber: string,
 |};
 
-const BoardingPassesPdfScreen = ({ boardingPassUrl }: Props) => (
-  <BoardingPassesPdf boardingPassUrl={boardingPassUrl} />
+const BoardingPassesPdfScreen = ({ boardingPassUrl, flightNumber }: Props) => (
+  <BoardingPassesPdf
+    boardingPassUrl={boardingPassUrl}
+    flightNumber={flightNumber}
+  />
 );
 
 export default BoardingPassesPdfScreen;
diff --git a/app/MMB/src/scenes/tickets/eTicketPdf/ETicketPdfScreen.js b/app/MMB/src/scenes/tickets/eTicketPdf/ETicketPdfScreen.js
index 3deedf3c..f32e985e 100644
--- a/app/MMB/src/scenes/tickets/eTicketPdf/ETicketPdfScreen.js
+++ b/app/MMB/src/scenes/tickets/eTicketPdf/ETicketPdfScreen.js
@@ -4,7 +4,7 @@ import * as React from 'react';
 import Share from 'react-native-share';
 import { documentDir } from '@kiwicom/mobile-assets';
 
-import ETicketPdf from './ETicketPdf';
+import ETicketPdf, { getEticketPath } from './ETicketPdf';
 import ShareButton from '../../../components/ShareButton';
 import BookingDetailContext from '../../../context/BookingDetailContext';
 
@@ -14,14 +14,25 @@ type ShareButtonProps = {|
 
 class ETicketShareButton extends React.Component<ShareButtonProps> {
   onPress = () => {
-    const eTicketPath = `${documentDir}/eTickets/${this.props.bookingId}.pdf`;
-    Share.open({ url: eTicketPath });
+    this.sharePromise();
+  };
+
+  sharePromise = async () => {
+    try {
+      const eTicketPath = `${documentDir}/${getEticketPath(
+        this.props.bookingId,
+      )}`;
+      await Share.open({ url: eTicketPath });
+    } catch (err) {
+      // No worries, user just canceled share
+      return;
+    }
   };
 
   render = () => <ShareButton onPress={this.onPress} />;
 }
 
-function ShareButtonWithContext() {
+export function ShareButtonWithContext() {
   return (
     <BookingDetailContext.Consumer>
       {({ bookingId }) => <ETicketShareButton bookingId={bookingId} />}
@@ -37,8 +48,4 @@ const ETicketPdfScreen = ({ ticketUrl }: Props) => (
   <ETicketPdf ticketUrl={ticketUrl} />
 );
 
-ETicketPdfScreen.navigationOptions = () => ({
-  headerRight: <ShareButtonWithContext />,
-});
-
 export default ETicketPdfScreen;
diff --git a/app/MMB/src/scenes/timeline/events/DownloadBoardingPassTimelineEvent.js b/app/MMB/src/scenes/timeline/events/DownloadBoardingPassTimelineEvent.js
index 9816fde8..b54e1cbf 100644
--- a/app/MMB/src/scenes/timeline/events/DownloadBoardingPassTimelineEvent.js
+++ b/app/MMB/src/scenes/timeline/events/DownloadBoardingPassTimelineEvent.js
@@ -34,6 +34,7 @@ class DownloadBoardingPassTimelineEvent extends React.Component<Props> {
         this.props,
         _ => _.data.leg.boardingPass.boardingPassUrl,
       ),
+      flightNumber: idx(this.props, _ => _.data.leg.boardingPass.flightNumber),
     });
   };
 
diff --git a/app/MMB/src/scenes/tripServices/transportation/AddressLocationInput.js b/app/MMB/src/scenes/tripServices/transportation/AddressLocationInput.js
new file mode 100644
index 00000000..5f2a0766
--- /dev/null
+++ b/app/MMB/src/scenes/tripServices/transportation/AddressLocationInput.js
@@ -0,0 +1,71 @@
+// @flow
+
+import * as React from 'react';
+import { View } from 'react-native';
+import { StyleSheet, TextIcon, TextInput, Text } from '@kiwicom/mobile-shared';
+import { Translation } from '@kiwicom/mobile-localization';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
+
+type Props = {|
+  +defaultValue?: string,
+  +autoFocus?: boolean,
+  +onChangeText?: (text: string) => void,
+  +keyboardType?: 'email-address',
+  +secureTextEntry?: boolean,
+|};
+
+export default function AddressLocationInput(props: Props) {
+  return (
+    <View style={styles.wrapper}>
+      <View style={[styles.iconWrapper, styles.inputWrapperStyle]}>
+        <TextIcon code="B" style={styles.icon} />
+        <Text style={styles.text}>
+          <Translation id="mmb.trip_services.transportation.address_picker.input.placeholder" />
+        </Text>
+      </View>
+      <View style={styles.inputWrapper}>
+        <TextInput {...props} inputWrapperStyle={styles.inputWrapperStyle} />
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  wrapper: {
+    position: 'absolute',
+    start: 0,
+    end: -65,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  inputWrapper: {
+    flex: 1,
+    paddingStart: 52,
+  },
+  inputWrapperStyle: {
+    android: {
+      elevation: 1,
+      height: 38,
+    },
+    ios: {
+      height: 37,
+    },
+  },
+  iconWrapper: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    position: 'absolute',
+    start: 0,
+    backgroundColor: defaultTokens.paletteCloudNormal,
+    borderTopStartRadius: 6,
+    borderBottomStartRadius: 6,
+  },
+  icon: {
+    fontSize: 14,
+    padding: 10,
+    color: defaultTokens.paletteInkLight,
+  },
+  text: {
+    color: defaultTokens.paletteInkLight,
+  },
+});
diff --git a/packages/rnmodules/card/CardManager.js b/packages/rnmodules/card/CardManager.js
new file mode 100644
index 00000000..44742d73
--- /dev/null
+++ b/packages/rnmodules/card/CardManager.js
@@ -0,0 +1,18 @@
+// @flow
+
+import { NativeModules } from 'react-native';
+
+type CardType = {
+  cardholder: string,
+  expiryMonth: string,
+  expiryYear: string,
+  number: string,
+};
+
+export const getCard = (): Promise<CardType> | null => {
+  return NativeModules.RNCardManager.getCard();
+};
+
+export const saveCard = (card: CardType) => {
+  NativeModules.RNCardManager.saveCard(card);
+};
diff --git a/packages/rnmodules/card/README.md b/packages/rnmodules/card/README.md
new file mode 100644
index 00000000..555881aa
--- /dev/null
+++ b/packages/rnmodules/card/README.md
@@ -0,0 +1,28 @@
+# CardManager
+
+This module allows to save card data and retrieve it afterwards.
+
+## API
+
+### getCard
+
+ ```js
+getCard(): Promise<Card | null>
+```
+
+### saveCard
+
+ ```js
+saveCard(card: Card): void
+```
+
+## Types
+
+### Card
+
+```js
+cardholder: string,
+expiryMonth: string,
+expiryYear: string,
+number: string,
+```
\ No newline at end of file
diff --git a/packages/rnmodules/index.js b/packages/rnmodules/index.js
index 37ce288b..1a3f4794 100644
--- a/packages/rnmodules/index.js
+++ b/packages/rnmodules/index.js
@@ -2,3 +2,4 @@
 
 export { translate } from './translation/TranslationManager';
 export { formatAmount } from './currency/CurrencyManager';
+export { getCard, saveCard } from './card/CardManager';
diff --git a/packages/shared/src/forms/TextInput.js b/packages/shared/src/forms/TextInput.js
index cdee8a13..847c7927 100644
--- a/packages/shared/src/forms/TextInput.js
+++ b/packages/shared/src/forms/TextInput.js
@@ -18,6 +18,7 @@ type Props = {|
   +secureTextEntry?: boolean,
   +label?: React.Element<typeof Translation>,
   +labelStyle?: StylePropType,
+  +inputWrapperStyle?: StylePropType,
 |};
 
 type State = {|
@@ -53,7 +54,7 @@ export default class TextInput extends React.Component<Props, State> {
           {this.props.label}
         </Text>
       )}
-      <View style={styleSheet.wrapper}>
+      <View style={[styleSheet.wrapper, this.props.inputWrapperStyle]}>
         <OriginalTextInput
           underlineColorAndroid="transparent"
           autoCorrect={false}


Test patch:
diff --git a/app/MMB/src/scenes/tickets/__tests__/TicketDeleteButton.test.js b/app/MMB/src/scenes/tickets/__tests__/TicketDeleteButton.test.js
index 73e771cc..49ec589e 100644
--- a/app/MMB/src/scenes/tickets/__tests__/TicketDeleteButton.test.js
+++ b/app/MMB/src/scenes/tickets/__tests__/TicketDeleteButton.test.js
@@ -2,16 +2,11 @@
 
 import * as React from 'react';
 import renderer from 'react-test-renderer';
-import RNFetchBlob from 'react-native-fetch-blob';
 
 import { TicketDeleteButton } from '../TicketDeleteButton';
 
 jest.mock('react-native-fetch-blob', () => {
-  let lsReturn = [];
   return {
-    setLsReturn: input => {
-      lsReturn = input;
-    },
     config: () => ({
       fetch: async () => ({
         path: () => {},
@@ -24,7 +19,7 @@ jest.mock('react-native-fetch-blob', () => {
       },
       exists: async () => true,
       unlink: async () => true,
-      ls: async () => lsReturn,
+      ls: async () => true,
     },
   };
 });
@@ -80,37 +75,4 @@ describe('TicketDeleteButton', () => {
     Component.componentDidUpdate({ isFocused: true, bookingId: 123456 });
     expect(spy).not.toHaveBeenCalled();
   });
-
-  describe('hasLocalBoardingPasses', () => {
-    it('should return false if no files has bookingId in filename', async () => {
-      const Component = new TicketDeleteButton({
-        bookingId: 123456,
-        isFocused: true,
-      });
-
-      RNFetchBlob.setLsReturn([
-        'test-bookingId:123.pdf',
-        'lol-bookingId:321.pdf',
-      ]);
-
-      const hasLocalBoardingPasses = await Component.hasLocalBoardingPasses();
-      expect(hasLocalBoardingPasses).toEqual(false);
-    });
-
-    it('should return true if any file has bookingId in filename', async () => {
-      const Component = new TicketDeleteButton({
-        bookingId: 123456,
-        isFocused: true,
-      });
-
-      RNFetchBlob.setLsReturn([
-        'test-bookingId:123.pdf',
-        'lol-bookingId:321.pdf',
-        'lol-bookingId:123456.pdf',
-      ]);
-
-      const hasLocalBoardingPasses = await Component.hasLocalBoardingPasses();
-      expect(hasLocalBoardingPasses).toEqual(true);
-    });
-  });
 });
diff --git a/app/MMB/src/scenes/tickets/boardingPassesPdf/__tests__/BoardingPassPdf.test.js b/app/MMB/src/scenes/tickets/boardingPassesPdf/__tests__/BoardingPassPdf.test.js
index f96785d5..43dcf062 100644
--- a/app/MMB/src/scenes/tickets/boardingPassesPdf/__tests__/BoardingPassPdf.test.js
+++ b/app/MMB/src/scenes/tickets/boardingPassesPdf/__tests__/BoardingPassPdf.test.js
@@ -5,7 +5,7 @@ import ShallowRenderer from 'react-test-renderer/shallow';
 
 const renderer = new ShallowRenderer();
 
-import { getPdfFilenameFromUrl, BoardingPassPdf } from '../BoardingPassPdf';
+import { BoardingPassPdf } from '../BoardingPassPdf';
 
 jest.mock('react-native-fetch-blob', () => {
   return {
@@ -24,43 +24,71 @@ jest.mock('react-native-fetch-blob', () => {
   };
 });
 
-const bookingId = 123456;
+const getWrapper = (
+  bookingId: number = 123456,
+  BoardingPassUrl: string = 'https://somedomain.com/folder/subfolder/file.pdf',
+  flightNumber: string = '123_321',
+) =>
+  renderer.render(
+    <BoardingPassPdf
+      bookingId={bookingId}
+      boardingPassUrl={BoardingPassUrl}
+      flightNumber={flightNumber}
+    />,
+  );
 
 describe('BoardingPassPdf', () => {
   it('renders', () => {
-    const wrapper = renderer.render(
-      <BoardingPassPdf
-        bookingId={bookingId}
-        boardingPassUrl="https://somedomain.com/folder/subfolder/file.pdf"
-      />,
-    );
+    const wrapper = getWrapper();
 
-    expect(wrapper.props.fileName).toBe(
-      'boardingPasses/file-bookingId:123456.pdf',
-    );
+    expect(wrapper).toMatchInlineSnapshot(`
+<PdfViewAndStore
+  fileName="boardingPasses/123456/123_321.pdf"
+  overwriteExisting={false}
+  url="https://somedomain.com/folder/subfolder/file.pdf"
+/>
+`);
   });
-});
-
-describe('getPdfFilenameFromUrl', () => {
-  it('should return correct filename given the URL of a pdf', () => {
-    const url =
-      'https://somedomainname.com/numbers1234/Vienna-Palma%2CMajorca_1234567890_5bafa3bd88fbf8e8c90a0a.pdf?v=12345676';
 
-    expect(getPdfFilenameFromUrl(url, bookingId)).toBe(
-      'Vienna-Palma%2CMajorca_1234567890_5bafa3bd88fbf8e8c90a0a-bookingId:123456',
-    );
-    const pdf = 'somePdf.pdf';
-    expect(getPdfFilenameFromUrl(pdf, bookingId)).toBe(
-      'somePdf-bookingId:123456',
-    );
+  it('show error if bookingId is missing', () => {
+    // $FlowExpectedError: Intentionally testing what happens with null value
+    const wrapper = getWrapper(null);
+    expect(wrapper).toMatchInlineSnapshot(`
+<GeneralError
+  errorMessage={
+    <Translation
+      id="mmb.boarding_passes.not_available"
+    />
+  }
+/>
+`);
   });
 
-  it('should return null if given a URL not containing .pdf', () => {
-    const url =
-      'https://somedomainname.com/numbers1234/Vienna-Palma%2CMajorca_1234567890_5bafa3bd88fbf8e8c90a0a.doc?v=12345676';
+  it('show error if boardingPassUrl is missing', () => {
+    // $FlowExpectedError: Intentionally testing what happens with null value
+    const wrapper = getWrapper(123, null);
+    expect(wrapper).toMatchInlineSnapshot(`
+<GeneralError
+  errorMessage={
+    <Translation
+      id="mmb.boarding_passes.not_available"
+    />
+  }
+/>
+`);
+  });
 
-    expect(getPdfFilenameFromUrl(url, bookingId)).toBe(null);
-    const pdf = 'someNonPdf.xls';
-    expect(getPdfFilenameFromUrl(pdf, bookingId)).toBe(null);
+  it('show error if flightNumber is missing', () => {
+    // $FlowExpectedError: Intentionally testing what happens with null value
+    const wrapper = getWrapper(123, 'test', null);
+    expect(wrapper).toMatchInlineSnapshot(`
+<GeneralError
+  errorMessage={
+    <Translation
+      id="mmb.boarding_passes.not_available"
+    />
+  }
+/>
+`);
   });
 });
diff --git a/packages/shared/src/forms/__tests__/__snapshots__/TextInput.test.js.snap b/packages/shared/src/forms/__tests__/__snapshots__/TextInput.test.js.snap
index 73f03336..d6265908 100644
--- a/packages/shared/src/forms/__tests__/__snapshots__/TextInput.test.js.snap
+++ b/packages/shared/src/forms/__tests__/__snapshots__/TextInput.test.js.snap
@@ -4,10 +4,13 @@ exports[`renders 1`] = `
 <React.Fragment>
   <Component
     style={
-      Object {
-        "flexDirection": "row",
-        "height": 47,
-      }
+      Array [
+        Object {
+          "flexDirection": "row",
+          "height": 47,
+        },
+        undefined,
+      ]
     }
   >
     <TextInput
@@ -39,10 +42,13 @@ exports[`renders with default value 1`] = `
 <React.Fragment>
   <Component
     style={
-      Object {
-        "flexDirection": "row",
-        "height": 47,
-      }
+      Array [
+        Object {
+          "flexDirection": "row",
+          "height": 47,
+        },
+        undefined,
+      ]
     }
   >
     <TextInput
@@ -89,10 +95,13 @@ exports[`renders with label 1`] = `
   </Text>
   <Component
     style={
-      Object {
-        "flexDirection": "row",
-        "height": 47,
-      }
+      Array [
+        Object {
+          "flexDirection": "row",
+          "height": 47,
+        },
+        undefined,
+      ]
     }
   >
     <TextInput
@@ -130,10 +139,13 @@ exports[`renders with placeholder 1`] = `
 <React.Fragment>
   <Component
     style={
-      Object {
-        "flexDirection": "row",
-        "height": 47,
-      }
+      Array [
+        Object {
+          "flexDirection": "row",
+          "height": 47,
+        },
+        undefined,
+      ]
     }
   >
     <TextInput