Repository: kiwicom/mobile
Pull request #1335 (2018-11-15 08:15:40)

Use with context in search results context


Patch:
diff --git a/app/hotels/src/allHotels/RenderSearchResults.js b/app/hotels/src/allHotels/RenderSearchResults.js
index cef923d6..beda37e7 100644
--- a/app/hotels/src/allHotels/RenderSearchResults.js
+++ b/app/hotels/src/allHotels/RenderSearchResults.js
@@ -8,13 +8,19 @@ import { StyleSheet, Device } from '@kiwicom/mobile-shared';
 import MapScreen from '../map/allHotels/MapScreen';
 import AllHotelsSearchList from './AllHotelsSearchList';
 import type { RenderSearchResults as RenderResultsType } from './__generated__/RenderSearchResults.graphql';
-import SearchResultsContext, {
+import {
+  withSearchResultsContext,
+  type SearchResultState,
   type ResultType,
 } from '../navigation/allHotels/SearchResultsContext';
 import LoadMoreButton from './LoadMoreButton';
 
-type PropsWithContext = {|
-  ...Props,
+type Props = {|
+  +data: RenderResultsType,
+  +hasMore: boolean,
+  +isLoading: boolean,
+  +onLoadMore: () => void,
+  +top: number,
   +show: ResultType,
 |};
 
@@ -22,11 +28,11 @@ export const topValue = 1000;
 export const lowValue = -100;
 const transitionDuration = 250;
 
-export class RenderSearchResults extends React.Component<PropsWithContext> {
+export class RenderSearchResults extends React.Component<Props> {
   mapAnimation: Animated.Value;
   listAnimation: Animated.Value;
 
-  constructor(props: PropsWithContext) {
+  constructor(props: Props) {
     super(props);
 
     const showList = props.show === 'list';
@@ -34,14 +40,14 @@ export class RenderSearchResults extends React.Component<PropsWithContext> {
     this.listAnimation = new Animated.Value(showList ? 0 : lowValue);
   }
 
-  componentDidUpdate = (prevProps: PropsWithContext) => {
+  componentDidUpdate(prevProps: Props) {
     if (prevProps.show === 'list' && this.props.show === 'map') {
       this.animateToMap();
     }
     if (prevProps.show === 'map' && this.props.show === 'list') {
       this.animateToList();
     }
-  };
+  }
 
   animateToMap = () => {
     Animated.parallel([
@@ -110,36 +116,16 @@ export class RenderSearchResults extends React.Component<PropsWithContext> {
   }
 }
 
-type Props = {|
-  +data: RenderResultsType,
-  +hasMore: boolean,
-  +isLoading: boolean,
-  +onLoadMore: () => void,
-  +top: number,
-|};
-
-class RenderSearchResultsWithContext extends React.Component<Props> {
-  renderInner = ({ show }) => (
-    <RenderSearchResults {...this.props} show={show} />
-  );
-
-  render() {
-    return (
-      <SearchResultsContext.Consumer>
-        {this.renderInner}
-      </SearchResultsContext.Consumer>
-    );
-  }
-}
-
 const styles = StyleSheet.create({
   content: {
     paddingBottom: Device.isIPhoneX ? 80 : 44,
   },
 });
 
+const select = ({ show }: SearchResultState) => ({ show });
+
 export default createFragmentContainer(
-  RenderSearchResultsWithContext,
+  withSearchResultsContext(select)(RenderSearchResults),
   graphql`
     fragment RenderSearchResults on AllHotelsInterface @relay(plural: true) {
       ...AllHotelsSearchList
diff --git a/app/hotels/src/navigation/HotelsNavigationStack.js b/app/hotels/src/navigation/HotelsNavigationStack.js
index 9ee039a0..c00cbb8d 100644
--- a/app/hotels/src/navigation/HotelsNavigationStack.js
+++ b/app/hotels/src/navigation/HotelsNavigationStack.js
@@ -13,7 +13,7 @@ import SingleHotel from './singleHotel/SingleHotelNavigationScreen';
 import SearchResults from './allHotels/SearchResultsScreen';
 import AdditionalPropsInjector from './singleHotel/AdditionalPropsInjector';
 import SingleHotelMapNavigationScreen from './singleHotel/SingleHotelMapNavigationScreen';
-import Payment from '../singleHotel/PaymentScreen';
+import Payment from '../singleHotel/paymentScreen/PaymentScreen';
 import GalleryGrid from '../gallery/GalleryGrid';
 
 export type NavigationProps = {|
diff --git a/app/hotels/src/navigation/HotelsTabletStack.js b/app/hotels/src/navigation/HotelsTabletStack.js
index a6ed5700..15776ea1 100644
--- a/app/hotels/src/navigation/HotelsTabletStack.js
+++ b/app/hotels/src/navigation/HotelsTabletStack.js
@@ -12,7 +12,7 @@ import { withMappedNavigationAndConfigProps as withMappedProps } from 'react-nav
 import SearchResults from './allHotels/SearchResultsScreen';
 import AdditionalPropsInjector from './singleHotel/AdditionalPropsInjector';
 import SingleHotelMapNavigationScreen from './singleHotel/SingleHotelMapNavigationScreen';
-import Payment from '../singleHotel/PaymentScreen';
+import Payment from '../singleHotel/paymentScreen/PaymentScreen';
 import GalleryGrid from '../gallery/GalleryGrid';
 
 export type NavigationProps = {|
diff --git a/app/hotels/src/navigation/allHotels/SearchResultsContext.js b/app/hotels/src/navigation/allHotels/SearchResultsContext.js
index 108476d9..ec060d6f 100644
--- a/app/hotels/src/navigation/allHotels/SearchResultsContext.js
+++ b/app/hotels/src/navigation/allHotels/SearchResultsContext.js
@@ -1,6 +1,7 @@
 // @flow
 
 import * as React from 'react';
+import { withContext } from '@kiwicom/mobile-shared';
 
 export type ResultType = 'list' | 'map';
 
@@ -48,21 +49,7 @@ class Provider extends React.Component<Props, State> {
 export default { Consumer, Provider };
 
 export function withSearchResultsContext(select: (state: State) => Object) {
-  return function(Component: React.ElementType) {
-    class WithSearchResultsContext extends React.Component<Props> {
-      // $FlowExpectedError: We need to pass on the navigationOptions if any, flow does not know about it, but a react component might have it
-      static navigationOptions = Component.navigationOptions;
-
-      mapStateToProps = (state: Object) => {
-        const stateProps = select(state);
-        return <Component {...this.props} {...stateProps} />;
-      };
-
-      render() {
-        return <Consumer>{this.mapStateToProps}</Consumer>;
-      }
-    }
-
-    return WithSearchResultsContext;
-  };
+  return withContext<State>(select, Consumer);
 }
+
+export type SearchResultState = State;
diff --git a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
index e437ab9e..83e0e13f 100644
--- a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
+++ b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
@@ -20,6 +20,7 @@ import HotelsNavigationOptions from '../HotelsNavigationOptions';
 import {
   withSearchResultsContext,
   type ResultType,
+  type SearchResultState,
 } from './SearchResultsContext';
 import SingleHotelContainer from '../../singleHotel/SingleHotelContainer';
 import type { RoomsConfiguration } from '../../singleHotel/AvailableHotelSearchInput';
@@ -192,7 +193,7 @@ const styles = StyleSheet.create({
   },
 });
 
-export default withSearchResultsContext(state => ({
+export default withSearchResultsContext((state: SearchResultState) => ({
   setResultType: state.setResultType,
   show: state.show,
 }))(SearchResultsScreen);
diff --git a/app/hotels/src/navigation/singleHotel/SingleHotelStack.js b/app/hotels/src/navigation/singleHotel/SingleHotelStack.js
index cbe16f7e..19eb13b0 100644
--- a/app/hotels/src/navigation/singleHotel/SingleHotelStack.js
+++ b/app/hotels/src/navigation/singleHotel/SingleHotelStack.js
@@ -11,7 +11,7 @@ import {
 import SingleHotelNavigationScreen from './SingleHotelNavigationScreen';
 import SingleHotelMapNavigationScreen from './SingleHotelMapNavigationScreen';
 import GalleryGrid from '../../gallery/GalleryGrid';
-import PaymentScreen from '../../singleHotel/PaymentScreen';
+import PaymentScreen from '../../singleHotel/paymentScreen/PaymentScreen';
 import AdditionalPropsInjector from './AdditionalPropsInjector';
 
 const DetailStack = StackNavigator(
diff --git a/app/hotels/src/singleHotel/PaymentScreen.js b/app/hotels/src/singleHotel/PaymentScreen.js
deleted file mode 100644
index 99887cac..00000000
--- a/app/hotels/src/singleHotel/PaymentScreen.js
+++ /dev/null
@@ -1,152 +0,0 @@
-// @flow
-
-import * as React from 'react';
-import { Platform, StatusBar, View } from 'react-native';
-import {
-  WebView,
-  Logger,
-  type WebViewStateChangeEvent,
-  GeneralError,
-} from '@kiwicom/mobile-shared';
-import { DateUtils, Translation } from '@kiwicom/mobile-localization';
-import querystring from 'querystring';
-import { HeaderTitle, type NavigationType } from '@kiwicom/mobile-navigation';
-import { graphql, PublicApiRenderer } from '@kiwicom/mobile-relay';
-import { HeaderBackButton } from 'react-navigation';
-import { defaultTokens } from '@kiwicom/mobile-orbit';
-
-import { sanitizeDate } from '../GraphQLSanitizers';
-import { withHotelsContext } from '../HotelsContext';
-import type { PaymentScreenQueryResponse } from './__generated__/PaymentScreenQuery.graphql';
-
-export type PaymentParameters = {|
-  +hotelId: string,
-  +checkin: Date,
-  +checkout: Date,
-  +rooms: Array<{|
-    +id: string,
-    +count: number, // how many rooms with this ID?
-  |}>,
-  +language: string,
-  +currency: string,
-  +version: string,
-|};
-
-type RoomConfig = {|
-  +roomId: string,
-  +count: number,
-|};
-
-export class PaymentScreen extends React.Component<PaymentParameters> {
-  static navigationOptions = ({
-    navigation,
-  }: {
-    navigation: NavigationType,
-  }) => {
-    function goBack() {
-      navigation.goBack(null);
-    }
-    return {
-      headerTitle: (
-        <HeaderTitle>
-          <Translation id="hotels.navigation.title.payment" />
-        </HeaderTitle>
-      ),
-      headerLeft: (
-        <HeaderBackButton
-          onPress={goBack}
-          tintColor={defaultTokens.paletteProductNormal}
-        />
-      ),
-    };
-  };
-
-  componentDidMount() {
-    Logger.ancillaryDisplayed(
-      Logger.Type.ANCILLARY_STEP_PAYMENT,
-      Logger.Provider.ANCILLARY_PROVIDER_BOOKINGCOM,
-    );
-  }
-
-  onNavigationStateChange = (event: WebViewStateChangeEvent) => {
-    if (!event.loading && event.url.includes('booking.com/confirmation')) {
-      Logger.ancillaryPurchased(
-        Logger.Type.ANCILLARY_STEP_PAYMENT,
-        Logger.Provider.ANCILLARY_PROVIDER_BOOKINGCOM,
-      );
-    }
-  };
-
-  getRoomConfig = () =>
-    this.props.rooms.map<RoomConfig>(room => ({
-      roomId: room.id,
-      count: room.count,
-    }));
-
-  renderInner = (props: PaymentScreenQueryResponse) => {
-    const url = props.hotelPaymentUrls?.bookingComPaymentUrl;
-    if (url == null) {
-      return (
-        <GeneralError
-          errorMessage={<Translation id="hotels.payment_screen.server_error" />}
-        />
-      );
-    }
-    return (
-      <React.Fragment>
-        <View testID="paymentScreenSingleHotel" />
-        <WebView
-          source={{
-            uri: createURI(this.props, url),
-          }}
-          onNavigationStateChange={this.onNavigationStateChange}
-        />
-      </React.Fragment>
-    );
-  };
-
-  render() {
-    return (
-      <React.Fragment>
-        <StatusBar barStyle="dark-content" />
-        <PublicApiRenderer
-          render={this.renderInner}
-          query={graphql`
-            query PaymentScreenQuery(
-              $hotelId: ID
-              $roomConfig: [RoomConfigInput]
-            ) {
-              hotelPaymentUrls(hotelId: $hotelId, roomConfig: $roomConfig) {
-                bookingComPaymentUrl
-              }
-            }
-          `}
-          variables={{
-            hotelId: this.props.hotelId,
-            roomConfig: this.getRoomConfig(),
-          }}
-        />
-      </React.Fragment>
-    );
-  }
-}
-
-export default withHotelsContext(state => ({
-  checkin: state.checkin,
-  checkout: state.checkout,
-  currency: state.currency,
-  version: state.version,
-}))(PaymentScreen);
-
-export function createURI(pp: PaymentParameters, url: string): string {
-  const checkinQuery = sanitizeDate(pp.checkin);
-  const intervalQuery = DateUtils.diffInDays(pp.checkout, pp.checkin);
-
-  return `${url}&${querystring.stringify({
-    checkin: checkinQuery,
-    interval: intervalQuery,
-    label: `kiwi-${Platform.OS}-react-${pp.version}`,
-    lang: pp.language,
-    selected_currency: pp.currency,
-  })}`;
-}
diff --git a/app/hotels/src/singleHotel/paymentScreen/BookingPaymentScreen.js b/app/hotels/src/singleHotel/paymentScreen/BookingPaymentScreen.js
new file mode 100644
index 00000000..a290f29a
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/BookingPaymentScreen.js
@@ -0,0 +1,93 @@
+// @flow strict
+
+import * as React from 'react';
+import { Platform, StatusBar } from 'react-native';
+import { Logger } from '@kiwicom/mobile-shared';
+import { DateUtils, DeviceInfo } from '@kiwicom/mobile-localization';
+import querystring from 'querystring';
+import { graphql, PublicApiRenderer } from '@kiwicom/mobile-relay';
+
+import { sanitizeDate } from '../../GraphQLSanitizers';
+import { withHotelsContext } from '../../HotelsContext';
+import type { BookingPaymentScreenQueryResponse } from './__generated__/BookingPaymentScreenQuery.graphql';
+import PaymentWebView from './PaymentWebView';
+
+type Props = {|
+  +hotelId: string,
+  +checkin: Date,
+  +checkout: Date,
+  +currency: string,
+  +version: string,
+  +roomConfig: $ReadOnlyArray<{|
+    +roomId: string,
+    +count: number,
+  |}>,
+|};
+
+export class BookingPaymentScreen extends React.Component<Props> {
+  componentDidMount() {
+    Logger.ancillaryDisplayed(
+      Logger.Type.ANCILLARY_STEP_PAYMENT,
+      Logger.Provider.ANCILLARY_PROVIDER_BOOKINGCOM,
+    );
+  }
+
+  createURI = (url: ?string): ?string => {
+    if (url == null) {
+      return null;
+    }
+
+    const checkinQuery = sanitizeDate(this.props.checkin);
+    const intervalQuery = DateUtils.diffInDays(
+      this.props.checkout,
+      this.props.checkin,
+    );
+
+    return `${url}&${querystring.stringify({
+      checkin: checkinQuery,
+      interval: intervalQuery,
+      label: `kiwi-${Platform.OS}-react-${this.props.version}`,
+      lang: DeviceInfo.getLanguage(),
+      selected_currency: this.props.currency,
+    })}`;
+  };
+
+  renderInner = (props: BookingPaymentScreenQueryResponse) => (
+    <PaymentWebView
+      url={this.createURI(props.hotelPaymentUrls?.bookingComPaymentUrl)}
+    />
+  );
+
+  render() {
+    return (
+      <React.Fragment>
+        <StatusBar barStyle="dark-content" />
+        <PublicApiRenderer
+          render={this.renderInner}
+          query={graphql`
+            query BookingPaymentScreenQuery(
+              $hotelId: ID
+              $roomConfig: [RoomConfigInput]
+            ) {
+              hotelPaymentUrls(hotelId: $hotelId, roomConfig: $roomConfig) {
+                bookingComPaymentUrl
+              }
+            }
+          `}
+          variables={{
+            hotelId: this.props.hotelId,
+            roomConfig: this.props.roomConfig,
+          }}
+        />
+      </React.Fragment>
+    );
+  }
+}
+
+export default withHotelsContext(state => ({
+  checkin: state.checkin,
+  checkout: state.checkout,
+  currency: state.currency,
+  version: state.version,
+  hotelId: state.hotelId,
+}))(BookingPaymentScreen);
diff --git a/app/hotels/src/singleHotel/paymentScreen/PaymentScreen.js b/app/hotels/src/singleHotel/paymentScreen/PaymentScreen.js
new file mode 100644
index 00000000..e3c8f85c
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/PaymentScreen.js
@@ -0,0 +1,71 @@
+// @flow
+
+import * as React from 'react';
+import { Translation } from '@kiwicom/mobile-localization';
+import { HeaderTitle, type NavigationType } from '@kiwicom/mobile-navigation';
+import { HeaderBackButton } from 'react-navigation';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
+
+import {
+  withHotelsContext,
+  type HotelsContextState,
+  type ApiProvider,
+} from '../../HotelsContext';
+import BookingPaymentScreen from './BookingPaymentScreen';
+import Stay22PaymentScreen from './Stay22PaymentScreen';
+
+export type Props = {|
+  +apiProvider: ApiProvider,
+  +rooms: Array<{|
+    +id: string,
+    +count: number, // how many rooms with this ID?
+  |}>,
+|};
+
+type RoomConfig = {|
+  +roomId: string,
+  +count: number,
+|};
+
+export class PaymentScreen extends React.Component<Props> {
+  static navigationOptions = ({
+    navigation,
+  }: {
+    navigation: NavigationType,
+  }) => {
+    function goBack() {
+      navigation.goBack(null);
+    }
+    return {
+      headerTitle: (
+        <HeaderTitle>
+          <Translation id="hotels.navigation.title.payment" />
+        </HeaderTitle>
+      ),
+      headerLeft: (
+        <HeaderBackButton
+          onPress={goBack}
+          tintColor={defaultTokens.paletteProductNormal}
+        />
+      ),
+    };
+  };
+
+  getRoomConfig = () =>
+    this.props.rooms.map<RoomConfig>(room => ({
+      roomId: room.id,
+      count: room.count,
+    }));
+
+  render() {
+    const roomConfig = this.getRoomConfig();
+    if (this.props.apiProvider === 'booking') {
+      return <BookingPaymentScreen roomConfig={roomConfig} />;
+    }
+    return <Stay22PaymentScreen roomConfig={roomConfig} />;
+  }
+}
+
+export default withHotelsContext((state: HotelsContextState) => ({
+  apiProvider: state.apiProvider,
+}))(PaymentScreen);
diff --git a/app/hotels/src/singleHotel/paymentScreen/PaymentWebView.js b/app/hotels/src/singleHotel/paymentScreen/PaymentWebView.js
new file mode 100644
index 00000000..605ec0a5
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/PaymentWebView.js
@@ -0,0 +1,69 @@
+// @flow strict
+
+import * as React from 'react';
+import { View } from 'react-native';
+import {
+  WebView,
+  GeneralError,
+  type WebViewStateChangeEvent,
+  Logger,
+} from '@kiwicom/mobile-shared';
+import { Translation } from '@kiwicom/mobile-localization';
+
+import {
+  withHotelsContext,
+  type HotelsContextState,
+  type ApiProvider,
+} from '../../HotelsContext';
+
+type Props = {|
+  +url: ?string,
+  +apiProvider: ApiProvider,
+|};
+
+export class PaymentWebView extends React.Component<Props> {
+  onNavigationStateChange = (event: WebViewStateChangeEvent) => {
+    /**
+     * Still we are only being redirected to booking.com for payment.
+     * If that will change in the future, we might pass this url as a prop from parent component
+     */
+    if (!event.loading && event.url.includes('booking.com/confirmation')) {
+      const {
+        ANCILLARY_PROVIDER_BOOKINGCOM,
+        ANCILLARY_PROVIDER_STAY22,
+      } = Logger.Provider;
+
+      Logger.ancillaryPurchased(
+        Logger.Type.ANCILLARY_STEP_PAYMENT,
+        this.props.apiProvider === 'booking'
+          ? ANCILLARY_PROVIDER_BOOKINGCOM
+          : ANCILLARY_PROVIDER_STAY22,
+      );
+    }
+  };
+
+  render() {
+    if (this.props.url == null) {
+      return (
+        <GeneralError
+          errorMessage={<Translation id="hotels.payment_screen.server_error" />}
+        />
+      );
+    }
+    return (
+      <React.Fragment>
+        <View testID="paymentScreenSingleHotel" />
+        <WebView
+          source={{
+            uri: this.props.url,
+          }}
+          onNavigationStateChange={this.onNavigationStateChange}
+        />
+      </React.Fragment>
+    );
+  }
+}
+
+const select = ({ apiProvider }: HotelsContextState) => ({ apiProvider });
+
+export default withHotelsContext(select)(PaymentWebView);
diff --git a/app/hotels/src/singleHotel/paymentScreen/Stay22PaymentScreen.js b/app/hotels/src/singleHotel/paymentScreen/Stay22PaymentScreen.js
new file mode 100644
index 00000000..175cd094
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/Stay22PaymentScreen.js
@@ -0,0 +1,55 @@
+// @flow strict
+
+import * as React from 'react';
+import { Logger } from '@kiwicom/mobile-shared';
+import { graphql, PublicApiRenderer } from '@kiwicom/mobile-relay';
+
+import { withHotelsContext } from '../../HotelsContext';
+import PaymentWebView from './PaymentWebView';
+import type { Stay22PaymentScreenQueryResponse } from './__generated__/Stay22PaymentScreenQuery.graphql';
+
+type Props = {|
+  +paymentLink: ?string,
+  +roomConfig: $ReadOnlyArray<{|
+    +roomId: string,
+    +count: number,
+  |}>,
+|};
+
+class Stay22PaymentScreen extends React.Component<Props> {
+  componentDidMount() {
+    Logger.ancillaryDisplayed(
+      Logger.Type.ANCILLARY_STEP_PAYMENT,
+      Logger.Provider.ANCILLARY_PROVIDER_STAY22,
+    );
+  }
+
+  renderInner = (props: Stay22PaymentScreenQueryResponse) => (
+    <PaymentWebView url={props.hotelPaymentUrls?.stay22PaymentUrl} />
+  );
+
+  render() {
+    return (
+      <PublicApiRenderer
+        render={this.renderInner}
+        query={graphql`
+          query Stay22PaymentScreenQuery(
+            $paymentLink: String!
+            $roomConfig: [RoomConfigInput]
+          ) {
+            hotelPaymentUrls(roomConfig: $roomConfig) {
+              stay22PaymentUrl(paymentLink: $paymentLink)
+            }
+          }
+        `}
+        variables={{
+          paymentLink: this.props.paymentLink,
+          roomConfig: this.props.roomConfig,
+        }}
+      />
+    );
+  }
+}
+export default withHotelsContext(state => ({
+  paymentLink: state.paymentLink,
+}))(Stay22PaymentScreen);
diff --git a/app/hotels/src/singleHotel/__generated__/PaymentScreenQuery.graphql.js b/app/hotels/src/singleHotel/paymentScreen/__generated__/BookingPaymentScreenQuery.graphql.js
similarity index 72%
rename from app/hotels/src/singleHotel/__generated__/PaymentScreenQuery.graphql.js
rename to app/hotels/src/singleHotel/paymentScreen/__generated__/BookingPaymentScreenQuery.graphql.js
index 52673059..0de3d74c 100644
--- a/app/hotels/src/singleHotel/__generated__/PaymentScreenQuery.graphql.js
+++ b/app/hotels/src/singleHotel/paymentScreen/__generated__/BookingPaymentScreenQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 28a12649218f3e7a941c619921aa507b
+ * @relayHash 5d92fb044f70f4e64e8d7f64d9402ada
  */
 
 /* eslint-disable */
@@ -13,24 +13,24 @@ export type RoomConfigInput = {
   roomId: string,
   count: number,
 };
-export type PaymentScreenQueryVariables = {|
+export type BookingPaymentScreenQueryVariables = {|
   hotelId?: ?string,
   roomConfig?: ?$ReadOnlyArray<?RoomConfigInput>,
 |};
-export type PaymentScreenQueryResponse = {|
+export type BookingPaymentScreenQueryResponse = {|
   +hotelPaymentUrls: ?{|
     +bookingComPaymentUrl: ?string
   |}
 |};
-export type PaymentScreenQuery = {|
-  variables: PaymentScreenQueryVariables,
-  response: PaymentScreenQueryResponse,
+export type BookingPaymentScreenQuery = {|
+  variables: BookingPaymentScreenQueryVariables,
+  response: BookingPaymentScreenQueryResponse,
 |};
 */
 
 
 /*
-query PaymentScreenQuery(
+query BookingPaymentScreenQuery(
   $hotelId: ID
   $roomConfig: [RoomConfigInput]
 ) {
@@ -91,13 +91,13 @@ v1 = [
 return {
   "kind": "Request",
   "operationKind": "query",
-  "name": "PaymentScreenQuery",
+  "name": "BookingPaymentScreenQuery",
   "id": null,
-  "text": "query PaymentScreenQuery(\n  $hotelId: ID\n  $roomConfig: [RoomConfigInput]\n) {\n  hotelPaymentUrls(hotelId: $hotelId, roomConfig: $roomConfig) {\n    bookingComPaymentUrl\n  }\n}\n",
+  "text": "query BookingPaymentScreenQuery(\n  $hotelId: ID\n  $roomConfig: [RoomConfigInput]\n) {\n  hotelPaymentUrls(hotelId: $hotelId, roomConfig: $roomConfig) {\n    bookingComPaymentUrl\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
-    "name": "PaymentScreenQuery",
+    "name": "BookingPaymentScreenQuery",
     "type": "RootQuery",
     "metadata": null,
     "argumentDefinitions": v0,
@@ -105,12 +105,12 @@ return {
   },
   "operation": {
     "kind": "Operation",
-    "name": "PaymentScreenQuery",
+    "name": "BookingPaymentScreenQuery",
     "argumentDefinitions": v0,
     "selections": v1
   }
 };
 })();
 // prettier-ignore
-(node/*: any*/).hash = '4c45a0c05b4bc8d0dcc000450b0382a1';
+(node/*: any*/).hash = '2237324bd3f3dab3ca90d1a90e28e5d1';
 module.exports = node;
diff --git a/app/hotels/src/singleHotel/paymentScreen/__generated__/Stay22PaymentScreenQuery.graphql.js b/app/hotels/src/singleHotel/paymentScreen/__generated__/Stay22PaymentScreenQuery.graphql.js
new file mode 100644
index 00000000..95c30c7e
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/__generated__/Stay22PaymentScreenQuery.graphql.js
@@ -0,0 +1,117 @@
+/**
+ * @flow
+ * @relayHash 12cdddc336c8166c26e51d5e7296a907
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ConcreteRequest } from 'relay-runtime';
+export type RoomConfigInput = {
+  roomId: string,
+  count: number,
+};
+export type Stay22PaymentScreenQueryVariables = {|
+  paymentLink: string,
+  roomConfig?: ?$ReadOnlyArray<?RoomConfigInput>,
+|};
+export type Stay22PaymentScreenQueryResponse = {|
+  +hotelPaymentUrls: ?{|
+    +stay22PaymentUrl: ?string
+  |}
+|};
+export type Stay22PaymentScreenQuery = {|
+  variables: Stay22PaymentScreenQueryVariables,
+  response: Stay22PaymentScreenQueryResponse,
+|};
+*/
+
+
+/*
+query Stay22PaymentScreenQuery(
+  $paymentLink: String!
+  $roomConfig: [RoomConfigInput]
+) {
+  hotelPaymentUrls(roomConfig: $roomConfig) {
+    stay22PaymentUrl(paymentLink: $paymentLink)
+  }
+}
+*/
+
+const node/*: ConcreteRequest*/ = (function(){
+var v0 = [
+  {
+    "kind": "LocalArgument",
+    "name": "paymentLink",
+    "type": "String!",
+    "defaultValue": null
+  },
+  {
+    "kind": "LocalArgument",
+    "name": "roomConfig",
+    "type": "[RoomConfigInput]",
+    "defaultValue": null
+  }
+],
+v1 = [
+  {
+    "kind": "LinkedField",
+    "alias": null,
+    "name": "hotelPaymentUrls",
+    "storageKey": null,
+    "args": [
+      {
+        "kind": "Variable",
+        "name": "roomConfig",
+        "variableName": "roomConfig",
+        "type": "[RoomConfigInput]"
+      }
+    ],
+    "concreteType": "HotelPaymentUrls",
+    "plural": false,
+    "selections": [
+      {
+        "kind": "ScalarField",
+        "alias": null,
+        "name": "stay22PaymentUrl",
+        "args": [
+          {
+            "kind": "Variable",
+            "name": "paymentLink",
+            "variableName": "paymentLink",
+            "type": "String!"
+          }
+        ],
+        "storageKey": null
+      }
+    ]
+  }
+];
+return {
+  "kind": "Request",
+  "operationKind": "query",
+  "name": "Stay22PaymentScreenQuery",
+  "id": null,
+  "text": "query Stay22PaymentScreenQuery(\n  $paymentLink: String!\n  $roomConfig: [RoomConfigInput]\n) {\n  hotelPaymentUrls(roomConfig: $roomConfig) {\n    stay22PaymentUrl(paymentLink: $paymentLink)\n  }\n}\n",
+  "metadata": {},
+  "fragment": {
+    "kind": "Fragment",
+    "name": "Stay22PaymentScreenQuery",
+    "type": "RootQuery",
+    "metadata": null,
+    "argumentDefinitions": v0,
+    "selections": v1
+  },
+  "operation": {
+    "kind": "Operation",
+    "name": "Stay22PaymentScreenQuery",
+    "argumentDefinitions": v0,
+    "selections": v1
+  }
+};
+})();
+// prettier-ignore
+(node/*: any*/).hash = '2e7ce872382d2baed38d7bb70ba521cb';
+module.exports = node;
diff --git a/app/hotels/src/singleHotel/roomList/BeddingInfo.js b/app/hotels/src/singleHotel/roomList/BeddingInfo.js
index fbcf4f9d..5bb3f965 100644
--- a/app/hotels/src/singleHotel/roomList/BeddingInfo.js
+++ b/app/hotels/src/singleHotel/roomList/BeddingInfo.js
@@ -19,16 +19,29 @@ type Props = {
 };
 
 export class BeddingInfo extends React.Component<Props> {
-  formatBeddingInfo = (): string => {
+  formatBeddingInfo = (): $ReadOnlyArray<React.Element<typeof Translation>> => {
     const { room } = this.props;
     const beddingOptions = room?.bedding ?? [];
-    return beddingOptions
-      .map(beddingOption => {
-        const type = beddingOption?.type;
-        const amount = beddingOption?.amount;
-        return [amount, type].filter(value => value != null).join(' ');
-      })
-      .join(' or '); // TODO: Translate
+    const beddingTranslation = [];
+    beddingOptions.forEach((beddingOption, index) => {
+      const type = beddingOption?.type;
+      const amount = beddingOption?.amount;
+      if (index > 0) {
+        beddingTranslation.push(
+          <Translation
+            key={`key-${index}`}
+            id="single_hotel.bedding_info.or"
+          />,
+        );
+      }
+      beddingTranslation.push(
+        <Translation
+          key={`${type || ''}-${amount || ''}`}
+          passThrough={[amount, type].filter(value => value != null).join(' ')}
+        />,
+      );
+    });
+    return beddingTranslation;
   };
 
   render() {
@@ -39,9 +52,7 @@ export class BeddingInfo extends React.Component<Props> {
       <View>
         <View style={styles.row}>
           <TextIcon code="&#xe085;" style={styles.icon} />
-          <Text style={styles.text}>
-            <Translation passThrough={` ${this.formatBeddingInfo()}`} />
-          </Text>
+          <Text style={styles.text}>{this.formatBeddingInfo()}</Text>
         </View>
         <View style={styles.row}>
           <TextIcon code="(" style={styles.icon} />
diff --git a/packages/localization/src/vocabularies/Hotels.js b/packages/localization/src/vocabularies/Hotels.js
index d1d4c003..0b5757fb 100644
--- a/packages/localization/src/vocabularies/Hotels.js
+++ b/packages/localization/src/vocabularies/Hotels.js
@@ -98,6 +98,7 @@ export default {
   'hotels_search.all_hotels_search_list.show_list': 'Show list',
 
   'single_hotel.bedding_info.guests': '__numberOfGuests__ Guests',
+  'single_hotel.bedding_info.or': ' or ',
   'single_hotel.book_now': 'Book Now',
   'single_hotel.book_now.description':
     '__1_personCount__ Guests · __2_numberOfRooms__ Rooms',
diff --git a/packages/shared/src/withContext/withContext.js b/packages/shared/src/withContext/withContext.js
index 558e86a8..867f7e64 100644
--- a/packages/shared/src/withContext/withContext.js
+++ b/packages/shared/src/withContext/withContext.js
@@ -12,9 +12,6 @@ export default function withContext<T>(
       ...T,
       +children: React.Element<any>,
     |}> {
-      // $FlowExpectedError: We need to pass on the navigationOptions if any, flow does not know about it, but a react component might have it
-      static navigationOptions = Component.navigationOptions;
-
       /**
        * All components that subscribes to context would re render even when props they are not subsrcibing
        * too changes. This shouldComponentUpdate should make the subscriber only re render when its
@@ -34,6 +31,8 @@ export default function withContext<T>(
     }
 
     class WithContext extends React.Component<Object> {
+      // $FlowExpectedError: We need to pass on the navigationOptions if any, flow does not know about it, but a react component might have it
+      static navigationOptions = Component.navigationOptions;
       renderInner = (state: T) => {
         const stateProps = select(state);
         return (
diff --git a/schema.graphql b/schema.graphql
index 95698578..ff489774 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1606,6 +1606,7 @@ interface HotelInterface {
 """The urls where the user will be redirected for payment"""
 type HotelPaymentUrls {
   bookingComPaymentUrl: String
+  stay22PaymentUrl(paymentLink: String!): String
 }
 
 type HotelPhoto {


Test patch:
diff --git a/app/hotels/src/singleHotel/__tests__/PaymentScreen.test.js b/app/hotels/src/singleHotel/__tests__/PaymentScreen.test.js
deleted file mode 100644
index 425da2a6..00000000
--- a/app/hotels/src/singleHotel/__tests__/PaymentScreen.test.js
+++ /dev/null
@@ -1,85 +0,0 @@
-// @flow
-
-import { NativeModules } from 'react-native';
-
-import { PaymentScreen, createURI } from '../PaymentScreen';
-
-const ONE_DAY = 8.64e7; // ms
-
-jest.mock('../../../package.json', () => ({
-  version: '1.0.0',
-}));
-
-it('creates correct URL', () => {
-  expect(
-    createURI(
-      {
-        hotelId: '1',
-        checkin: new Date(ONE_DAY),
-        checkout: new Date(3 * ONE_DAY), // 2 days later
-        rooms: [
-          {
-            id: '7709411_91461863_1_1_0',
-            count: 1,
-          },
-          {
-            id: '7709404_91461863_0_1_0',
-            count: 2,
-          },
-        ],
-        language: 'en',
-        currency: 'EUR',
-        version: 'rn-test',
-      },
-      'http://test.url.lol',
-    ),
-  ).toMatchSnapshot();
-
-  expect(
-    createURI(
-      {
-        hotelId: '2',
-        checkin: new Date(2 * ONE_DAY),
-        checkout: new Date(16 * ONE_DAY), // 14 days later
-        rooms: [
-          {
-            id: '7709411_91461863_1_1_0',
-            count: 5,
-          },
-        ],
-        language: 'cs',
-        currency: 'JPY',
-        version: 'rn-test',
-      },
-      'http://test.url.lol',
-    ),
-  ).toMatchSnapshot();
-});
-
-it('logs ancillaryPurchased event', () => {
-  jest.resetAllMocks();
-  // $FlowExpectedError: We don't need props for this test
-  const Component = new PaymentScreen({});
-  const loadingEvent = {
-    loading: true,
-    url: 'https://secure.booking.com/confirmation-en-gb.com',
-  };
-
-  const loadingDoneEvent = {
-    loading: false,
-    url: 'https://secure.booking.com/confirmation-en-gb.com',
-  };
-
-  const badUrlEvent = {
-    loading: false,
-    url: 'https://secure.booking.com/book.html',
-  };
-
-  Component.onNavigationStateChange(loadingDoneEvent);
-  Component.onNavigationStateChange(badUrlEvent);
-  Component.onNavigationStateChange(loadingEvent);
-
-  expect(
-    NativeModules.RNLoggingModule.ancillaryPurchased,
-  ).toHaveBeenCalledTimes(1);
-});
diff --git a/app/hotels/src/singleHotel/paymentScreen/__tests__/BookingPaymentScreen.test.js b/app/hotels/src/singleHotel/paymentScreen/__tests__/BookingPaymentScreen.test.js
new file mode 100644
index 00000000..8bfff8d9
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/__tests__/BookingPaymentScreen.test.js
@@ -0,0 +1,43 @@
+// @flow
+
+import { BookingPaymentScreen } from '../BookingPaymentScreen';
+
+const ONE_DAY = 8.64e7; // ms
+
+it('creates correct URL', () => {
+  expect(
+    new BookingPaymentScreen({
+      hotelId: '1',
+      checkin: new Date(ONE_DAY),
+      checkout: new Date(3 * ONE_DAY), // 2 days later
+      roomConfig: [
+        {
+          roomId: '7709411_91461863_1_1_0',
+          count: 1,
+        },
+        {
+          roomId: '7709404_91461863_0_1_0',
+          count: 2,
+        },
+      ],
+      currency: 'EUR',
+      version: 'rn-test',
+    }).createURI('http://test.url.lol'),
+  ).toMatchSnapshot();
+
+  expect(
+    new BookingPaymentScreen({
+      hotelId: '2',
+      checkin: new Date(2 * ONE_DAY),
+      checkout: new Date(16 * ONE_DAY), // 14 days later
+      roomConfig: [
+        {
+          roomId: '7709411_91461863_1_1_0',
+          count: 5,
+        },
+      ],
+      currency: 'JPY',
+      version: 'rn-test',
+    }).createURI('http://test.url.lol'),
+  ).toMatchSnapshot();
+});
diff --git a/app/hotels/src/singleHotel/paymentScreen/__tests__/PaymentWebView.test.js b/app/hotels/src/singleHotel/paymentScreen/__tests__/PaymentWebView.test.js
new file mode 100644
index 00000000..9b14b4ef
--- /dev/null
+++ b/app/hotels/src/singleHotel/paymentScreen/__tests__/PaymentWebView.test.js
@@ -0,0 +1,33 @@
+// @flow
+
+import { NativeModules } from 'react-native';
+
+import { PaymentWebView } from '../PaymentWebView';
+
+it('logs ancillaryPurchased event', () => {
+  jest.resetAllMocks();
+  // $FlowExpectedError: We don't need props for this test
+  const Component = new PaymentWebView({});
+  const loadingEvent = {
+    loading: true,
+    url: 'https://secure.booking.com/confirmation-en-gb.com',
+  };
+
+  const loadingDoneEvent = {
+    loading: false,
+    url: 'https://secure.booking.com/confirmation-en-gb.com',
+  };
+
+  const badUrlEvent = {
+    loading: false,
+    url: 'https://secure.booking.com/book.html',
+  };
+
+  Component.onNavigationStateChange(loadingDoneEvent);
+  Component.onNavigationStateChange(badUrlEvent);
+  Component.onNavigationStateChange(loadingEvent);
+
+  expect(
+    NativeModules.RNLoggingModule.ancillaryPurchased,
+  ).toHaveBeenCalledTimes(1);
+});
diff --git a/app/hotels/src/singleHotel/__tests__/__snapshots__/PaymentScreen.test.js.snap b/app/hotels/src/singleHotel/paymentScreen/__tests__/__snapshots__/BookingPaymentScreen.test.js.snap
similarity index 81%
rename from app/hotels/src/singleHotel/__tests__/__snapshots__/PaymentScreen.test.js.snap
rename to app/hotels/src/singleHotel/paymentScreen/__tests__/__snapshots__/BookingPaymentScreen.test.js.snap
index 5663c594..9cb9a170 100644
--- a/app/hotels/src/singleHotel/__tests__/__snapshots__/PaymentScreen.test.js.snap
+++ b/app/hotels/src/singleHotel/paymentScreen/__tests__/__snapshots__/BookingPaymentScreen.test.js.snap
@@ -2,4 +2,4 @@
 
 exports[`creates correct URL 1`] = `"http://test.url.lol&checkin=1970-01-02&interval=2&label=kiwi-ios-react-rn-test&lang=en&selected_currency=EUR"`;
 
-exports[`creates correct URL 2`] = `"http://test.url.lol&checkin=1970-01-03&interval=14&label=kiwi-ios-react-rn-test&lang=cs&selected_currency=JPY"`;
+exports[`creates correct URL 2`] = `"http://test.url.lol&checkin=1970-01-03&interval=14&label=kiwi-ios-react-rn-test&lang=en&selected_currency=JPY"`;
diff --git a/app/hotels/src/singleHotel/roomList/__tests__/__snapshots__/formatBeddingInfo.test.js.snap b/app/hotels/src/singleHotel/roomList/__tests__/__snapshots__/formatBeddingInfo.test.js.snap
index 53961866..8cac6b8e 100644
--- a/app/hotels/src/singleHotel/roomList/__tests__/__snapshots__/formatBeddingInfo.test.js.snap
+++ b/app/hotels/src/singleHotel/roomList/__tests__/__snapshots__/formatBeddingInfo.test.js.snap
@@ -1,9 +1,35 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`doesn't crash on empty input 1`] = `""`;
+exports[`doesn't crash on empty input 1`] = `Array []`;
 
-exports[`formats bedding information 1`] = `"1 Single Bed(s)"`;
+exports[`doesn't crash on empty input 2`] = `Array []`;
 
-exports[`pluralizes persons 1`] = `"2 Single Bed(s)"`;
+exports[`formats bedding information 1`] = `
+Array [
+  <Translation
+    passThrough="1 Single Bed(s)"
+  />,
+]
+`;
 
-exports[`provides all bedding options 1`] = `"2 Single Bed(s) or 1 Twin Bed(s)"`;
+exports[`pluralizes persons 1`] = `
+Array [
+  <Translation
+    passThrough="2 Single Bed(s)"
+  />,
+]
+`;
+
+exports[`provides all bedding options 1`] = `
+Array [
+  <Translation
+    passThrough="2 Single Bed(s)"
+  />,
+  <Translation
+    id="single_hotel.bedding_info.or"
+  />,
+  <Translation
+    passThrough="1 Twin Bed(s)"
+  />,
+]
+`;
diff --git a/app/hotels/src/singleHotel/roomList/__tests__/formatBeddingInfo.test.js b/app/hotels/src/singleHotel/roomList/__tests__/formatBeddingInfo.test.js
index d42ffe89..da76a02f 100644
--- a/app/hotels/src/singleHotel/roomList/__tests__/formatBeddingInfo.test.js
+++ b/app/hotels/src/singleHotel/roomList/__tests__/formatBeddingInfo.test.js
@@ -58,7 +58,7 @@ it('provides all bedding options', () => {
 });
 
 it("doesn't crash on empty input", () => {
-  expect(new BeddingInfo({ room: null }).formatBeddingInfo()).toEqual('');
+  expect(new BeddingInfo({ room: null }).formatBeddingInfo()).toMatchSnapshot();
   expect(
     // $FlowRelayIssue: https://github.com/facebook/relay/issues/2394
     new BeddingInfo({