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);
+ }
+ );
+}