diff --git a/app.json b/app.json index 5984d39..fa19498 100644 --- a/app.json +++ b/app.json @@ -21,7 +21,7 @@ "backgroundColor": "#fa4454" }, "package": "dev.vasc.vshop2", - "versionCode": 63 + "versionCode": 64 }, "web": { "bundler": "metro", @@ -42,7 +42,8 @@ "icon": "./assets/images/notification-icon.png", "color": "#fa4454" } - ] + ], + "react-native-background-fetch" ], "experiments": { "typedRoutes": true diff --git a/app/(authenticated)/settings.tsx b/app/(authenticated)/settings.tsx index 224c499..8a9e6bc 100644 --- a/app/(authenticated)/settings.tsx +++ b/app/(authenticated)/settings.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Checkbox, List, Text, TouchableRipple } from "react-native-paper"; import { useTranslation } from "react-i18next"; import { Linking, ToastAndroid, ScrollView } from "react-native"; @@ -11,14 +11,10 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { defaultUser } from "~/utils/valorant-api"; import * as Clipboard from "expo-clipboard"; import { useRouter } from "expo-router"; -import { - checkShop, - isWishlistCheckRegistered, - registerWishlistCheck, - unregisterWishlistCheck, -} from "~/utils/wishlist"; +import { checkShop } from "~/utils/wishlist"; import * as Notifications from "expo-notifications"; import { usePostHog } from "posthog-react-native"; +import { useWishlistStore } from "~/hooks/useWishlistStore"; function Settings() { const { t } = useTranslation(); @@ -26,15 +22,16 @@ function Settings() { const { user, setUser } = useUserStore(); const { isDonator, screenshotModeEnabled, toggleScreenshotMode } = useFeatureStore(); + const notificationEnabled = useWishlistStore( + (state) => state.notificationEnabled + ); + const setNotificationEnabled = useWishlistStore( + (state) => state.setNotificationEnabled + ); + const wishlistedSkins = useWishlistStore((state) => state.skinIds); const { showDonatePopup } = useDonatePopupStore(); const posthog = usePostHog(); - const [isRegistered, setIsRegistered] = useState(false); - - useEffect(() => { - checkWishlistCheckStatus(); - }, []); - const handleLogout = async () => { await CookieManager.clearAll(true); await AsyncStorage.removeItem("region"); @@ -45,10 +42,10 @@ function Settings() { const toggleNotificationEnabled = async () => { if (isDonator) { - if (!isRegistered) { + if (!notificationEnabled) { const permission = await Notifications.requestPermissionsAsync(); if (permission.granted) { - await registerWishlistCheck(); + setNotificationEnabled(true); ToastAndroid.show( t("wishlist.notification.enabled"), ToastAndroid.LONG @@ -60,24 +57,17 @@ function Settings() { ); } } else { - await unregisterWishlistCheck(); + setNotificationEnabled(false); ToastAndroid.show( t("wishlist.notification.disabled"), ToastAndroid.LONG ); } - - await checkWishlistCheckStatus(); } else { showDonatePopup(); } }; - const checkWishlistCheckStatus = async () => { - const isRegistered = await isWishlistCheckRegistered(); - setIsRegistered(isRegistered); - }; - const showLastExecution = async () => { const lastWishlistCheck = await AsyncStorage.getItem("lastWishlistCheck"); const ms = Number.parseInt(lastWishlistCheck || "0"); @@ -122,7 +112,7 @@ function Settings() { )} right={() => ( )} @@ -212,7 +202,7 @@ function Settings() { )} /> - checkShop()}> + checkShop(wishlistedSkins)}> ( diff --git a/app/_layout.tsx b/app/_layout.tsx index e1281ca..6ba2f1b 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -19,6 +19,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { SplashScreen } from "expo-router"; import { useTranslation } from "react-i18next"; import { SharedPostHogProvider } from "~/components/Posthog"; +import { initBackgroundFetch } from "~/utils/wishlist"; export const CombinedDarkTheme = { ...merge(PaperDarkTheme, NavigationDarkTheme), @@ -36,6 +37,8 @@ function RootLayout() { const { t } = useTranslation(); useEffect(() => { + initBackgroundFetch(); + // If user has set the region, he *should* be a returning user AsyncStorage.getItem("region").then((region) => { if (region) { diff --git a/hooks/useWishlistStore.ts b/hooks/useWishlistStore.ts index 9a8d4a0..c2ef04a 100644 --- a/hooks/useWishlistStore.ts +++ b/hooks/useWishlistStore.ts @@ -3,12 +3,17 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; interface WishlistState { + notificationEnabled: boolean; + setNotificationEnabled: (value: boolean) => void; skinIds: string[]; toggleSkin: (uuid: string) => void; } export const useWishlistStore = create()( persist( (set, get) => ({ + notificationEnabled: false, + setNotificationEnabled: (value: boolean) => + set({ notificationEnabled: value }), skinIds: [], toggleSkin: (uuid: string) => set({ diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f393a9d --- /dev/null +++ b/index.ts @@ -0,0 +1,17 @@ +import "expo-router/entry"; + +import BackgroundFetch from "react-native-background-fetch"; +import { wishlistBgTask } from "./utils/wishlist"; + +BackgroundFetch.registerHeadlessTask(async (event) => { + let taskId = event.taskId; + let isTimeout = event.timeout; + if (isTimeout) { + console.log("[BackgroundFetch] Headless TIMEOUT:", taskId); + BackgroundFetch.finish(taskId); + return; + } + + await wishlistBgTask(); + BackgroundFetch.finish(taskId); +}); diff --git a/package.json b/package.json index 9848edc..b8f25a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vshop", - "main": "expo-router/entry", + "main": "index.ts", "version": "1.0.0", "scripts": { "start": "expo start", @@ -30,7 +30,6 @@ "expo": "~51.0.36", "expo-application": "~5.9.1", "expo-av": "~14.0.7", - "expo-background-fetch": "~12.0.1", "expo-clipboard": "~6.0.3", "expo-constants": "~16.0.2", "expo-dev-client": "~4.0.27", @@ -57,6 +56,7 @@ "react-dom": "18.2.0", "react-i18next": "^15.0.2", "react-native": "0.74.5", + "react-native-background-fetch": "^4.2.5", "react-native-gesture-handler": "~2.16.2", "react-native-mask-text": "^0.14.2", "react-native-paper": "^4.12.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed90fab..4a1ebfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,6 @@ importers: expo-av: specifier: ~14.0.7 version: 14.0.7(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) - expo-background-fetch: - specifier: ~12.0.1 - version: 12.0.1(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) expo-clipboard: specifier: ~6.0.3 version: 6.0.3(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) @@ -128,6 +125,9 @@ importers: react-native: specifier: 0.74.5 version: 0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0) + react-native-background-fetch: + specifier: ^4.2.5 + version: 4.2.5 react-native-gesture-handler: specifier: ~2.16.2 version: 2.16.2(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) @@ -2708,11 +2708,6 @@ packages: peerDependencies: expo: '*' - expo-background-fetch@12.0.1: - resolution: {integrity: sha512-8915rCoRKWBwCkhSSowv2+kH+QV6YYR7ES8r+yWAUPQ0vq+26NojeWAZtR0vCyTW+QLsQrYVV5Eh0brpZIMS7w==} - peerDependencies: - expo: '*' - expo-clipboard@6.0.3: resolution: {integrity: sha512-RIKDsuHkYfaspifbFpVC8sBVFKR05L7Pj7mU2/XkbrW9m01OBNvdpGraXEMsTFCx97xMGsZpEw9pPquL4j4xVg==} peerDependencies: @@ -2839,11 +2834,6 @@ packages: peerDependencies: expo: '*' - expo-task-manager@11.8.2: - resolution: {integrity: sha512-Uhy3ol5gYeZOyeRFddYjLI1B2DGRH1gjp/YC8Hpn5p5MVENviySoKNF+wd98rRvOAokzrzElyDBHSTfX+C3tpg==} - peerDependencies: - expo: '*' - expo-updates-interface@0.16.2: resolution: {integrity: sha512-929XBU70q5ELxkKADj1xL0UIm3HvhYhNAOZv5DSk7rrKvLo7QDdPyl+JVnwZm9LrkNbH4wuE2rLoKu1KMgZ+9A==} peerDependencies: @@ -4574,6 +4564,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-native-background-fetch@4.2.5: + resolution: {integrity: sha512-llOU+UWPd1fjOFGNGB4v4YrYaA0sMLn5hMk0aNx0Ok9+wQ2nQillVwj7G/XZEpIiD2vTVkntTNNXWycF5Ju0kQ==} + react-native-gesture-handler@2.16.2: resolution: {integrity: sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg==} peerDependencies: @@ -5364,9 +5357,6 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} - unimodules-app-loader@4.6.0: - resolution: {integrity: sha512-FRNIlx7sLBDVPG117JnEBhnzZkTIgZTEwYW2rzrY9HdvLBTpRN+k0dxY50U/CAhFHW3zMD0OP5JAlnSQRhx5HA==} - unique-filename@3.0.0: resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9310,11 +9300,6 @@ snapshots: dependencies: expo: 51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) - expo-background-fetch@12.0.1(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))): - dependencies: - expo: 51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) - expo-task-manager: 11.8.2(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))) - expo-clipboard@6.0.3(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))): dependencies: expo: 51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) @@ -9493,11 +9478,6 @@ snapshots: transitivePeerDependencies: - supports-color - expo-task-manager@11.8.2(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))): - dependencies: - expo: 51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) - unimodules-app-loader: 4.6.0 - expo-updates-interface@0.16.2(expo@51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))): dependencies: expo: 51.0.36(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2)) @@ -11547,6 +11527,8 @@ snapshots: react-is@18.3.1: {} + react-native-background-fetch@4.2.5: {} + react-native-gesture-handler@2.16.2(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): dependencies: '@egjs/hammerjs': 2.0.17 @@ -12452,8 +12434,6 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} - unimodules-app-loader@4.6.0: {} - unique-filename@3.0.0: dependencies: unique-slug: 4.0.0 diff --git a/utils/wishlist.ts b/utils/wishlist.ts index 4640b0a..8518b64 100644 --- a/utils/wishlist.ts +++ b/utils/wishlist.ts @@ -12,10 +12,7 @@ import i18n from "./localization"; import { checkDonator } from "./vshop-api"; import { useWishlistStore } from "~/hooks/useWishlistStore"; import * as Notifications from "expo-notifications"; -import { Platform } from "react-native"; -import * as BackgroundFetch from "expo-background-fetch"; -import * as TaskManager from "expo-task-manager"; -import * as Network from "expo-network"; +import BackgroundFetch from "react-native-background-fetch"; import { posthog } from "~/components/Posthog"; Notifications.setNotificationHandler({ @@ -26,11 +23,14 @@ Notifications.setNotificationHandler({ }), }); -const BACKGROUND_FETCH_TASK = "wishlist_check"; const NOTIFICATION_CHANNEL = "wishlist"; -TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { - console.log("Executing VShop wishlist background task"); +export async function wishlistBgTask() { + await useWishlistStore.persist.rehydrate(); + const wishlistStore = useWishlistStore.getState(); + + if (!wishlistStore.notificationEnabled) return; + posthog.capture("wishlist_check"); const lastWishlistCheckTs = Number.parseInt( @@ -42,47 +42,17 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { `Last wishlist check ${lastWishlistCheck}, current date: ${now.getTime()}` ); - const networkStatus = await Network.getNetworkStateAsync(); - console.log(`Is internet reachable: ${networkStatus.isInternetReachable}`); - - if ( - (!isSameDayUTC(lastWishlistCheck, now) || lastWishlistCheckTs === 0) && - networkStatus.isInternetReachable - ) { + if (!isSameDayUTC(lastWishlistCheck, now) || lastWishlistCheckTs === 0) { await AsyncStorage.setItem("lastWishlistCheck", now.getTime().toString()); console.log("New day, checking shop in the background"); - await checkShop(); - - return BackgroundFetch.BackgroundFetchResult.NewData; + await checkShop(wishlistStore.skinIds); } console.log("No wishlist check needed"); - - return BackgroundFetch.BackgroundFetchResult.NoData; -}); - -export async function registerWishlistCheck() { - if (Platform.OS !== "android") return; - - return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - minimumInterval: 60, - stopOnTerminate: false, - startOnBoot: true, - }); } -export async function unregisterWishlistCheck() { - if (Platform.OS !== "android") return; - - return BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK); -} - -export async function isWishlistCheckRegistered() { - return TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK); -} - -export async function checkShop() { +export async function checkShop(wishlist: string[]) { await Notifications.setNotificationChannelAsync(NOTIFICATION_CHANNEL, { name: "Wishlist", importance: Notifications.AndroidImportance.MAX, @@ -104,9 +74,6 @@ export async function checkShop() { const region = (await AsyncStorage.getItem("region")) || "eu"; const shop = await getShop(accessToken, entitlementsToken, region, userId); - await useWishlistStore.persist.rehydrate(); - const wishlist = useWishlistStore.getState().skinIds; - var hit = false; for (let i = 0; i < wishlist.length; i++) { if (shop.SkinsPanelLayout.SingleItemOffers.includes(wishlist[i])) { @@ -159,3 +126,29 @@ export async function checkShop() { }); } } + +export async function initBackgroundFetch() { + await BackgroundFetch.configure( + { + minimumFetchInterval: 15, + stopOnTerminate: false, + enableHeadless: true, + startOnBoot: true, + // Android options + forceAlarmManager: false, + requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY, + requiresCharging: false, + requiresDeviceIdle: false, + requiresBatteryNotLow: false, + requiresStorageNotLow: false, + }, + async (taskId: string) => { + await wishlistBgTask(); + BackgroundFetch.finish(taskId); + }, + (taskId: string) => { + console.log("[Fetch] TIMEOUT taskId:", taskId); + BackgroundFetch.finish(taskId); + } + ); +}