From 50ed7aae108f823c15c905e0cff13d8a2c8bf288 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sat, 14 Feb 2026 21:12:47 +0800 Subject: [PATCH] fix(mobile): hide payment ui outside apk (#4847) --- apps/mobile/src/atoms/server-configs.ts | 6 ++-- apps/mobile/src/lib/error-parser.ts | 8 ++++- apps/mobile/src/lib/payment.ts | 32 +++++++++++++++++++ .../modules/dialogs/UpgradeRequiredDialog.tsx | 5 +++ .../src/modules/settings/SettingsList.tsx | 11 +++++-- .../src/modules/settings/routes/Plan.tsx | 4 +-- .../settings/routes/navigateToPlanScreen.ts | 5 +++ 7 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 apps/mobile/src/lib/payment.ts diff --git a/apps/mobile/src/atoms/server-configs.ts b/apps/mobile/src/atoms/server-configs.ts index 3112b93d5..ff2869282 100644 --- a/apps/mobile/src/atoms/server-configs.ts +++ b/apps/mobile/src/atoms/server-configs.ts @@ -2,16 +2,18 @@ import { createAtomHooks } from "@follow/utils/jotai" import type { StatusConfigs } from "@follow-app/client-sdk" import { atom } from "jotai" +import { isPaymentFeatureEnabled } from "@/src/lib/payment" + export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = createAtomHooks( atom>(null), ) export const useIsPaymentEnabled = () => { const serverConfigs = useServerConfigs() - return Boolean(serverConfigs?.PAYMENT_ENABLED) + return isPaymentFeatureEnabled(serverConfigs?.PAYMENT_ENABLED) } export const getIsPaymentEnabled = () => { const serverConfigs = getServerConfigs() - return Boolean(serverConfigs?.PAYMENT_ENABLED) + return isPaymentFeatureEnabled(serverConfigs?.PAYMENT_ENABLED) } diff --git a/apps/mobile/src/lib/error-parser.ts b/apps/mobile/src/lib/error-parser.ts index 07b64348c..3e5288876 100644 --- a/apps/mobile/src/lib/error-parser.ts +++ b/apps/mobile/src/lib/error-parser.ts @@ -112,7 +112,13 @@ export const toastFetchError = (error: Error, { title: _title }: { title?: strin return } - const needUpgradeError = status === 402 && getIsPaymentEnabled() + const isPaymentFeatureEnabled = getIsPaymentEnabled() + + if (status === 402 && !isPaymentFeatureEnabled) { + return toast.error(t("errors:1004")) + } + + const needUpgradeError = status === 402 && isPaymentFeatureEnabled if (needUpgradeError) { showUpgradeRequiredDialog({ diff --git a/apps/mobile/src/lib/payment.ts b/apps/mobile/src/lib/payment.ts new file mode 100644 index 000000000..4a0ca68e1 --- /dev/null +++ b/apps/mobile/src/lib/payment.ts @@ -0,0 +1,32 @@ +import DeviceInfo from "react-native-device-info" + +import { isAndroid } from "./platform" + +const GOOGLE_PLAY_INSTALLER_PACKAGE_NAME = "com.android.vending" +let isAndroidApkInstallCache: boolean | undefined + +const isGooglePlayInstall = (installerPackageName?: string | null) => + installerPackageName === GOOGLE_PLAY_INSTALLER_PACKAGE_NAME + +export const isAndroidApkInstall = () => { + if (isAndroidApkInstallCache !== undefined) { + return isAndroidApkInstallCache + } + + if (!isAndroid) { + isAndroidApkInstallCache = false + return isAndroidApkInstallCache + } + + try { + isAndroidApkInstallCache = !isGooglePlayInstall(DeviceInfo.getInstallerPackageNameSync()) + return isAndroidApkInstallCache + } catch { + // Treat unknown installer as APK-style install and keep behavior permissive. + isAndroidApkInstallCache = true + return isAndroidApkInstallCache + } +} + +export const isPaymentFeatureEnabled = (paymentEnabled?: boolean | null) => + Boolean(paymentEnabled) && isAndroidApkInstall() diff --git a/apps/mobile/src/modules/dialogs/UpgradeRequiredDialog.tsx b/apps/mobile/src/modules/dialogs/UpgradeRequiredDialog.tsx index df0e20cf2..e5515173f 100644 --- a/apps/mobile/src/modules/dialogs/UpgradeRequiredDialog.tsx +++ b/apps/mobile/src/modules/dialogs/UpgradeRequiredDialog.tsx @@ -3,6 +3,7 @@ import { useEffect } from "react" import { useTranslation } from "react-i18next" import { View } from "react-native" +import { getIsPaymentEnabled } from "@/src/atoms/server-configs" import { Text } from "@/src/components/ui/typography/Text" import type { DialogComponent } from "@/src/lib/dialog" import { Dialog } from "@/src/lib/dialog" @@ -20,6 +21,10 @@ let currentPayload: UpgradeDialogPayload = defaultPayload const getPayload = () => currentPayload export const showUpgradeRequiredDialog = (payload?: UpgradeDialogPayload) => { + if (!getIsPaymentEnabled()) { + return + } + currentPayload = { title: payload?.title, message: payload?.message, diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx index 5408a3ba9..2f6ee0c5b 100644 --- a/apps/mobile/src/modules/settings/SettingsList.tsx +++ b/apps/mobile/src/modules/settings/SettingsList.tsx @@ -7,7 +7,7 @@ import { Fragment, useMemo } from "react" import { useTranslation } from "react-i18next" import { Alert, PixelRatio, View } from "react-native" -import { useServerConfigs } from "@/src/atoms/server-configs" +import { getIsPaymentEnabled, useServerConfigs } from "@/src/atoms/server-configs" import { GroupedInsetListCard, GroupedInsetListNavigationLink, @@ -28,6 +28,7 @@ import { UserSettingCuteFiIcon } from "@/src/icons/user_setting_cute_fi" import { signOut } from "@/src/lib/auth" import { useNavigation } from "@/src/lib/navigation/hooks" import type { Navigation } from "@/src/lib/navigation/Navigation" +import { isPaymentFeatureEnabled } from "@/src/lib/payment" import { accentColor } from "@/src/theme/colors" import { AboutScreen } from "./routes/About" @@ -109,7 +110,7 @@ const SubscriptionGroupNavigationLinks: GroupNavigationLink[] = [ }, iconBackgroundColor: accentColor, anonymous: false, - hideIf: (serverConfigs) => !serverConfigs?.PAYMENT_ENABLED, + hideIf: (serverConfigs) => !isPaymentFeatureEnabled(serverConfigs?.PAYMENT_ENABLED), }, ] @@ -210,7 +211,11 @@ const NavigationLinkGroup: FC<{ } onPress={() => { - if (link.trialNotAllowed && (role === UserRole.Free || role === UserRole.Trial)) { + if ( + link.trialNotAllowed && + (role === UserRole.Free || role === UserRole.Trial) && + getIsPaymentEnabled() + ) { navigation.presentControllerView(PlanScreen) } else { link.onPress({ navigation }) diff --git a/apps/mobile/src/modules/settings/routes/Plan.tsx b/apps/mobile/src/modules/settings/routes/Plan.tsx index 268e265fb..25bebbca3 100644 --- a/apps/mobile/src/modules/settings/routes/Plan.tsx +++ b/apps/mobile/src/modules/settings/routes/Plan.tsx @@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next" import type { LayoutChangeEvent } from "react-native" import { ActivityIndicator, Animated, Easing, Pressable, StyleSheet, View } from "react-native" -import { useServerConfigs } from "@/src/atoms/server-configs" +import { useIsPaymentEnabled, useServerConfigs } from "@/src/atoms/server-configs" import { NavigationBlurEffectHeaderView, SafeNavigationScrollView, @@ -111,11 +111,11 @@ const isFeatureValueVisible = (value: PaymentFeature[keyof PaymentFeature] | nul export const PlanScreen: NavigationControllerView = () => { const { t } = useTranslation("settings") const serverConfigs = useServerConfigs() + const isPaymentEnabled = useIsPaymentEnabled() const role = useUserRole() const roleEndAt = useRoleEndAt() const plans = useMemo(() => serverConfigs?.PAYMENT_PLAN_LIST ?? [], [serverConfigs]) - const isPaymentEnabled = serverConfigs?.PAYMENT_ENABLED const defaultBillingPeriod: BillingPeriod = "yearly" const [billingPeriod, setBillingPeriod] = useState(defaultBillingPeriod) diff --git a/apps/mobile/src/modules/settings/routes/navigateToPlanScreen.ts b/apps/mobile/src/modules/settings/routes/navigateToPlanScreen.ts index b46b16015..a0ae7f2ab 100644 --- a/apps/mobile/src/modules/settings/routes/navigateToPlanScreen.ts +++ b/apps/mobile/src/modules/settings/routes/navigateToPlanScreen.ts @@ -1,6 +1,11 @@ +import { getIsPaymentEnabled } from "@/src/atoms/server-configs" import { Navigation } from "@/src/lib/navigation/Navigation" export const navigateToPlanScreen = () => { + if (!getIsPaymentEnabled()) { + return Promise.resolve() + } + return import("./Plan") .then(({ PlanScreen }) => { Navigation.rootNavigation.pushControllerView(PlanScreen)