diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx
index 8ed5746..7b07980 100644
--- a/apps/expo/app/_layout.tsx
+++ b/apps/expo/app/_layout.tsx
@@ -8,7 +8,8 @@ import { TamaguiProvider, useTheme as useThemeTamagui } from "tamagui";
import "../data/logger";
import { ThemeProvider, useTheme } from "../components/theme-provider";
-import { clearOpenedApp, clearShortcutListener, listenForShortcut } from "../data/shortcut.listener";
+import { clearShortcutListener, listenForShortcut } from "../data/shortcut.listener";
+import { ShortCutPayload } from "../data/shortcut.payload";
import config from "../tamagui.config";
export { ErrorBoundary } from "expo-router";
@@ -58,7 +59,7 @@ function RootLayoutNav() {
console.log("App has come to the foreground!");
} else if (appState.current.match(/active/) && nextAppState === "background") {
clearShortcutListener();
- void clearOpenedApp();
+ void ShortCutPayload.clear();
console.log("App has come to the background!");
}
appState.current = nextAppState;
diff --git a/apps/expo/app/break/[appShortcutName].tsx b/apps/expo/app/break/[appShortcutName].tsx
index f9528a9..e545b46 100644
--- a/apps/expo/app/break/[appShortcutName].tsx
+++ b/apps/expo/app/break/[appShortcutName].tsx
@@ -30,7 +30,7 @@ const Break = observer(() => {
setLoaded(true);
});
void OverviewStore.init();
- }, [loaded, searchParams.appShortcutName]);
+ }, [loaded, searchParams.appShortcutName, searchParams.timestamp]);
const animationProgress = useRef(new Animated.Value(0));
const selectedApp = BreakStore.app;
diff --git a/apps/expo/app/setup/index.tsx b/apps/expo/app/setup/index.tsx
index 3b2bdb7..6bbf60f 100644
--- a/apps/expo/app/setup/index.tsx
+++ b/apps/expo/app/setup/index.tsx
@@ -100,9 +100,9 @@ const Setup = observer(() => {
- Select "App"-Trigger
+ Select "App"-Trigger
- Scroll down and select "App" as the trigger.
+ Scroll down and select "App" as the trigger.
@@ -125,10 +125,10 @@ const Setup = observer(() => {
Configure the trigger
- Select "Choose" and search for the app you want to use.
+ Select "Choose" and search for the app you want to use.
Info: You need to create a trigger for every app you want to use.
- Select "open directly"
- Select "Next" when you are done.
+ Select "open directly"
+ Select "Next" when you are done.
@@ -149,9 +149,9 @@ const Setup = observer(() => {
- Select "digital break" as the action
+ Select "digital break" as the action
- Search for "digital break" and select it.
+ Search for "digital break" and select it.
diff --git a/apps/expo/data/break.store.ts b/apps/expo/data/break.store.ts
index 15b91b7..17b8287 100644
--- a/apps/expo/data/break.store.ts
+++ b/apps/expo/data/break.store.ts
@@ -1,6 +1,4 @@
-import { Vibration } from "react-native";
import Constants, { AppOwnership } from "expo-constants";
-import * as Haptics from "expo-haptics";
import * as Linking from "expo-linking";
import { router } from "expo-router";
import { makeAutoObservable } from "mobx";
@@ -9,7 +7,7 @@ import * as ExpoExitApp from "../../../packages/expo-exit-app";
import { AppStatisticsStore } from "./app.statistics";
import type { App } from "./apps";
import { AppsStore, deepLinks } from "./apps";
-import { clearOpenedApp, updateOpenedApp } from "./shortcut.listener";
+import { ShortCutPayload } from "./shortcut.payload";
const isRunningInExpoGo = Constants.appOwnership === AppOwnership.Expo;
@@ -54,61 +52,14 @@ export class BreakStoreSingleton {
void this.appStatisticsStore.trackEvent({ appId: app.id, type: "break-start" });
}
}
- private getHapticImpactEnum = (impact: string): Haptics.ImpactFeedbackStyle | undefined => {
- switch (impact) {
- case "heavy":
- return Haptics.ImpactFeedbackStyle.Heavy;
- case "medium":
- return Haptics.ImpactFeedbackStyle.Medium;
- case "light":
- return Haptics.ImpactFeedbackStyle.Light;
- }
- };
-
- public async hapticImpact(): Promise {
- const pattern: ("light" | "medium" | "heavy" | "vibrate" | number)[] = [];
- const duration = this.app?.settings.breakDurationSeconds ?? 0;
- const durationInMs = duration * 1;
- for (let i = 0; i < durationInMs; ++i) {
- if (i < duration / 2) {
- pattern.push("light");
- } else {
- pattern.push("medium");
- }
- pattern.push(100);
- }
- console.log("Haptic pattern", pattern);
- for (let i = 0; i < pattern.length; ++i) {
- const e = pattern[i];
- if (i % 2 === 0) {
- // Vibration length, always 400 for iOS
- if (typeof e === "number") {
- Vibration.vibrate(e);
- await new Promise((res) => setTimeout(res, e));
- // Default
- } else if (e === "vibrate" || !e) {
- Vibration.vibrate();
- // Use native impact type
- } else {
- console.log("Haptic impact", e);
- await Haptics.impactAsync(this.getHapticImpactEnum(e) ?? Haptics.ImpactFeedbackStyle.Medium);
- }
- // Await for the pause
- } else {
- if (typeof e !== "number") return;
- await new Promise((res) => setTimeout(res, e));
- }
- }
- }
public async openApp(): Promise {
if (!this.app) {
throw new Error("App not initialized");
}
await this.appStatisticsStore.trackEvent({ appId: this.app.id, type: "app-reopen" });
- await updateOpenedApp(this.app.key, "app-reopen");
- await new Promise((resolve) => setTimeout(resolve, 500));
- // await AsyncStorage.setItem("openedApp", `${this.app.key}_${Date.now()}_app-reopen`);
+ await ShortCutPayload.update(this.app.key, "app-reopen");
+ await new Promise((resolve) => setTimeout(resolve, 500)); // app intent isnt fast enough
await Linking.openURL(deepLinks[this.app.key as keyof typeof deepLinks]);
router.replace("/");
}
@@ -118,8 +69,7 @@ export class BreakStoreSingleton {
throw new Error("App not initialized");
}
await this.appStatisticsStore.trackEvent({ appId: this.app.id, type: "app-close" });
- // await AsyncStorage.setItem("openedApp", `${this.app.key}_${Date.now()}_app-reopen`);
- await clearOpenedApp();
+ await ShortCutPayload.clear();
ExpoExitApp.exit();
}
diff --git a/apps/expo/data/shortcut.listener.ts b/apps/expo/data/shortcut.listener.ts
index 7cfac01..dc0391f 100644
--- a/apps/expo/data/shortcut.listener.ts
+++ b/apps/expo/data/shortcut.listener.ts
@@ -1,23 +1,17 @@
-import * as FileSystem from "expo-file-system";
-
-import { appConfig } from "../constants/app.config";
+import { ShortCutPayload } from "./shortcut.payload";
const MAX_TRY_COUNT = 10;
let intervalId: NodeJS.Timeout | null = null;
-const storageKey = "openedApp";
-const pathSplitted = FileSystem.documentDirectory?.split("/");
-const appPath = pathSplitted?.slice(0, pathSplitted.length - 2).join("/");
-const dataPath = `${appPath}/Library/Application Support/${appConfig.bundleIdentifier}/RCTAsyncLocalStorage_V1/appintent.json`;
// why FileSystem? because AsyncStorage doesnt work in combination with the App Intent.
-// It seems like AsyncStorage caches the value and not directly writes it to the file system.
+// It seems like AsyncStorage caches the value in memory and not directly writes it to the file system.
export const listenForShortcut = async (): Promise<{ app: string; timestamp: number }> => {
let tryCount = 0;
return new Promise((resolve, reject) => {
const time = new Date().getTime();
intervalId = setInterval(() => {
try {
- getOpenedApp()
+ ShortCutPayload.getPayload()
.then((appPayload) => {
if (appPayload && appPayload.event === "break-start") {
clearInterval(intervalId ?? 0);
@@ -41,11 +35,9 @@ export const listenForShortcut = async (): Promise<{ app: string; timestamp: num
}
});
} catch (error) {
- console.log("maybe something went wrong");
- console.log(JSON.stringify(error));
+ console.log(error);
}
}, 500);
- console.log("interval after interval", JSON.stringify(intervalId));
});
};
@@ -54,35 +46,3 @@ export const clearShortcutListener = () => {
clearInterval(intervalId);
}
};
-
-export const getOpenedApp = async (): Promise<{ app: string; timestamp: string; event: string } | null> => {
- const string = await FileSystem.readAsStringAsync(dataPath);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const manifest = JSON.parse(string);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- const openedAppPayload = manifest[storageKey] as string | undefined;
- if (openedAppPayload) {
- const [openedApp, timestamp, event] = openedAppPayload.split("_") as [string, string, string];
- return { app: openedApp, timestamp, event };
- } else {
- return null;
- }
-};
-
-export const updateOpenedApp = async (app: string, event: string) => {
- const string = await FileSystem.readAsStringAsync(dataPath);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const manifest = JSON.parse(string);
- const withoutApp = manifest as Record;
- withoutApp.openedApp = `${app}_${Date.now()}_${event}`;
- await FileSystem.writeAsStringAsync(dataPath, JSON.stringify(withoutApp));
-};
-
-export const clearOpenedApp = async () => {
- const string = await FileSystem.readAsStringAsync(dataPath);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const manifest = JSON.parse(string);
- const withoutApp = manifest as Record;
- delete withoutApp.openedApp;
- await FileSystem.writeAsStringAsync(dataPath, JSON.stringify(withoutApp));
-};
diff --git a/apps/expo/data/shortcut.payload.ts b/apps/expo/data/shortcut.payload.ts
new file mode 100644
index 0000000..7a0cec8
--- /dev/null
+++ b/apps/expo/data/shortcut.payload.ts
@@ -0,0 +1,41 @@
+import * as FileSystem from "expo-file-system";
+
+import { appConfig } from "../constants/app.config";
+
+class ShortCutPayloadSingleton {
+ private readonly storageKey = "openedApp";
+ private pathSplitted = FileSystem.documentDirectory?.split("/");
+ private appPath = this.pathSplitted?.slice(0, this.pathSplitted.length - 2).join("/");
+ private dataPath = `${this.appPath}/Library/Application Support/${appConfig.bundleIdentifier}/RCTAsyncLocalStorage_V1/appintent.json`;
+
+ private loadAppIntentPayload = async (): Promise> => {
+ const string = await FileSystem.readAsStringAsync(this.dataPath);
+ const payload = JSON.parse(string) as Record;
+ return payload;
+ };
+
+ public getPayload = async (): Promise<{ app: string; timestamp: string; event: string } | null> => {
+ const payload = await this.loadAppIntentPayload();
+ const openedAppPayload = payload[this.storageKey] as string | undefined;
+ if (openedAppPayload) {
+ const [openedApp, timestamp, event] = openedAppPayload.split("_") as [string, string, string];
+ return { app: openedApp, timestamp, event };
+ } else {
+ return null;
+ }
+ };
+
+ public update = async (app: string, event: string) => {
+ const appIntentPayload = await this.loadAppIntentPayload();
+ appIntentPayload.openedApp = `${app}_${Date.now()}_${event}`;
+ await FileSystem.writeAsStringAsync(this.dataPath, JSON.stringify(appIntentPayload));
+ };
+
+ public clear = async () => {
+ const appIntentPayload = await this.loadAppIntentPayload();
+ delete appIntentPayload.openedApp;
+ await FileSystem.writeAsStringAsync(this.dataPath, JSON.stringify(appIntentPayload));
+ };
+}
+
+export const ShortCutPayload = new ShortCutPayloadSingleton();
diff --git a/packages/shortcuts-generator/src/index.ts b/packages/shortcuts-generator/src/index.ts
index 9dc8cb0..971308a 100644
--- a/packages/shortcuts-generator/src/index.ts
+++ b/packages/shortcuts-generator/src/index.ts
@@ -23,7 +23,13 @@ const generateShortcuts = async () => {
);
};
+if (os.platform() !== "darwin") {
+ console.error("ERROR: shortcut generation stopped because you are not on macOS. Signing shortcuts requires macOS.");
+ process.exit(0);
+}
+
const time = new Date().getTime();
+
void generateShortcuts().then(() => {
console.log(`took ${new Date().getTime() - time}ms`);
});