From 2f3ecac7384daa6db4227e1ef8ff95288130cd90 Mon Sep 17 00:00:00 2001 From: Ore O Date: Thu, 23 May 2024 18:09:52 +0100 Subject: [PATCH] Quest Update, Health UI & Home Page redesign (#218) * feat: add quest page * feat: allow people fund via the profile page * feat: display quest list if already subscribed * feat: update to the quest details page * fix: one less click to manual edits * feat: save quests locally, add health card, new api util * feat: logic to save quest prompt * fix: make sure questId is added when saving quest prompt * fix: switch to using async storage * feat: redesign home ui * feat: switch to using async storage * misc: build updates --- mobile/README.md | 8 + mobile/android/app/build.gradle | 2 +- mobile/app.config.ts | 7 +- mobile/eas.json | 9 +- mobile/ios/Fusion/Fusion.entitlements | 5 + mobile/ios/Fusion/Info.plist | 4 +- mobile/package-lock.json | 33 +- mobile/package.json | 6 +- mobile/src/@types/index.ts | 19 +- mobile/src/components/health-details.tsx | 161 ++++++++++ mobile/src/components/modal.tsx | 7 +- .../create-prompt-sheet.tsx | 1 + .../prompts/headers/edit-prompt-header.tsx | 2 +- .../headers/quick-add-prompt-header.tsx | 15 +- .../components/quests/join-quest-sheet.tsx | 28 +- mobile/src/contexts/account.context.tsx | 40 ++- mobile/src/lib/db.ts | 61 ++++ mobile/src/pages/edit-prompt.tsx | 1 + mobile/src/pages/home.tsx | 267 +++++++--------- mobile/src/pages/prompt-entry.tsx | 2 +- mobile/src/pages/quest-detail.tsx | 282 +++++++++-------- mobile/src/pages/quests.tsx | 51 +-- mobile/src/services/prompt.service.ts | 30 +- mobile/src/services/quest.service.ts | 299 ++++++++++++++++++ mobile/src/utils/consent.ts | 8 +- mobile/src/utils/health.ts | 74 ++++- mobile/src/utils/utils.tsx | 36 ++- 27 files changed, 1070 insertions(+), 388 deletions(-) create mode 100644 mobile/src/components/health-details.tsx create mode 100644 mobile/src/services/quest.service.ts diff --git a/mobile/README.md b/mobile/README.md index 4c237f6a..e0a8bfe4 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -191,3 +191,11 @@ Expo handles environment variables a little different than regular web app. We s and then use constants. See appInsights.js for an example. More docs - https://docs.expo.dev/build-reference/variables/ - Follow this doc for neurosity bluetooth integration: https://docs.neurosity.co/docs/api/bluetooth-react-native + +### Custom Notifications + +Currently used to nudge users on Sundays & at the start of every month to view their Fusion Copilot insights page + +- Right now, this notification doesn't present the summaries but it should contain dynamic content movine forward. + +- This is dependent on the taskmanager.service.ts diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index d91277b8..d2fc7ffc 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -120,7 +120,7 @@ missingDimensionStrategy "store", "play" applicationId 'com.neurofusion.fusion' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 84 + versionCode 86 versionName "2.0.0" missingDimensionStrategy 'store', 'play' } diff --git a/mobile/app.config.ts b/mobile/app.config.ts index bd8a3983..f13ef409 100644 --- a/mobile/app.config.ts +++ b/mobile/app.config.ts @@ -20,7 +20,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ ios: { supportsTablet: true, bundleIdentifier: "com.neurofusion.fusion", - buildNumber: "84", + buildNumber: "86", backgroundColor: "#0B0816", config: { usesNonExemptEncryption: false, @@ -28,6 +28,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ infoPlist: { UIBackgroundModes: ["remote-notification", "fetch"], }, + associatedDomains: ["applinks:usefusion.ai", "applinks:usefusion.app"], }, android: { adaptiveIcon: { @@ -35,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ backgroundColor: "#0B0816", }, package: "com.neurofusion.fusion", - versionCode: 84, + versionCode: 86, softwareKeyboardLayoutMode: "pan", }, web: { @@ -51,7 +52,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ process.env.APP_INSIGHTS_CONNECTION_STRING ?? "InstrumentationKey=5a52ca8a-bd71-4c4c-84f6-d51429acbe03;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/", fusionBackendUrl: - process.env.FUSION_BACKEND_API_URL ?? "http://localhost:4000", //"https://neurofusion-backend.azurewebsites.net", + process.env.FUSION_BACKEND_API_URL ?? "https://upset-bugs-look.loca.lt", //"https://neurofusion-backend.azurewebsites.net", fusionRelayUrl: "wss://relay.usefusion.ai", fusionNostrPublicKey: "5f3a52d8027cdde03a41857e98224dafd69495204d93071199aa86921aa02674", diff --git a/mobile/eas.json b/mobile/eas.json index 89d18194..abe33a47 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -7,7 +7,8 @@ "developmentClient": true, "distribution": "internal", "ios": { - "resourceClass": "m-medium" + "resourceClass": "m-medium", + "image": "macos-sonoma-14.4-xcode-15.3" }, "env": { "APP_INSIGHTS_CONNECTION_STRING": "InstrumentationKey=5a52ca8a-bd71-4c4c-84f6-d51429acbe03;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/", @@ -17,7 +18,8 @@ "preview": { "distribution": "internal", "ios": { - "resourceClass": "m-medium" + "resourceClass": "m-medium", + "image": "macos-sonoma-14.4-xcode-15.3" }, "env": { "APP_INSIGHTS_CONNECTION_STRING": "InstrumentationKey=5a52ca8a-bd71-4c4c-84f6-d51429acbe03;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/", @@ -26,7 +28,8 @@ }, "production": { "ios": { - "resourceClass": "m-medium" + "resourceClass": "m-medium", + "image": "macos-sonoma-14.4-xcode-15.3" }, "env": { "APP_INSIGHTS_CONNECTION_STRING": "InstrumentationKey=e9a047e3-efc6-4339-acef-1ef135fd15ea;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/", diff --git a/mobile/ios/Fusion/Fusion.entitlements b/mobile/ios/Fusion/Fusion.entitlements index bb2ac68a..a38cb356 100644 --- a/mobile/ios/Fusion/Fusion.entitlements +++ b/mobile/ios/Fusion/Fusion.entitlements @@ -4,6 +4,11 @@ aps-environment development + com.apple.developer.associated-domains + + applinks:usefusion.ai + applinks:usefusion.app + com.apple.developer.healthkit com.apple.developer.healthkit.access diff --git a/mobile/ios/Fusion/Info.plist b/mobile/ios/Fusion/Info.plist index 9c9e9c20..d5cab243 100644 --- a/mobile/ios/Fusion/Info.plist +++ b/mobile/ios/Fusion/Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 84 + 86 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS @@ -93,5 +93,7 @@ Automatic UIViewControllerBasedStatusBarAppearance + deploymentTarget + 12.0 \ No newline at end of file diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 60d641c6..2ef4cc00 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -48,6 +48,7 @@ "expo-system-ui": "~2.2.1", "expo-task-manager": "~11.1.1", "expo-web-browser": "~12.1.1", + "muse-js": "^3.3.0", "nativewind": "^2.0.11", "nostr-tools": "1.11.1", "papaparse": "^5.4.1", @@ -79,6 +80,7 @@ "react-native-webview": "^13.8.1", "react-native-webview-crypto": "^0.0.25", "react-native-zip-archive": "^6.0.9", + "rxjs": "^7.8.1", "text-encoding": "^0.7.0", "uuid": "^9.0.0", "wrn-echarts": "^0.1.5" @@ -7528,6 +7530,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.2.tgz", + "integrity": "sha512-E3/gvUxuBxtsLVNZLNr0ATDoWC+QHMIjJvP94CtApCDDM+db4kII+Ci5cWF2At8OQ9Wq1fd1x1c245oL4uD94w==" + }, "node_modules/@types/webpack-env": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", @@ -18544,6 +18551,31 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/muse-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/muse-js/-/muse-js-3.3.0.tgz", + "integrity": "sha512-FH7svDlPGbXa5Fpj5WwMI8fCixLH+Nepq19mXKpmgiQAUn2XRKGCVi9NvdBmWs2QKSFq9M/8aMDjuLc9lSpWpg==", + "dependencies": { + "@types/web-bluetooth": "^0.0.2", + "rxjs": "^6.0.0 || ^5.6.0-forward-compat.4" + } + }, + "node_modules/muse-js/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/muse-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -22571,7 +22603,6 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } diff --git a/mobile/package.json b/mobile/package.json index d737f38e..826e87d8 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -54,6 +54,8 @@ "expo-status-bar": "~1.4.4", "expo-system-ui": "~2.2.1", "expo-task-manager": "~11.1.1", + "expo-web-browser": "~12.1.1", + "muse-js": "^3.3.0", "nativewind": "^2.0.11", "nostr-tools": "1.11.1", "papaparse": "^5.4.1", @@ -85,10 +87,10 @@ "react-native-webview": "^13.8.1", "react-native-webview-crypto": "^0.0.25", "react-native-zip-archive": "^6.0.9", + "rxjs": "^7.8.1", "text-encoding": "^0.7.0", "uuid": "^9.0.0", - "wrn-echarts": "^0.1.5", - "expo-web-browser": "~12.1.1" + "wrn-echarts": "^0.1.5" }, "reactNativePermissionsIOS": [ "Notifications", diff --git a/mobile/src/@types/index.ts b/mobile/src/@types/index.ts index 5235caa3..2f15d420 100644 --- a/mobile/src/@types/index.ts +++ b/mobile/src/@types/index.ts @@ -15,6 +15,7 @@ export type PromptAdditionalMeta = { category?: string; isNotificationActive?: boolean; customOptionText?: string; // ; separated list of options + questId?: string; }; export type CreatePrompt = Omit & { @@ -88,10 +89,22 @@ export interface Quest { title: string; description: string; guid: string; - startDate?: number; - endDate?: number; + organizerName: string; + startTimestamp?: number; + endTimestamp?: number; prompts?: Prompt[]; additionalMeta?: object; config?: string; - organizerName?: string; +} + +export interface QuestDataset { + questGuid: string; + type: "prompt_response" | "health"; + value: string; + timestamp: number; +} + +export interface QuestPrompt { + questId: string; + promptId: string; } diff --git a/mobile/src/components/health-details.tsx b/mobile/src/components/health-details.tsx new file mode 100644 index 00000000..fe8f72ea --- /dev/null +++ b/mobile/src/components/health-details.tsx @@ -0,0 +1,161 @@ +import dayjs from "dayjs"; +import React, { useEffect } from "react"; +import { View, Text } from "react-native"; +import { black } from "tailwindcss/colors"; + +import { Button } from "./button"; +import { Reload } from "./icons"; + +import { AccountContext } from "~/contexts"; +import { + buildHealthDataset, + connectAppleHealth, + FusionHealthDataset, + secondsToHms, +} from "~/utils"; + +export const HealthCard = () => { + const [healthDataset, setHealthDataset] = React.useState< + FusionHealthDataset[] + >([]); + const accountContext = React.useContext(AccountContext); + + useEffect(() => { + console.log("user loading", accountContext?.userLoading); + console.log("user preferences", accountContext?.userPreferences); + + (async () => { + if ( + accountContext?.userLoading === false && + accountContext.userPreferences["enableHealthConnect"] === true + ) { + await syncHealthData(); + } + })(); + }, [accountContext]); + + const syncHealthData = async () => { + // reuse functions from settings page + const res = await connectAppleHealth(); + // update the user preferences if not set and user has connected health + if ( + res === true && + accountContext?.userPreferences["enableHealthConnect"] === false + ) { + accountContext?.setUserPreferences({ + ...accountContext.userPreferences, + enableHealthConnect: true, + }); + } + console.log("health connect response: ", res); + + // build the health dataset + try { + const res = await buildHealthDataset( + dayjs().startOf("day").subtract(5, "days"), + dayjs() + ); + + if (res) { + console.log("health data", res); + setHealthDataset(res); + } + } catch (error) { + console.error("Failed to sync health data", error); + } + }; + + return ( + + + Health + + {/* sync button */} + {accountContext?.userLoading === false && + accountContext?.userPreferences["enableHealthConnect"] === false && ( +