Kaynağa Gözat

for testing

Viktoriia 3 hafta önce
ebeveyn
işleme
68c27d7430

+ 5 - 1
App.tsx

@@ -114,6 +114,8 @@ const linking = {
   }
 };
 
+import { IAPProvider } from 'src/contexts/IAPContext';
+
 const App = () => {
   return (
     <GestureHandlerRootView style={{ flex: 1 }}>
@@ -121,7 +123,9 @@ const App = () => {
         <SheetProvider>
           <QueryClientProvider client={queryClient}>
             <ErrorProvider>
-              <InnerApp />
+              <IAPProvider>
+                <InnerApp />
+              </IAPProvider>
             </ErrorProvider>
           </QueryClientProvider>
         </SheetProvider>

+ 12 - 0
Route.tsx

@@ -120,6 +120,8 @@ import AddRegionsNewScreen from 'src/screens/InAppScreens/TravelsScreen/AddRegio
 import RegionsVisitedScreen from 'src/screens/InAppScreens/TravelsScreen/RegionsVisitedScreen';
 import { clearLocalDatabaseOnLogout } from 'src/watermelondb/features/chat/data/chat.repo';
 import { stopAutoSyncListener } from 'src/watermelondb/features/chat/networkSync';
+import PremiumPaywallScreen from 'src/screens/PremiumPaywallScreen/PremiumPaywallScreen';
+import ManageSubscriptionScreen from 'src/screens/ManageSubscriptionScreen';
 
 enableScreens();
 
@@ -767,6 +769,16 @@ const Route = () => {
               </MapDrawer.Navigator>
             )}
           </ScreenStack.Screen>
+          <ScreenStack.Screen 
+            name={NAVIGATION_PAGES.PREMIUM_PAYWALL} 
+            component={PremiumPaywallScreen} 
+            // options={{ presentation: 'modal' }} 
+          />
+          <ScreenStack.Screen 
+            name={NAVIGATION_PAGES.MANAGE_SUBSCRIPTION} 
+            component={ManageSubscriptionScreen} 
+            options={{ presentation: 'modal' }} 
+          />
         </ScreenStack.Navigator>
       </NavigationProvider>
     </PushNotificationProvider>

+ 2 - 1
app.config.ts

@@ -210,6 +210,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         isAndroidBackgroundLocationEnabled: false
       }
     ],
-    'react-native-compressor'
+    'react-native-compressor',
+    'expo-iap'
   ]
 });

BIN
assets/premium/badge.png


BIN
assets/premium/data-export.png


BIN
assets/premium/discounts.png


BIN
assets/premium/location-filter.png


BIN
assets/premium/offline-maps.png


BIN
assets/premium/premium.png


BIN
assets/premium/series.png


BIN
assets/premium/trusted.png


+ 1 - 0
package.json

@@ -53,6 +53,7 @@
     "expo-dev-client": "~6.0.13",
     "expo-file-system": "~19.0.16",
     "expo-font": "~14.0.8",
+    "expo-iap": "^5.6.3",
     "expo-image": "~3.0.8",
     "expo-image-picker": "~17.0.8",
     "expo-location": "~19.0.7",

+ 10 - 0
src/components/MenuDrawer/index.tsx

@@ -1,6 +1,7 @@
 import React, { useState } from 'react';
 import { View, Image, Linking, Text, TouchableOpacity } from 'react-native';
 import { useNavigation } from '@react-navigation/native';
+import { useIAPContext } from 'src/contexts/IAPContext';
 
 import { MenuButton } from '../MenuButton';
 import { styles } from './styles';
@@ -23,6 +24,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
 export const MenuDrawer = (props: any) => {
   const token = storage.get('token', StoreType.STRING) as string;
   const navigation = useNavigation();
+  const { isPremium } = useIAPContext();
 
   return (
     <>
@@ -98,6 +100,14 @@ export const MenuDrawer = (props: any) => {
               })
             }
           />
+          {isPremium && (
+            <MenuButton
+              label="Manage subscription"
+              icon={<DocumentIcon fill={Colors.DARK_BLUE} width={20} height={20} />}
+              red={false}
+              buttonFn={() => navigation.navigate(NAVIGATION_PAGES.MANAGE_SUBSCRIPTION as never)}
+            />
+          )}
         </View>
 
         <View style={styles.bottomMenu}>

+ 185 - 0
src/contexts/IAPContext.tsx

@@ -0,0 +1,185 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { Platform } from 'react-native';
+import * as IAP from 'expo-iap';
+import { appApi, fetchPremiumStatus } from '@api/app';
+import { storage, StoreType } from 'src/storage';
+
+const ITEM_SKUS = Platform.select({
+  ios: ['nm_premium_monthly', 'nm_premium_annual'],
+  android: ['nm_premium_monthly', 'nm_premium_annual'],
+}) || [];
+
+interface IAPContextType {
+  isPremium: boolean;
+  subscriptions: any[];
+  purchaseSubscription: (sku: string) => Promise<boolean>;
+  restorePurchases: () => Promise<boolean>;
+  loading: boolean;
+}
+
+const IAPContext = createContext<IAPContextType>({
+  isPremium: false,
+  subscriptions: [],
+  purchaseSubscription: async () => false,
+  restorePurchases: async () => false,
+  loading: false,
+});
+
+export const useIAPContext = () => useContext(IAPContext);
+
+export const IAPProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [isPremium, setIsPremium] = useState(false);
+  const [subscriptions, setSubscriptions] = useState<any[]>([]);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    initIAP();
+
+    // Listeners for purchases
+    // let purchaseUpdateSubscription: any;
+    // let purchaseErrorSubscription: any;
+
+    return () => {
+      // Cleanup
+      // if (purchaseUpdateSubscription) purchaseUpdateSubscription.remove();
+      // if (purchaseErrorSubscription) purchaseErrorSubscription.remove();
+    };
+  }, []);
+
+  const initIAP = async () => {
+    try {
+      setLoading(true);
+      if (IAP.initConnection) {
+        console.log('-----')
+        await IAP.initConnection();
+      }
+
+      if (IAP.fetchProducts) {
+        try {
+          const subs = await IAP.fetchProducts({ skus: ITEM_SKUS, type: 'subs' });
+          console.log('Fetched subscriptions:', subs);
+          setSubscriptions(subs || []);
+        } catch (err) {
+          console.error('Error fetching products:', err);
+        }
+      }
+
+      const token = storage.get('token', StoreType.STRING) as string;
+      if (token) {
+        const status = await fetchPremiumStatus(token);
+        console.log('status', status);
+        if (status && 'premium-status' in status) {
+          if (status['premium-status'] === 1) {
+            setIsPremium(true);
+          }
+        }
+      }
+    } catch (err) {
+      console.warn('IAP Initialization error:', err);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const purchaseSubscription = async (sku: string) => {
+    try {
+      setLoading(true);
+      let purchase;
+      if (IAP.requestPurchase) {
+        purchase = await IAP.requestPurchase({
+          request: {
+            apple: { sku },
+            google: { skus: [sku] }
+          },
+          type: 'subs'
+        });
+      }
+
+      if (purchase) {
+        console.log('Purchase successful! Object:', JSON.stringify(purchase, null, 2));
+        const singlePurchase = Array.isArray(purchase) ? purchase[0] : purchase;
+
+        const token = storage.get('token', StoreType.STRING) as string;
+        if (token) {
+          const payload = {
+            receipt: singlePurchase.purchaseToken || singlePurchase.transactionId,
+            platform: Platform.OS,
+            productId: singlePurchase.productId || singlePurchase.id,
+            full_receipt: JSON.stringify(purchase)
+          };
+          console.log('Sending premiumReceipt to backend with payload:', payload);
+          try {
+            const apiRes = await appApi.premiumReceipt(token, payload);
+            console.log('Backend premiumReceipt response:', apiRes);
+          } catch (apiErr) {
+            console.error('Backend premiumReceipt failed:', apiErr);
+          }
+        }
+
+        if (IAP.finishTransaction) {
+          await IAP.finishTransaction({ purchase: singlePurchase, isConsumable: false });
+        }
+
+        setIsPremium(true);
+        return true;
+      }
+      return false;
+    } catch (err) {
+      console.warn('Purchase error:', err);
+      return false;
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const restorePurchases = async () => {
+    try {
+      setLoading(true);
+      if (IAP.getAvailablePurchases) {
+        const purchases = await IAP.getAvailablePurchases();
+        console.log('Available purchases for restore:', purchases);
+
+        if (purchases && purchases.length > 0) {
+          const token = storage.get('token', StoreType.STRING) as string;
+          if (token) {
+            const payload = {
+              purchases: JSON.stringify(purchases),
+              full_receipt: JSON.stringify(purchases),
+              platform: Platform.OS
+            };
+            console.log('Sending restorePremium to backend with payload:', payload);
+            try {
+              const res = await appApi.premiumReceipt(token, payload);
+              console.log('Backend restorePremium response:', res);
+            } catch (err) {
+              console.error('Backend restorePremium error:', err);
+            }
+          }
+
+          setIsPremium(true);
+          return true;
+        }
+      }
+      return false;
+    } catch (err) {
+      console.warn('Restore error:', err);
+      return false;
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <IAPContext.Provider
+      value={{
+        isPremium,
+        subscriptions,
+        purchaseSubscription,
+        restorePurchases,
+        loading,
+      }}
+    >
+      {children}
+    </IAPContext.Provider>
+  );
+};

+ 9 - 0
src/modules/api/app/app-api.ts

@@ -8,6 +8,11 @@ export interface PostGetLastUpdate extends ResponseType {
 
 export interface PostGetPremiumStatus extends ResponseType {
   'premium-status': number;
+  platform: 'web' | 'apple' | 'google';
+  data?: {
+    end_date: string | null;
+    next_payment: string | null;
+  };
 }
 
 export const appApi = {
@@ -18,5 +23,9 @@ export const appApi = {
     request.postForm<PostGetLastUpdate>(API.GET_LAST_DARE_DB_UPDATE, { date }),
   premiumStatus: (token: string) =>
     request.postForm<PostGetPremiumStatus>(API.GET_PREMIUM_STATUS, { token }),
+  premiumReceipt: (token: string, payload: any) =>
+    request.postForm<PostGetPremiumStatus>(API.PREMIUM_RECEIPT, { token, ...payload }),
+  restorePremium: (token: string, payload: any) =>
+    request.postForm<PostGetPremiumStatus>(API.RESTORE_PREMIUM, { token, ...payload }),
   getLastMapTilesUpdate: () => request.postForm<PostGetLastUpdate>(API.GET_LAST_MAP_TILES_UPDATE)
 };

+ 15 - 32
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -55,6 +55,7 @@ import InfoIcon from 'assets/icons/info-solid.svg';
 import Tooltip from 'react-native-walkthrough-tooltip';
 import FriendsIcon from 'assets/icons/friend.svg';
 import CloseSvg from 'assets/icons/close.svg';
+import { usePremiumGuard } from 'src/utils/usePremiumGuard';
 
 export interface FilterModalRef {
   openFilter: (type: string) => void;
@@ -165,7 +166,7 @@ const FilterModal = forwardRef<
     const [countriesForSelector, setCountriesForSelector] = useState<
       { country: number; name: string; count: number; flag: string | null }[]
     >([]);
-    const [premiumModalVisible, setPremiumModalVisible] = useState(false);
+    const { guardFeature } = usePremiumGuard();
     const [toolTipVisible, setToolTipVisible] = useState<boolean>(false);
     const [defaultYear, setDefaultYear] = useState({
       label: moment().year().toString(),
@@ -647,23 +648,19 @@ const FilterModal = forwardRef<
     };
 
     const toggleTrustedSwitch = async () => {
-      if (!isPremium) {
-        setPremiumModalVisible(true);
-        return;
-      }
-      setShowNomads && setShowNomads(false);
-      storage.set('showNomads', false);
-      setTrustedVisible(!trustedVisible);
+      guardFeature(() => {
+        setShowNomads && setShowNomads(false);
+        storage.set('showNomads', false);
+        setTrustedVisible(!trustedVisible);
+      });
     };
 
     const toggleFriendsSwitch = async () => {
-      if (!isPremium) {
-        setPremiumModalVisible(true);
-        return;
-      }
-      setShowNomads && setShowNomads(false);
-      storage.set('showNomads', false);
-      setFriendsVisible(!friendsVisible);
+      guardFeature(() => {
+        setShowNomads && setShowNomads(false);
+        storage.set('showNomads', false);
+        setFriendsVisible(!friendsVisible);
+      });
     };
 
     const requestBackgroundPermissionSafe = async () => {
@@ -917,11 +914,9 @@ const FilterModal = forwardRef<
             <TouchableOpacity
               style={styles.megaSelector}
               onPress={() => {
-                if (!isPremium) {
-                  setPremiumModalVisible(true);
-                  return;
-                }
-                setCountrySelectorVisible(true);
+                guardFeature(() => {
+                  setCountrySelectorVisible(true);
+                });
               }}
               disabled={!isSharing}
             >
@@ -993,18 +988,6 @@ const FilterModal = forwardRef<
             data={countriesForSelector ?? []}
             initialSelectedIds={selectedCountries.map((item) => parseInt(item.country))}
           />
-
-          <WarningModal
-            type={'success'}
-            isVisible={premiumModalVisible}
-            message={
-              'This feature is available to Premium users. Premium account settings can be managed on our website.'
-            }
-            action={() => {}}
-            onClose={() => setPremiumModalVisible(false)}
-            title={'Premium Feature'}
-          />
-
           <WarningModal
             type={'success'}
             isVisible={askLocationVisible}

+ 6 - 8
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -10,6 +10,7 @@ import * as MapLibreRN from '@maplibre/maplibre-react-native';
 import { API_HOST } from 'src/constants';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useNavigation } from '@react-navigation/native';
+import { usePremiumGuard } from 'src/utils/usePremiumGuard';
 
 const MarkerItem = ({
   marker,
@@ -27,6 +28,7 @@ const MarkerItem = ({
   calloutPressRef?: React.MutableRefObject<boolean>;
 }) => {
   const navigation = useNavigation();
+  const { guardFeature } = usePremiumGuard();
 
   if (!marker || !marker.coordinates) {
     return null;
@@ -121,9 +123,7 @@ const MarkerItem = ({
               {parsedAvatars && (
                 <TouchableOpacity
                   onPress={() => {
-                    if (!isPremium) {
-                      setPremiumModalVisible(true);
-                    } else {
+                    guardFeature(() => {
                       navigation.navigate(
                         ...([
                           NAVIGATION_PAGES.USERS_LIST,
@@ -134,7 +134,7 @@ const MarkerItem = ({
                           }
                         ] as never)
                       );
-                    }
+                    });
                   }}
                   style={styles.userImageContainer}
                 >
@@ -227,9 +227,7 @@ const MarkerItem = ({
               {parsedAvatars && (
                 <TouchableOpacity
                   onPressIn={() => {
-                    if (!isPremium) {
-                      setPremiumModalVisible(true);
-                    } else {
+                    guardFeature(() => {
                       navigation.navigate(
                         ...([
                           NAVIGATION_PAGES.USERS_LIST,
@@ -240,7 +238,7 @@ const MarkerItem = ({
                           }
                         ] as never)
                       );
-                    }
+                    });
                   }}
                   style={styles.userImageContainer}
                 >

+ 14 - 22
src/screens/InAppScreens/MapScreen/MultipleSeriesModal/index.tsx

@@ -9,11 +9,12 @@ import { API_HOST } from 'src/constants';
 import { useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useSubscription } from 'src/screens/OfflineMapsScreen/useSubscription';
+import { usePremiumGuard } from 'src/utils/usePremiumGuard';
 
 const MultipleSeriesModal = () => {
   const isSmallScreen = Dimensions.get('window').width < 383;
   const navigation = useNavigation();
-  const { isPremium, loading } = useSubscription();
+  const { guardFeature } = usePremiumGuard();
   const [seriesData, setSeriesData] = useState<any>(null);
   const [markers, setMarkers] = useState<any[]>([]);
 
@@ -76,11 +77,6 @@ const MultipleSeriesModal = () => {
 
           shouldOpenWarningModalRef.current = false;
           seriesData.setIsWarningModalVisible(true);
-        } else if (shouldOpenPremiumWarningModalRef.current) {
-          if (!seriesData) return;
-
-          shouldOpenPremiumWarningModalRef.current = false;
-          seriesData.setPremiumModalVisible(true);
         }
       }}
       containerStyle={styles.sheetContainer}
@@ -125,23 +121,19 @@ const MultipleSeriesModal = () => {
                       {parsedAvatars && (
                         <TouchableOpacity
                           onPress={() => {
-                            if (!isPremium) {
-                              shouldOpenPremiumWarningModalRef.current = true;
-                              SheetManager.hide('multiple-series-modal');
-
-                              return;
-                            }
                             SheetManager.hide('multiple-series-modal');
-                            navigation.navigate(
-                              ...([
-                                NAVIGATION_PAGES.USERS_LIST,
-                                {
-                                  id: item.id,
-                                  isFromHere: false,
-                                  type: 'series'
-                                }
-                              ] as never)
-                            );
+                            guardFeature(() => {
+                              navigation.navigate(
+                                ...([
+                                  NAVIGATION_PAGES.USERS_LIST,
+                                  {
+                                    id: item.id,
+                                    isFromHere: false,
+                                    type: 'series'
+                                  }
+                                ] as never)
+                              );
+                            });
                           }}
                           style={styles.userImageContainer}
                         >

+ 4 - 16
src/screens/InAppScreens/MapScreen/index.tsx

@@ -13,6 +13,8 @@ import {
 } from 'react-native';
 import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
 
+import { useIAPContext } from 'src/contexts/IAPContext';
+
 import * as MapLibreRN from '@maplibre/maplibre-react-native';
 import { styles } from './style';
 import { SafeAreaView } from 'react-native-safe-area-context';
@@ -539,7 +541,6 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [isEditModalVisible, setIsEditModalVisible] = useState(false);
   const filterModalRef = useRef<FilterModalRef>(null);
   const [isLocationLoading, setIsLocationLoading] = useState(false);
-  const [premiumModalVisible, setPremiumModalVisible] = useState(false);
 
   const [modalState, setModalState] = useState({
     selectedFirstYear: 2021,
@@ -601,7 +602,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   const [didFinishLoadingStyle, setDidFinishLoadingStyle] = useState(false);
 
-  const { isPremium, loading } = useSubscription();
+  const { isPremium, loading } = useIAPContext();
 
   useEffect(() => {
     if (netInfo && netInfo.isConnected !== null) {
@@ -1944,8 +1945,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             token,
             toggleSeries,
             setSelectedMarker,
-            setIsWarningModalVisible,
-            setPremiumModalVisible
+            setIsWarningModalVisible
           } as any
         });
 
@@ -2558,8 +2558,6 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             marker={selectedMarker}
             toggleSeries={toggleSeries}
             token={token}
-            isPremium={isPremium}
-            setPremiumModalVisible={setPremiumModalVisible}
             calloutPressRef={calloutPressRef}
           />
         )}
@@ -2899,16 +2897,6 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         }}
         message="NomadMania app needs location permissions to function properly. Open settings?"
       />
-      <WarningModal
-        type={'success'}
-        isVisible={premiumModalVisible}
-        message={
-          'This feature is available to Premium users. Premium account settings can be managed on our website.'
-        }
-        action={() => { }}
-        onClose={() => setPremiumModalVisible(false)}
-        title={'Premium Feature'}
-      />
       <MultipleSeriesModal />
     </SafeAreaView>
   );

+ 178 - 0
src/screens/ManageSubscriptionScreen/index.tsx

@@ -0,0 +1,178 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Linking, Platform } from 'react-native';
+import { AntDesign } from '@expo/vector-icons';
+import { useNavigation } from '@react-navigation/native';
+import { storage, StoreType } from 'src/storage';
+import { fetchPremiumStatus } from '@api/app';
+import { Colors } from 'src/theme';
+import * as IAP from 'expo-iap';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { API_HOST } from 'src/constants';
+
+const ManageSubscriptionScreen = () => {
+  const navigation = useNavigation();
+  const [loading, setLoading] = useState(true);
+  const [validUntil, setValidUntil] = useState<string | null>(null);
+  const [subSource, setSubSource] = useState<string>('web');
+
+  useEffect(() => {
+    const loadStatus = async () => {
+      try {
+        const token = storage.get('token', StoreType.STRING) as string;
+        if (token) {
+          const status = await fetchPremiumStatus(token);
+          if (status?.data) {
+            setValidUntil(status.data.end_date || status.data.next_payment || null);
+            setSubSource(status.platform || 'web');
+          }
+        }
+      } catch (err) {
+        console.warn('Failed to fetch premium status on manage screen:', err);
+      } finally {
+        setLoading(false);
+      }
+    };
+    loadStatus();
+  }, []);
+
+  const handleManage = async () => {
+    if (subSource === 'wp') {
+      Linking.openURL(API_HOST + '/my-account/subscriptions/').catch(err =>
+        console.error('Failed to open WP subscription URL:', err)
+      );
+    } else if (subSource === 'apple' || subSource === 'google') {
+      try {
+        if (IAP.deepLinkToSubscriptions) {
+          await IAP.deepLinkToSubscriptions();
+        } else {
+          if (Platform.OS === 'ios') {
+            Linking.openURL('https://apps.apple.com/account/subscriptions');
+          } else {
+            Linking.openURL('https://play.google.com/store/account/subscriptions');
+          }
+        }
+      } catch (e) {
+        console.error('Deep link to subscriptions failed', e);
+      }
+    } else {
+      if (Platform.OS === 'ios') {
+        Linking.openURL('https://apps.apple.com/account/subscriptions');
+      } else {
+        Linking.openURL('https://play.google.com/store/account/subscriptions');
+      }
+    }
+  };
+
+  return (
+    <SafeAreaView style={styles.safeArea} edges={['bottom', 'left', 'right']}>
+      <View style={styles.container}>
+        <TouchableOpacity style={styles.closeButton} onPress={() => navigation.goBack()}>
+          <AntDesign name="close" size={20} color={Colors.LIGHT_GRAY} />
+        </TouchableOpacity>
+
+        <Text style={styles.title}>Manage Subscription</Text>
+        <Text style={styles.subtitle}>Review your subscription details</Text>
+
+        <View style={styles.detailsContainer}>
+          {loading ? (
+            <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+          ) : (
+            <View>
+              <Text style={styles.infoLabel}>Status:</Text>
+              <Text style={styles.infoValue}>Active</Text>
+
+              <View style={styles.separator} />
+
+              <Text style={styles.infoLabel}>Valid Until / Next Payment:</Text>
+              <Text style={styles.infoValue}>{validUntil || 'Lifetime / Unknown'}</Text>
+
+              <View style={styles.separator} />
+
+              <Text style={styles.infoLabel}>Source:</Text>
+              <Text style={styles.infoValue}>
+                {subSource === 'wp' ? 'Website' :
+                  subSource === 'apple' ? 'App Store' :
+                    subSource === 'google' ? 'Google Play' : 'Unknown'}
+              </Text>
+            </View>
+          )}
+        </View>
+
+        {!loading && (
+          <TouchableOpacity style={styles.manageButton} onPress={handleManage}>
+            <Text style={styles.manageButtonText}>Manage Subscription</Text>
+          </TouchableOpacity>
+        )}
+      </View>
+    </SafeAreaView>
+  );
+};
+
+const styles = StyleSheet.create({
+  safeArea: {
+    flex: 1,
+    backgroundColor: Colors.WHITE,
+  },
+  container: {
+    flex: 1,
+    padding: 16,
+    backgroundColor: Colors.WHITE,
+  },
+  closeButton: {
+    alignSelf: 'flex-start',
+    padding: 5,
+  },
+  title: {
+    fontSize: 20,
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE,
+    marginBottom: 8,
+    textAlign: 'center',
+  },
+  subtitle: {
+    fontSize: 14,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    textAlign: 'center',
+    marginBottom: 20,
+  },
+  detailsContainer: {
+    backgroundColor: Colors.FILL_LIGHT,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    borderRadius: 12,
+    padding: 16,
+    marginBottom: 30,
+  },
+  infoLabel: {
+    fontSize: 14,
+    color: Colors.TEXT_GRAY,
+    fontFamily: 'montserrat-600',
+    marginBottom: 4,
+  },
+  infoValue: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE,
+  },
+  separator: {
+    height: 1,
+    backgroundColor: Colors.LIGHT_GRAY,
+    marginVertical: 15,
+  },
+  manageButton: {
+    backgroundColor: Colors.WHITE,
+    borderWidth: 1,
+    borderColor: Colors.ORANGE,
+    padding: 15,
+    borderRadius: 8,
+    alignItems: 'center',
+  },
+  manageButtonText: {
+    color: Colors.ORANGE,
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+});
+
+export default ManageSubscriptionScreen;

+ 337 - 0
src/screens/PremiumPaywallScreen/PremiumPaywallScreen.tsx

@@ -0,0 +1,337 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, Image, ScrollView } from 'react-native';
+import { useIAPContext } from 'src/contexts/IAPContext';
+import { useNavigation } from '@react-navigation/native';
+import { AntDesign } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+const PremiumPaywallScreen = () => {
+  const { subscriptions, purchaseSubscription, restorePurchases, loading, isPremium } = useIAPContext();
+  const navigation = useNavigation();
+
+  const [selectedSku, setSelectedSku] = useState<string | null>(null);
+  const [displaySubs, setDisplaySubs] = useState<any[]>([]);
+
+  useEffect(() => {
+    if (subscriptions.length > 0) {
+      const subs = [...subscriptions];
+      if (subs.length === 1) {
+        subs.push({
+          id: 'dummy_premium_monthly',
+          title: 'NM Premium Monthly (Dummy)',
+          description: 'NM Premium',
+          price: 9.99,
+          currency: 'USD',
+          displayPrice: '9,99 USD'
+        });
+      }
+
+      subs.sort((a, b) => (b.price || 0) - (a.price || 0));
+
+      setDisplaySubs(subs);
+      if (!selectedSku) {
+        setSelectedSku(subs[0].productId || subs[0].id);
+      }
+    }
+  }, [subscriptions]);
+
+  const handlePurchase = async (sku: string) => {
+    const success = await purchaseSubscription(sku);
+    if (success) {
+      Alert.alert('Success', 'Welcome to NomadMania Premium!');
+      if (navigation.canGoBack()) {
+        navigation.goBack();
+      }
+    } else {
+      Alert.alert('Error', 'Purchase failed or was canceled.');
+    }
+  };
+
+  const handleRestore = async () => {
+    const success = await restorePurchases();
+    if (success) {
+      Alert.alert('Success', 'Purchases restored successfully.');
+      if (navigation.canGoBack()) {
+        navigation.goBack();
+      }
+    } else {
+      Alert.alert('Notice', 'No previous purchases found to restore.');
+    }
+  };
+
+  const renderProduct = (product: any) => {
+    const productId = product.productId || product.id;
+    const title = product.title || product.displayName || productId;
+    const desc = product.description;
+    const price = product.localizedPrice || product.displayPrice || `${product.price} ${product.currency}`;
+    const isSelected = selectedSku === productId;
+
+    return (
+      <TouchableOpacity
+        key={productId}
+        style={[styles.productCard, isSelected ? styles.productCardSelected : styles.productCardUnselected]}
+        onPress={() => setSelectedSku(productId)}
+        disabled={loading}
+      >
+        <View style={styles.productInfo}>
+          <Text style={styles.productTitle}>{title}</Text>
+          <Text style={styles.productDesc}>{desc}</Text>
+        </View>
+        <View style={styles.priceContainer}>
+          <Text style={styles.productPrice}>{price}</Text>
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  return (
+    <SafeAreaView style={styles.safeArea}>
+      <ScrollView style={styles.container}>
+        <View style={styles.headerRow}>
+          <TouchableOpacity style={styles.closeButton} onPress={() => navigation.goBack()}>
+            <AntDesign name="close" size={20} color={Colors.LIGHT_GRAY} />
+          </TouchableOpacity>
+          <TouchableOpacity style={styles.restoreHeaderButton} onPress={handleRestore} disabled={loading}>
+            <Text style={styles.restoreHeaderText}>Restore Purchases</Text>
+          </TouchableOpacity>
+          <View style={{ width: 30 }}></View>
+        </View>
+
+        <Text style={styles.title}>NomadMania Premium</Text>
+        <Text style={styles.subtitle}>Join Premium and shape the future of NomadMania</Text>
+
+        {isPremium && (
+          <View style={styles.premiumBadge}>
+            <Text style={styles.premiumText}>You are currently a Premium user!</Text>
+          </View>
+        )}
+
+        <Image source={require('../../../assets/premium/premium.png')} style={styles.premiumBadgeImage} />
+
+        <View style={styles.infoBox}>
+          <Text style={styles.infoBoxTitle}>The current Premium version includes:</Text>
+
+          <View>
+            <Image source={require('../../../assets/premium/badge.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>A Premium badge on your profile and ranking page</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/location-filter.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>Friends and Countries filter in the nomads location sharing tool</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/trusted.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>Automatically become a Trusted User with all benefits</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/series.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>View specific Series visited by other members</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/data-export.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>Profile data export in the web</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/discounts.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>Loyalty Programme - discounts for Tours and other Events</Text>
+          </View>
+
+          <View>
+            <Image source={require('../../../assets/premium/offline-maps.png')} style={styles.infoBoxImage} />
+            <Text style={styles.infoBoxText}>Offline maps in the app</Text>
+          </View>
+        </View>
+      </ScrollView>
+
+      <View style={styles.productsContainer}>
+        {displaySubs.length > 0 ? (
+          displaySubs.map(renderProduct)
+        ) : (
+          <View style={styles.emptyContainer}>
+            {loading ? (
+              <ActivityIndicator size="large" color={Colors.ORANGE} />
+            ) : (
+              <Text style={styles.emptyText}>No subscriptions available right now.</Text>
+            )}
+          </View>
+        )}
+      </View>
+
+      <TouchableOpacity
+        style={styles.joinButton}
+        onPress={() => {
+          if (selectedSku) handlePurchase(selectedSku);
+        }}
+        disabled={loading || !selectedSku}
+      >
+        <Text style={styles.joinButtonText}>Join NomadMania Premium</Text>
+      </TouchableOpacity>
+
+      {loading && (
+        <View style={styles.loadingOverlay}>
+          <ActivityIndicator size="large" color="#ffffff" />
+        </View>
+      )}
+    </SafeAreaView>
+  );
+};
+
+const styles = StyleSheet.create({
+  safeArea: {
+    flex: 1,
+    backgroundColor: Colors.WHITE,
+  },
+  container: {
+    flex: 1,
+    padding: 16,
+    backgroundColor: Colors.WHITE,
+  },
+  headerRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    marginBottom: 8,
+  },
+  closeButton: {
+    padding: 5,
+  },
+  restoreHeaderButton: {
+    padding: 5,
+  },
+  restoreHeaderText: {
+    fontSize: 13,
+    color: Colors.TEXT_GRAY,
+    fontWeight: '600',
+  },
+  title: {
+    fontSize: 20,
+    color: Colors.ORANGE,
+    marginBottom: 8,
+    textAlign: 'center',
+    textTransform: 'uppercase',
+    fontFamily: 'montserrat-700'
+  },
+  subtitle: {
+    fontSize: 14,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    textAlign: 'center',
+    marginBottom: 20,
+  },
+  premiumBadgeImage: {
+    width: '100%',
+    alignSelf: 'center',
+    marginBottom: 16,
+    borderRadius: 6,
+  },
+  infoBox: {
+    backgroundColor: Colors.ORANGE,
+    padding: 16,
+    borderRadius: 6,
+    marginBottom: 16,
+    gap: 16
+  },
+  infoBoxTitle: {
+    fontSize: 14,
+    fontFamily: 'montserrat-700',
+    color: Colors.WHITE,
+    textAlign: 'center',
+  },
+  infoBoxImage: {
+    alignSelf: 'center',
+    marginBottom: 8,
+  },
+  infoBoxText: {
+    fontSize: 14,
+    fontWeight: '700',
+    color: Colors.DARK_BLUE,
+    textAlign: 'center',
+  },
+  productsContainer: {
+    justifyContent: 'center',
+    paddingHorizontal: 16,
+    paddingTop: 8,
+    marginBottom: 16,
+    gap: 8,
+  },
+  productCard: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: Colors.WHITE,
+    borderWidth: 2,
+    borderRadius: 12,
+    padding: 12,
+  },
+  productCardSelected: {
+    borderColor: Colors.ORANGE,
+  },
+  productCardUnselected: {
+    borderColor: Colors.FILL_LIGHT,
+  },
+  productInfo: {
+    flex: 1,
+  },
+  productTitle: {
+    fontSize: 14,
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE,
+    marginBottom: 4,
+  },
+  productDesc: {
+    fontSize: 13,
+    color: Colors.DARK_BLUE,
+  },
+  priceContainer: {
+    marginLeft: 15,
+  },
+  productPrice: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: Colors.ORANGE,
+  },
+  joinButton: {
+    backgroundColor: Colors.ORANGE,
+    padding: 16,
+    borderRadius: 8,
+    alignItems: 'center',
+    marginHorizontal: 16,
+  },
+  joinButtonText: {
+    color: Colors.WHITE,
+    fontSize: 16,
+    fontFamily: 'montserrat-700',
+    fontWeight: 'bold',
+  },
+  loadingOverlay: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  premiumBadge: {
+    padding: 15,
+    marginBottom: 20,
+  },
+  premiumText: {
+    color: Colors.DARK_BLUE,
+    textAlign: 'center',
+    fontWeight: 'bold',
+  },
+  emptyContainer: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 20,
+  },
+  emptyText: {
+    color: Colors.TEXT_GRAY,
+    fontSize: 16,
+  }
+});
+
+export default PremiumPaywallScreen;

+ 6 - 2
src/types/api.ts

@@ -226,7 +226,9 @@ export enum API_ENDPOINT {
   GET_TRIPS_FOR_REGION = 'get-trips-for-region',
   GET_REGIONS_THAT_HAVE_TRIPS = 'get-regions-that-have-trips',
   GET_MAP_DATA = 'get-map-data',
-  GET_SINGLE_SERIES_ITEM = 'get-single-item'
+  GET_SINGLE_SERIES_ITEM = 'get-single-item',
+  PREMIUM_RECEIPT = 'premium-receipt',
+  RESTORE_PREMIUM = 'restore-premium'
 }
 
 export enum API {
@@ -426,7 +428,9 @@ export enum API {
   GET_TRIPS_FOR_REGION = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_FOR_REGION}`,
   GET_REGIONS_THAT_HAVE_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_THAT_HAVE_TRIPS}`,
   GET_MAP_DATA = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_MAP_DATA}`,
-  GET_SINGLE_SERIES_ITEM = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SINGLE_SERIES_ITEM}`
+  GET_SINGLE_SERIES_ITEM = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SINGLE_SERIES_ITEM}`,
+  PREMIUM_RECEIPT = `${API_ROUTE.APP}/${API_ENDPOINT.PREMIUM_RECEIPT}`,
+  RESTORE_PREMIUM = `${API_ROUTE.APP}/${API_ENDPOINT.RESTORE_PREMIUM}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 3 - 1
src/types/navigation.ts

@@ -94,5 +94,7 @@ export enum NAVIGATION_PAGES {
   IN_APP_EVENTS_TAB = 'Events',
   CREATE_SHARED_TRIP = 'inAppCreateSharedTrip',
   EDIT_NM_DATA = 'inAppEditNmData',
-  REGIONS_VISITED = 'inAppRegionsVisited'
+  REGIONS_VISITED = 'inAppRegionsVisited',
+  PREMIUM_PAYWALL = 'inAppPremiumPaywall',
+  MANAGE_SUBSCRIPTION = 'inAppManageSubscription'
 }

+ 18 - 0
src/utils/usePremiumGuard.ts

@@ -0,0 +1,18 @@
+import { useNavigation } from '@react-navigation/native';
+import { useIAPContext } from 'src/contexts/IAPContext';
+import { NAVIGATION_PAGES } from 'src/types';
+
+export const usePremiumGuard = () => {
+  const { isPremium } = useIAPContext();
+  const navigation = useNavigation<any>();
+
+  const guardFeature = (onPremiumCallback: () => void) => {
+    if (isPremium) {
+      onPremiumCallback();
+    } else {
+      navigation.navigate(NAVIGATION_PAGES.PREMIUM_PAYWALL);
+    }
+  };
+
+  return { guardFeature, isPremium };
+};