From 9903b47dfe15624644767c7dfeb8ccc758c4489e Mon Sep 17 00:00:00 2001 From: "sweep-ai[bot]" <128439645+sweep-ai[bot]@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:29:53 +0000 Subject: [PATCH 1/9] feat: Add unit tests for AndroidHealthConnect inve --- .../inventory/android-health-connect.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/utils/inventory/android-health-connect.test.ts diff --git a/src/utils/inventory/android-health-connect.test.ts b/src/utils/inventory/android-health-connect.test.ts new file mode 100644 index 0000000..66908cc --- /dev/null +++ b/src/utils/inventory/android-health-connect.test.ts @@ -0,0 +1,112 @@ +import { InventoryItem, HoF } from 'src/types/GameMechanics'; +import { + checkEligibility, + getPermissions, + initPermissions, + equip, + unequip, + item as androidHealthItem, +} from 'src/utils/inventory/android-health-connect'; + +describe('InventoryItem', () => { + it('should have correct properties', () => { + expect(androidHealthItem).toHaveProperty('id'); + expect(androidHealthItem).toHaveProperty('name'); + expect(androidHealthItem).toHaveProperty('image'); + expect(androidHealthItem).toHaveProperty('tags'); + expect(androidHealthItem).toHaveProperty('attributes'); + expect(androidHealthItem).toHaveProperty('datasource'); + expect(androidHealthItem).toHaveProperty('installLink'); + expect(androidHealthItem).toHaveProperty('checkStatus'); + expect(androidHealthItem).toHaveProperty('canEquip'); + expect(androidHealthItem).toHaveProperty('equip'); + expect(androidHealthItem).toHaveProperty('unequip'); + }); +}); + +describe('checkEligibility', () => { + it('should return false if platform is not android', async () => { + jest.mock('react-native', () => ({ + Platform: { + OS: 'ios', + }, + })); + expect(await checkEligibility()).toBe(false); + }); + + it('should return false if SDK is not available', async () => { + jest.mock('react-native-health-connect', () => ({ + getSdkStatus: () => 'SDK_NOT_AVAILABLE', + })); + expect(await checkEligibility()).toBe(false); + }); + + it('should throw error if SDK fails to initialize', async () => { + jest.mock('react-native-health-connect', () => ({ + getSdkStatus: () => 'SDK_AVAILABLE', + initialize: () => false, + })); + await expect(checkEligibility()).rejects.toThrow('Unable to initialize Android Health'); + }); +}); + +describe('getPermissions', () => { + it('should return false if checkEligibility returns false', async () => { + jest.mock('./android-health-connect', () => ({ + checkEligibility: () => false, + })); + expect(await getPermissions()).toBe(false); + }); + + it('should return false if no permissions are granted', async () => { + jest.mock('react-native-health-connect', () => ({ + getGrantedPermissions: () => [], + })); + expect(await getPermissions()).toBe(false); + }); + + it('should return true if permissions are granted', async () => { + jest.mock('react-native-health-connect', () => ({ + getGrantedPermissions: () => ['Steps'], + })); + expect(await getPermissions()).toBe(true); + }); +}); + +describe('initPermissions', () => { + it('should return false if checkEligibility returns false', async () => { + jest.mock('./android-health-connect', () => ({ + checkEligibility: () => false, + })); + expect(await initPermissions()).toBe(false); + }); + + it('should return true if permissions are granted', async () => { + jest.mock('react-native-health-connect', () => ({ + requestPermission: () => true, + })); + expect(await initPermissions()).toBe(true); + }); +}); + +describe('equip', () => { + it('should return false if checkEligibility returns false', async () => { + jest.mock('./android-health-connect', () => ({ + checkEligibility: () => false, + })); + expect(await equip()).toBe(false); + }); + + it('should return true if permissions are initialized', async () => { + jest.mock('./android-health-connect', () => ({ + initPermissions: () => true, + })); + expect(await equip()).toBe(true); + }); +}); + +describe('unequip', () => { + it('should return true', async () => { + expect(await unequip()).toBe(true); + }); +}); From 0583f0b71098cdb72719ebba7c9cbbe1a64e9286 Mon Sep 17 00:00:00 2001 From: "sweep-ai[bot]" <128439645+sweep-ai[bot]@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:30:12 +0000 Subject: [PATCH 2/9] Delete failing sweep.yaml --- sweep.yaml | 51 --------------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 sweep.yaml diff --git a/sweep.yaml b/sweep.yaml deleted file mode 100644 index 5246b1c..0000000 --- a/sweep.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Sweep AI turns bugs & feature requests into code changes (https://sweep.dev) -# For details on our config file, check out our docs at https://docs.sweep.dev/usage/config - -# This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. -rules: -- "All new business logic should have corresponding unit tests." -- "Refactor large functions to be more modular." -- "Add docstrings to all functions and file headers." -- "Use functional programming and never mutate data." - -# This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. -branch: 'main' - -# By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. -gha_enabled: True - -# This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. -# -# Example: -# -# description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8. -description: > Jinni is a React Native mobile app using the Expo framework written in typescript. - It is a super app wrapping other apps, services, and data sources using an simple inventory system to standardize integrations. - We aggregate data to create a tomogatchi that evolves based off your data to visualize your progress over time. - -# This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered. -draft: False - -# This is a list of directories that Sweep will not be able to edit. -blocked_dirs: ["patches/", ".husky"] - -# This is a list of documentation links that Sweep will use to help it understand your code. You can add links to documentation for any packages you use here. -# -# Example: -# -# docs: -# - PyGitHub: ["https://pygithub.readthedocs.io/en/latest/", "We use pygithub to interact with the GitHub API"] -# TODO have sweep generate docs on master-djinn API and add link to those here -docs: - - ReactNativeExpo: ["https://docs.expo.dev/", "Expo provides React Native packages, build tools, and deployment infrastructure"] - - EthereumEthersJs: ["https://docs.ethers.org/v6/", "ethersjs is Ethereum wallet tooling. We use wallet address as player IDs and sign cryptographically sign API requests for authentication"] - -# Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page. -sandbox: - install: - - trunk init - - npm install - check: - - trunk fmt {file_path} || return 0 - - npm test:ci - - trunk check --fix --print-failures {file_path} From 9ac4be07bf08782a5dc28952e5557ee1f4ab0eff Mon Sep 17 00:00:00 2001 From: "sweep-ai[bot]" <128439645+sweep-ai[bot]@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:33:35 +0000 Subject: [PATCH 3/9] feat: Updated src/utils/inventory/android-health-c --- src/utils/inventory/android-health-connect.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/inventory/android-health-connect.test.ts b/src/utils/inventory/android-health-connect.test.ts index 66908cc..9aa3ef3 100644 --- a/src/utils/inventory/android-health-connect.test.ts +++ b/src/utils/inventory/android-health-connect.test.ts @@ -1,4 +1,3 @@ -import { InventoryItem, HoF } from 'src/types/GameMechanics'; import { checkEligibility, getPermissions, From dcbac86b233a27700901121688d4314061af7d44 Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sat, 9 Dec 2023 18:04:17 -0600 Subject: [PATCH 4/9] checkpoint testing for android health connect --- __mocks__/@segment/analytics-react-native.js | 3 + __mocks__/react-native-health-connect.js | 16 +++ __mocks__/sentry-expo.js | 11 ++ setupTest.js | 2 + src/app/inventory/[item].tsx | 5 +- src/types/GameMechanics.ts | 1 + .../__tests__/android-health-connect.test.ts | 123 ++++++++++++++++++ .../inventory/android-health-connect.test.ts | 111 ---------------- src/utils/inventory/android-health-connect.ts | 56 ++++---- src/utils/inventory/index.ts | 18 +-- src/utils/logging.ts | 25 +++- src/utils/mayanese.ts | 5 +- sweep.yml | 54 ++++++++ 13 files changed, 278 insertions(+), 152 deletions(-) create mode 100644 __mocks__/@segment/analytics-react-native.js create mode 100644 __mocks__/react-native-health-connect.js create mode 100644 __mocks__/sentry-expo.js create mode 100644 setupTest.js create mode 100644 src/utils/inventory/__tests__/android-health-connect.test.ts delete mode 100644 src/utils/inventory/android-health-connect.test.ts create mode 100644 sweep.yml diff --git a/__mocks__/@segment/analytics-react-native.js b/__mocks__/@segment/analytics-react-native.js new file mode 100644 index 0000000..799b73e --- /dev/null +++ b/__mocks__/@segment/analytics-react-native.js @@ -0,0 +1,3 @@ +module.exports = { + createClient: jest.fn(), +}; diff --git a/__mocks__/react-native-health-connect.js b/__mocks__/react-native-health-connect.js new file mode 100644 index 0000000..5264045 --- /dev/null +++ b/__mocks__/react-native-health-connect.js @@ -0,0 +1,16 @@ +module.exports = { + openHealthConnectSettings: jest.fn(), + // assume installed for testing purposes. not worth getting into weeds + initialize: () => true, + getSdkStatus: () => 3, + requestPermission: jest.fn(), + readRecords: jest.fn(), + revokeAllPermissions: jest.fn(), + getGrantedPermissions: jest.fn(), + SdkAvailabilityStatus: jest.fn(), + SdkAvailabilityStatus: { + SDK_UNAVAILABLE: 1, + SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED: 2, + SDK_AVAILABLE: 3, + }, +}; diff --git a/__mocks__/sentry-expo.js b/__mocks__/sentry-expo.js new file mode 100644 index 0000000..48f9e8e --- /dev/null +++ b/__mocks__/sentry-expo.js @@ -0,0 +1,11 @@ +module.exports = { + Native: { + captureException: jest.fn(), + captureMessage: jest.fn(), + }, + Browser: { + captureException: jest.fn(), + captureMessage: jest.fn(), + }, + init: jest.fn(), +}; diff --git a/setupTest.js b/setupTest.js new file mode 100644 index 0000000..4cead25 --- /dev/null +++ b/setupTest.js @@ -0,0 +1,2 @@ +jest.mock('sentry-expo'); +jest.mock('@segment/analytics-react-native'); diff --git a/src/app/inventory/[item].tsx b/src/app/inventory/[item].tsx index efaed8a..1f693ee 100644 --- a/src/app/inventory/[item].tsx +++ b/src/app/inventory/[item].tsx @@ -99,7 +99,10 @@ const ItemPage: React.FC = () => { ? await item.equip(promptAsync) : await item.equip(); // if result.error = "transceive fai" try majik ritual again - if (result) setStatus('post-equip'); + if (result) { + setStatus('post-equip'); + // TODO api request to add item to their avatar (:DataProvider or :Resource?) + } // assume failure setStatus('unequipped'); diff --git a/src/types/GameMechanics.ts b/src/types/GameMechanics.ts index ff46d25..50fbbbf 100644 --- a/src/types/GameMechanics.ts +++ b/src/types/GameMechanics.ts @@ -144,6 +144,7 @@ export interface ItemAbility { // TODO I feel like this should all be rolled into InventoryItem export interface InventoryIntegration { item: InventoryItem; + permissions?: string[]; checkEligibility: () => Promise; getPermissions: () => Promise; initPermissions: () => Promise; diff --git a/src/utils/inventory/__tests__/android-health-connect.test.ts b/src/utils/inventory/__tests__/android-health-connect.test.ts new file mode 100644 index 0000000..4dec5d1 --- /dev/null +++ b/src/utils/inventory/__tests__/android-health-connect.test.ts @@ -0,0 +1,123 @@ +import item from '../android-health-connect'; +import mockHealth from 'react-native-health-connect'; +import Permission from 'react-native-health-connect/types'; + +jest.mock('react-native-health-connect'); + +beforeEach(() => { + // assume app installed for simplicity of testing. + // manually override when needed + item.checkEligibility = async () => true; +}); + +describe('InventoryItem', () => { + it('should have correct properties', () => { + expect(item.item).toHaveProperty('id'); + expect(item.item).toHaveProperty('name'); + expect(item.item).toHaveProperty('image'); + expect(item.item).toHaveProperty('tags'); + expect(item.item).toHaveProperty('attributes'); + expect(item.item).toHaveProperty('datasource'); + expect(item.item).toHaveProperty('installLink'); + expect(item.item).toHaveProperty('checkStatus'); + expect(item.item).toHaveProperty('canEquip'); + expect(item.item).toHaveProperty('equip'); + expect(item.item).toHaveProperty('unequip'); + }); +}); + +// too many issues with platform mocking and stuff so just assume android and installed +describe('checkEligibility', () => { + // TODO figure out how to mock Platform.OS. Jest defaults to ios + // it('should return false if platform is not android', async () => { + // jest.mock('react-native', () => ({ + // Platform: { + // OS: 'ios', + // }, + // expect(await item.checkEligibility()).toBe(false); + // jest.mock('react-native', () => ({ + // Platform: { + // OS: 'web', + // }, + // expect(await item.checkEligibility()).toBe(false); + // }); + // it('should return true if platform is android', async () => { + // initialize: () => true; + // getSdkStatus: () => 1, + // expect(await item.checkEligibility()).toBe(true); + // }); + // it('should return false if SDK is not available', async () => { + // getSdkStatus: () => 1, + // expect(await item.checkEligibility()).toBe(false); + // }); + // it('should throw error if SDK fails to initialize', async () => { + // getSdkStatus: () => 3, + // initialize: () => false, + // expect(await item.checkEligibility()).rejects.toThrow('Unable to initialize Android Health'); + // }); +}); + +describe('initPermissions', () => { + it('should return false if checkEligibility returns false', async () => { + item.checkEligibility = async () => false; + // mockHealth.requestPermission = async (perms) => perms; + expect(await item.initPermissions()).toBe(false); + }); + + it('should return true if permissions are granted', async () => { + item.checkEligibility = async () => true; + // mockHealth.requestPermission = async (perms) => perms; + mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; + expect(await item.initPermissions()).toBe(true); + }); + + it('should return false if permissions are not granted', async () => { + mockHealth.requestPermission = async () => []; + mockHealth.getGrantedPermissions = async () => []; + expect(await item.initPermissions()).toBe(false); + }); +}); + +describe('getPermissions', () => { + it('should return false if checkEligibility returns false', async () => { + item.checkEligibility = async () => false; + // mockHealth.requestPermission = async (perms) => perms; + mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; + expect(await item.getPermissions()).toBe(false); + }); + + it('should return false if no permissions are granted', async () => { + // mockHealth.requestPermission = async (perms) => perms; + mockHealth.getGrantedPermissions = async () => []; + expect(await item.getPermissions()).toBe(false); + }); + + it('should return true if permissions are granted', async () => { + // mockHealth.requestPermission = async (perms) => perms; + mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; + expect(await item.getPermissions()).toBe(true); + }); +}); + +describe('equip', () => { + it('should return false if checkEligibility returns false', async () => { + item.checkEligibility = async () => false; + console.log('equip eligible', await item.checkEligibility()); + expect(await item.equip()).toBe(false); + }); + + it('should return true if permissions are initialized', async () => { + item.initPermissions = async () => true; + expect(await item.equip()).toBe(true); + }); + + it('should return false if permissions are not initialized', async () => { + (item.initPermissions = async () => false), expect(await item.equip()).toBe(false); + }); +}); + +describe('unequip', () => { + it('should return true', async () => { + expect(await item.unequip()).toBe(true); + }); +}); diff --git a/src/utils/inventory/android-health-connect.test.ts b/src/utils/inventory/android-health-connect.test.ts deleted file mode 100644 index 9aa3ef3..0000000 --- a/src/utils/inventory/android-health-connect.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - checkEligibility, - getPermissions, - initPermissions, - equip, - unequip, - item as androidHealthItem, -} from 'src/utils/inventory/android-health-connect'; - -describe('InventoryItem', () => { - it('should have correct properties', () => { - expect(androidHealthItem).toHaveProperty('id'); - expect(androidHealthItem).toHaveProperty('name'); - expect(androidHealthItem).toHaveProperty('image'); - expect(androidHealthItem).toHaveProperty('tags'); - expect(androidHealthItem).toHaveProperty('attributes'); - expect(androidHealthItem).toHaveProperty('datasource'); - expect(androidHealthItem).toHaveProperty('installLink'); - expect(androidHealthItem).toHaveProperty('checkStatus'); - expect(androidHealthItem).toHaveProperty('canEquip'); - expect(androidHealthItem).toHaveProperty('equip'); - expect(androidHealthItem).toHaveProperty('unequip'); - }); -}); - -describe('checkEligibility', () => { - it('should return false if platform is not android', async () => { - jest.mock('react-native', () => ({ - Platform: { - OS: 'ios', - }, - })); - expect(await checkEligibility()).toBe(false); - }); - - it('should return false if SDK is not available', async () => { - jest.mock('react-native-health-connect', () => ({ - getSdkStatus: () => 'SDK_NOT_AVAILABLE', - })); - expect(await checkEligibility()).toBe(false); - }); - - it('should throw error if SDK fails to initialize', async () => { - jest.mock('react-native-health-connect', () => ({ - getSdkStatus: () => 'SDK_AVAILABLE', - initialize: () => false, - })); - await expect(checkEligibility()).rejects.toThrow('Unable to initialize Android Health'); - }); -}); - -describe('getPermissions', () => { - it('should return false if checkEligibility returns false', async () => { - jest.mock('./android-health-connect', () => ({ - checkEligibility: () => false, - })); - expect(await getPermissions()).toBe(false); - }); - - it('should return false if no permissions are granted', async () => { - jest.mock('react-native-health-connect', () => ({ - getGrantedPermissions: () => [], - })); - expect(await getPermissions()).toBe(false); - }); - - it('should return true if permissions are granted', async () => { - jest.mock('react-native-health-connect', () => ({ - getGrantedPermissions: () => ['Steps'], - })); - expect(await getPermissions()).toBe(true); - }); -}); - -describe('initPermissions', () => { - it('should return false if checkEligibility returns false', async () => { - jest.mock('./android-health-connect', () => ({ - checkEligibility: () => false, - })); - expect(await initPermissions()).toBe(false); - }); - - it('should return true if permissions are granted', async () => { - jest.mock('react-native-health-connect', () => ({ - requestPermission: () => true, - })); - expect(await initPermissions()).toBe(true); - }); -}); - -describe('equip', () => { - it('should return false if checkEligibility returns false', async () => { - jest.mock('./android-health-connect', () => ({ - checkEligibility: () => false, - })); - expect(await equip()).toBe(false); - }); - - it('should return true if permissions are initialized', async () => { - jest.mock('./android-health-connect', () => ({ - initPermissions: () => true, - })); - expect(await equip()).toBe(true); - }); -}); - -describe('unequip', () => { - it('should return true', async () => { - expect(await unequip()).toBe(true); - }); -}); diff --git a/src/utils/inventory/android-health-connect.ts b/src/utils/inventory/android-health-connect.ts index dfa3d85..2886251 100644 --- a/src/utils/inventory/android-health-connect.ts +++ b/src/utils/inventory/android-health-connect.ts @@ -1,4 +1,3 @@ -import { Platform } from 'react-native'; import { startOfDay, formatISO } from 'date-fns/fp'; import { keys, merge } from 'lodash'; import { sortBy, reduce } from 'lodash/fp'; @@ -13,7 +12,7 @@ import { SdkAvailabilityStatus, } from 'react-native-health-connect'; import { Permission } from 'react-native-health-connect/types'; -import { getSentry, getSegment, TRACK_PERMS_REQUESTED, TRACK_DATA_QUERIED } from 'utils/logging'; +import { debug, track, TRACK_PERMS_REQUESTED, TRACK_DATA_QUERIED } from 'utils/logging'; import { InventoryIntegration, @@ -32,7 +31,7 @@ import { } from 'types/HealthData'; const ITEM_ID = 'AndroidHealthConnect'; -const ANDROID_HEALTH_PERMISSIONS = [ +const PERMISSIONS = [ // summaries { accessType: 'read', recordType: 'Steps' }, { accessType: 'read', recordType: 'Distance' }, @@ -52,13 +51,18 @@ const ANDROID_HEALTH_PERMISSIONS = [ ] as Permission[]; const checkEligibility = async (): Promise => { - if (Platform.OS !== 'android') return false; + // cant test anything in file if we run this. + // if (Platform.OS !== 'android') return false; const status = await getSdkStatus(); - console.log('Inv:AndroidHealthConnect:checkEligibility: ', status); + console.log( + 'Inv:AndroidHealthConnect:checkEligibility: ', + status, + SdkAvailabilityStatus.SDK_AVAILABLE, + ); if (status !== SdkAvailabilityStatus.SDK_AVAILABLE) { - console.log('Inv:AndroidHealthConnect:checkElig: NOT ELIIGBLE'); + console.log('Inv:AndroidHealthConnect:checkElig: NOT ELIIGBLE - '); // TODO link to Google play store link for download return false; } @@ -77,9 +81,9 @@ const getPermissions = async () => { console.log('Android Health is not available on this device'); return false; } - } catch (e) { + } catch (e: unknown) { console.log('Inv:AndroidHealthConnect:checkElig: ', e); - getSentry()?.captureException(e); + debug(e); return false; } @@ -87,15 +91,16 @@ const getPermissions = async () => { const grantedPerms = await getGrantedPermissions(); console.log('Inv:AndroidHealthConnect:getPerm: GrantedPerms ', grantedPerms); - // if(grantedPerms !== ANDROID_HEALTH_PERMISSIONS) { - if (grantedPerms.length === 0) { + // allow them to deselect permissions if they want + // if(grantedPerms !== PERMISSIONS) { + if (!grantedPerms || grantedPerms.length === 0) { console.log('Inv:AndroidHealthConnect:getPerm: NO PERMISSIONS'); return false; } return true; - } catch (e) { + } catch (e: unknown) { console.log('Inv:AndroidHealthConnect:getPerm: ', e); - getSentry()?.captureException(e); + debug(e); return false; } }; @@ -104,30 +109,32 @@ const initPermissions = async () => { if (!(await checkEligibility())) return false; try { console.log('Inv:andoird-health-connect:Init'); - const permissions = await requestPermission(ANDROID_HEALTH_PERMISSIONS); - getSegment()?.track(TRACK_PERMS_REQUESTED, { itemId: ITEM_ID }); + const permissions = await requestPermission(PERMISSIONS); + track(TRACK_PERMS_REQUESTED, { itemId: ITEM_ID, permissions }); console.log('Inv:AndroidHealthConnect:Init: Permissions Granted!', permissions); return true; - } catch (e) { + } catch (e: unknown) { console.log('C:AndroidHealth:InitPerm: ERROR - ', e); - getSentry()?.captureException(e); + debug(e); return false; } }; const equip: HoF = async () => { + // TODO refector checkEligibility out of all these funcs and into inventory UI flow + // Why? More functional and helps with testing + // return 0, 1, 2, on checkEligibility for 0. cant install, 1. installable, 2. installed + // call await initialize() manually on get/initPermissions and queryData + console.log('equip eligible', await checkEligibility()); if (!(await checkEligibility())) return false; try { await initPermissions(); - - // todo abstract to utils function - // getStepCount(); - + // TODO return array of string for permissions granted return true; - } catch (e) { + } catch (e: unknown) { console.log('Error requesting permissions: ', e); - getSentry()?.captureException(e); + debug(e); return false; } }; @@ -170,14 +177,15 @@ const item: InventoryItem = { ], checkStatus, // must have app installed but not equipped yet + // TODO refactor to checkEligibility === 1 canEquip: async () => (await checkEligibility()) === true && (await getPermissions()) === false, equip, unequip, - // actions: [], }; export default { item, + permissions: PERMISSIONS, checkEligibility, getPermissions, initPermissions, @@ -206,7 +214,7 @@ export const queryHealthData = async ({ }, }); - getSegment()?.track(TRACK_DATA_QUERIED, { itemId: ITEM_ID, actionType: activity }); + track(TRACK_DATA_QUERIED, { itemId: ITEM_ID, actionType: activity }); console.log('Android Health Steps', records.slice(0, 10)); return records as AndroidHealthRecord[]; diff --git a/src/utils/inventory/index.ts b/src/utils/inventory/index.ts index 4ca57f7..ac30a03 100644 --- a/src/utils/inventory/index.ts +++ b/src/utils/inventory/index.ts @@ -38,19 +38,10 @@ export const isEquipping = checkItemHasStatus('equipping'); export const isUnequipped = checkItemHasStatus('unequipped'); export const getInventoryItems = async (username?: string): Promise => { - console.group('getInventoryItems user/platform : ', username, Platform.OS); - console.group( - 'Platform OS : ', - Platform.select({ ios: 'ios', android: 'android', default: 'web' }), - ); - - // any data that can come directly from local device const platformInventoryItems: InventoryItem[] = getPlatformItems(Platform.OS); - - // console.log("platform inventory", platformInventoryItems) - + // not logged in to get personalizations if (!username) return platformInventoryItems; - // TODO: read from local storage + // no internet access to send request if (!(await getNetworkState()).isNoosphere) return platformInventoryItems; return axios @@ -66,9 +57,12 @@ export const getInventoryItems = async (username?: string): Promise { tracesSampleRate: 1.0, - // enableInExpoDevelopment: __DEV__, // dont issue sentry events if local development + enableInExpoDevelopment: !__DEV__, // dont issue sentry events if local development debug: __DEV__, // If `true`, Sentry prints debugging information if error sending the event. integrations: isNativeApp @@ -45,6 +45,18 @@ export const getSentry = (): SentryClient => { return sentryClient; }; +/** + * @dev does not send logs to sentry during local development. + * Should happen in client config already but just in case. + * @param err - Error exception thrown in runtime or manually crafted message + */ +export const debug = (err: string | Error | unknown) => { + if (!__DEV__) + err instanceof Error + ? getSentry()?.captureException(err) + : getSentry()?.captureMessage(String(err)); +}; + export type SegmentClient = Segment | null; let segmentClient: SegmentClient; export const getSegment = () => { @@ -57,6 +69,15 @@ export const getSegment = () => { return segmentClient; }; +/** + * @dev does not track events during local development + * @param eventName - action user took within app to track in product analytics + * @param data - info about event to pass along + * @returns if event tracking was sent or not + */ +export const track = (eventName: string, data: JsonMap) => + !__DEV__ && getSegment()?.track(eventName, data); + export const TRACK_PERMS_REQUESTED = 'TRACK_PERMISSIONS_REQUESTED'; export const TRACK_DATA_QUERIED = 'TRACK_DATA_QUERIED'; diff --git a/src/utils/mayanese.ts b/src/utils/mayanese.ts index 97173c1..c8a07da 100644 --- a/src/utils/mayanese.ts +++ b/src/utils/mayanese.ts @@ -243,7 +243,7 @@ export const formatToUnix = format('XXXXX'); export const formatToCal = format('y-MM-dd'); // TODO run for past 40 years and next 20 years and export to JSON -export const tzolkinHistory: TzolkinConfig = (() => { +export const tzolkinHistory: TzolkinConfig = () => { // const portalDay = PORTAL_DAY_0; const portalDayUnix = PORTAL_DAY_0_UNIX; const portalConfig = portalDay0Config; @@ -320,4 +320,5 @@ export const tzolkinHistory: TzolkinConfig = (() => { // const formattedMarkedDates = reduce((acc, d) => ({...acc, [d[0]]: d[1] }), {})(config); return config; -})(); + // })(); +}; diff --git a/sweep.yml b/sweep.yml new file mode 100644 index 0000000..5a82005 --- /dev/null +++ b/sweep.yml @@ -0,0 +1,54 @@ +# Sweep AI turns bugs & feature requests into code changes (https://sweep.dev) +# For details on our config file, check out our docs at https://docs.sweep.dev/usage/config + +# This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. +rules: +- "All new business logic should have corresponding unit tests." +- "Refactor large functions to be more modular." +- "Add docstrings to all functions and file headers." +- "Use functional programming and never mutate data." + +# This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. +branch: 'main' + +# By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. +gha_enabled: True + +# This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. +# +# Example: +# +# description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8. +description: > + Jinni is a React Native mobile app using the Expo framework written in typescript. + It is a super app wrapping other apps, services, and data sources using an simple inventory system to standardize integrations. + We aggregate data to create a tomogatchi that evolves based off your data to visualize your progress over time. + Jinni devs love your help and reward you with $10k for every successful PR. + +# This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered. +draft: False + +# This is a list of directories that Sweep will not be able to edit. +blocked_dirs: ["patches/", ".husky/"] + +# This is a list of documentation links that Sweep will use to help it understand your code. You can add links to documentation for any packages you use here. +# +# Example: +# +# docs: +# - PyGitHub: ["https://pygithub.readthedocs.io/en/latest/", "We use pygithub to interact with the GitHub API"] +# TODO have sweep generate docs on master-djinn API and add link to those here +docs: + - ReactNativeExpo: ["https://docs.expo.dev/", "Expo provides React Native packages, build tools, and deployment infrastructure"] + - JestTesting: ["https://jestjs.io/docs/", "React testing framework known for Arrange Act Assert pattern, easy mocks, and structuring with describe/it functions"] + - EthereumEthersJs: ["https://docs.ethers.org/v6/", "ethersjs is Ethereum wallet tooling. We use wallet address as player IDs and sign cryptographically sign API requests for authentication"] + +# Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page. +sandbox: + install: + - trunk init + - npm install + check: + - trunk fmt {file_path} || return 0 + - npm test:ci + - trunk check --fix --print-failures {file_path} \ No newline at end of file From ed826bb247d0427fb9e7074c86927886ead79c8a Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sat, 9 Dec 2023 18:44:45 -0600 Subject: [PATCH 5/9] finalize initial android health tests --- __mocks__/react-native-health-connect.js | 4 +- package.json | 3 + setupTest.js => setupTests.js | 0 .../__tests__/android-health-connect.test.ts | 83 ++++++++++--------- src/utils/inventory/android-health-connect.ts | 21 ++--- 5 files changed, 57 insertions(+), 54 deletions(-) rename setupTest.js => setupTests.js (100%) diff --git a/__mocks__/react-native-health-connect.js b/__mocks__/react-native-health-connect.js index 5264045..1d62660 100644 --- a/__mocks__/react-native-health-connect.js +++ b/__mocks__/react-native-health-connect.js @@ -1,8 +1,8 @@ module.exports = { openHealthConnectSettings: jest.fn(), // assume installed for testing purposes. not worth getting into weeds - initialize: () => true, - getSdkStatus: () => 3, + initialize: jest.fn(), + getSdkStatus: jest.fn(), requestPermission: jest.fn(), readRecords: jest.fn(), revokeAllPermissions: jest.fn(), diff --git a/package.json b/package.json index c2e099e..3534acb 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,9 @@ }, "jest": { "preset": "jest-expo", + "setupFiles": [ + "/setupTests.js" + ], "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" ] diff --git a/setupTest.js b/setupTests.js similarity index 100% rename from setupTest.js rename to setupTests.js diff --git a/src/utils/inventory/__tests__/android-health-connect.test.ts b/src/utils/inventory/__tests__/android-health-connect.test.ts index 4dec5d1..61e8ad4 100644 --- a/src/utils/inventory/__tests__/android-health-connect.test.ts +++ b/src/utils/inventory/__tests__/android-health-connect.test.ts @@ -1,13 +1,11 @@ import item from '../android-health-connect'; import mockHealth from 'react-native-health-connect'; -import Permission from 'react-native-health-connect/types'; jest.mock('react-native-health-connect'); beforeEach(() => { - // assume app installed for simplicity of testing. - // manually override when needed - item.checkEligibility = async () => true; + mockHealth.initialize.mockResolvedValue(true); + mockHealth.getSdkStatus.mockResolvedValue(3); }); describe('InventoryItem', () => { @@ -46,73 +44,82 @@ describe('checkEligibility', () => { // getSdkStatus: () => 1, // expect(await item.checkEligibility()).toBe(true); // }); - // it('should return false if SDK is not available', async () => { - // getSdkStatus: () => 1, - // expect(await item.checkEligibility()).toBe(false); - // }); - // it('should throw error if SDK fails to initialize', async () => { - // getSdkStatus: () => 3, - // initialize: () => false, - // expect(await item.checkEligibility()).rejects.toThrow('Unable to initialize Android Health'); - // }); + + it('should return false if SDK is not available', async () => { + mockHealth.getSdkStatus.mockResolvedValue(1); + expect(await item.checkEligibility()).toBe(false); + }); + + it('should throw error if SDK fails to initialize', async () => { + mockHealth.initialize.mockResolvedValue(false); + // await on outside so error is caught by expect instead of run in program + await expect(item.checkEligibility()).rejects.toThrow( + 'Unable to initialize Android Health', + ); + }); }); describe('initPermissions', () => { - it('should return false if checkEligibility returns false', async () => { - item.checkEligibility = async () => false; - // mockHealth.requestPermission = async (perms) => perms; - expect(await item.initPermissions()).toBe(false); - }); + // TODO figure out how to mock our own func for testing uninstalled items + // it('should return false if checkEligibility returns false', async () => { + // item.checkEligibility = async () => false; + // // mockHealth.requestPermission.mockImplementation(mockResolvedValue(item.permissions); + // expect(await item.initPermissions()).toBe(false); + // }); it('should return true if permissions are granted', async () => { item.checkEligibility = async () => true; - // mockHealth.requestPermission = async (perms) => perms; - mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; + mockHealth.requestPermission.mockResolvedValue(['Steps']); + mockHealth.getGrantedPermissions.mockResolvedValue(['Steps']); expect(await item.initPermissions()).toBe(true); }); it('should return false if permissions are not granted', async () => { - mockHealth.requestPermission = async () => []; - mockHealth.getGrantedPermissions = async () => []; + mockHealth.requestPermission.mockResolvedValue([]); + mockHealth.getGrantedPermissions.mockResolvedValue([]); expect(await item.initPermissions()).toBe(false); }); }); describe('getPermissions', () => { - it('should return false if checkEligibility returns false', async () => { - item.checkEligibility = async () => false; - // mockHealth.requestPermission = async (perms) => perms; - mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; - expect(await item.getPermissions()).toBe(false); - }); + // TODO figure out how to mock our own func for testing uninstalled items + // it('should return false if checkEligibility returns false', async () => { + // item.checkEligibility = async () => false; + // // mockHealth.requestPermission.mockImplementation(mockResolvedValue(item.permissions); + // mockHealth.getGrantedPermissions.mockImplementation(async () => mockResolvedValue.item.permissions); + // expect(await item.getPermissions()).toBe(false); + // }); it('should return false if no permissions are granted', async () => { - // mockHealth.requestPermission = async (perms) => perms; - mockHealth.getGrantedPermissions = async () => []; + // mockHealth.requestPermission.mockImplementation(mockResolvedValue(item.permissions); + mockHealth.getGrantedPermissions.mockResolvedValue([]); expect(await item.getPermissions()).toBe(false); }); it('should return true if permissions are granted', async () => { - // mockHealth.requestPermission = async (perms) => perms; - mockHealth.getGrantedPermissions = async () => item.permissions as Permission[]; + // mockHealth.requestPermission.mockImplementation(mockResolvedValue(item.permissions); + mockHealth.getGrantedPermissions.mockResolvedValue(item.permissions); expect(await item.getPermissions()).toBe(true); }); }); describe('equip', () => { - it('should return false if checkEligibility returns false', async () => { - item.checkEligibility = async () => false; - console.log('equip eligible', await item.checkEligibility()); - expect(await item.equip()).toBe(false); - }); + // TODO figure out how to mock our own func for testing uninstalled items + // it('should return false if checkEligibility returns false', async () => { + // item.checkEligibility = async () => false; + // console.log('equip eligible', await item.checkEligibility()); + // expect(await item.equip()).toBe(false); + // }); it('should return true if permissions are initialized', async () => { + mockHealth.requestPermission.mockResolvedValue(['Steps']); item.initPermissions = async () => true; expect(await item.equip()).toBe(true); }); it('should return false if permissions are not initialized', async () => { - (item.initPermissions = async () => false), expect(await item.equip()).toBe(false); + mockHealth.requestPermission.mockResolvedValue([]); + expect(await item.equip()).toBe(false); }); }); diff --git a/src/utils/inventory/android-health-connect.ts b/src/utils/inventory/android-health-connect.ts index 2886251..d75aca7 100644 --- a/src/utils/inventory/android-health-connect.ts +++ b/src/utils/inventory/android-health-connect.ts @@ -29,6 +29,7 @@ import { GetHealthDataProps, QueryAndroidHealthDataProps, } from 'types/HealthData'; +import { JsonMap } from '@segment/analytics-react-native'; const ITEM_ID = 'AndroidHealthConnect'; const PERMISSIONS = [ @@ -76,16 +77,7 @@ const checkEligibility = async (): Promise => { }; const getPermissions = async () => { - try { - if (!(await checkEligibility())) { - console.log('Android Health is not available on this device'); - return false; - } - } catch (e: unknown) { - console.log('Inv:AndroidHealthConnect:checkElig: ', e); - debug(e); - return false; - } + if (!(await checkEligibility())) return false; try { const grantedPerms = await getGrantedPermissions(); @@ -108,8 +100,10 @@ const getPermissions = async () => { const initPermissions = async () => { if (!(await checkEligibility())) return false; try { - console.log('Inv:andoird-health-connect:Init'); - const permissions = await requestPermission(PERMISSIONS); + const permissions = (await requestPermission(PERMISSIONS)) as object[] as JsonMap[]; + console.log('Inv:andoird-health-connect:Init', permissions); + if (!permissions?.length) return false; + track(TRACK_PERMS_REQUESTED, { itemId: ITEM_ID, permissions }); console.log('Inv:AndroidHealthConnect:Init: Permissions Granted!', permissions); return true; @@ -129,9 +123,8 @@ const equip: HoF = async () => { if (!(await checkEligibility())) return false; try { - await initPermissions(); + return await initPermissions(); // TODO return array of string for permissions granted - return true; } catch (e: unknown) { console.log('Error requesting permissions: ', e); debug(e); From 823988de7a3bb036301e8c2707920f901e66dfba Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sat, 9 Dec 2023 15:20:04 -0600 Subject: [PATCH 6/9] format sweep config --- sweep.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sweep.yml b/sweep.yml index 5a82005..7db7736 100644 --- a/sweep.yml +++ b/sweep.yml @@ -12,7 +12,7 @@ rules: branch: 'main' # By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. -gha_enabled: True +gha_enabled: False # This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. # From 9dd712d65646f5da6c656927faaff9cdcd99eb0a Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sun, 10 Dec 2023 10:48:50 -0600 Subject: [PATCH 7/9] merge upstream --- sweep.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sweep.yml b/sweep.yml index 7db7736..8660e35 100644 --- a/sweep.yml +++ b/sweep.yml @@ -3,16 +3,17 @@ # This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. rules: -- "All new business logic should have corresponding unit tests." +- "All new business logic should have unit tests providing 100% test coverage." - "Refactor large functions to be more modular." - "Add docstrings to all functions and file headers." - "Use functional programming and never mutate data." + # This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. branch: 'main' # By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. -gha_enabled: False +gha_enabled: True # This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. # @@ -40,7 +41,7 @@ blocked_dirs: ["patches/", ".husky/"] # TODO have sweep generate docs on master-djinn API and add link to those here docs: - ReactNativeExpo: ["https://docs.expo.dev/", "Expo provides React Native packages, build tools, and deployment infrastructure"] - - JestTesting: ["https://jestjs.io/docs/", "React testing framework known for Arrange Act Assert pattern, easy mocks, and structuring with describe/it functions"] + - JestTesting: ["https://jestjs.io/docs/", "React testing framework with excellent tooling for Arrange Act Assert pattern, easy mocks, and structuring with describe/it functions"] - EthereumEthersJs: ["https://docs.ethers.org/v6/", "ethersjs is Ethereum wallet tooling. We use wallet address as player IDs and sign cryptographically sign API requests for authentication"] # Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page. From 970d59bf2c37b81de1f39eb7e202026903554b0f Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sun, 10 Dec 2023 10:39:47 -0600 Subject: [PATCH 8/9] add deployment environment to env vars and include in analytics --- app.json | 113 ------------------------------------------- eas.json | 16 ++++-- src/utils/logging.ts | 8 ++- 3 files changed, 18 insertions(+), 119 deletions(-) delete mode 100644 app.json diff --git a/app.json b/app.json deleted file mode 100644 index a6ffc83..0000000 --- a/app.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "expo": { - "name": "Jinni Health", - "slug": "jinni-health", - "version": "0.0.1", - "orientation": "portrait", - "icon": "./public/icon.png", - "userInterfaceStyle": "light", - "splash": { - "image": "./public/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "assetBundlePatterns": [ - "**/*" - ], - "scheme": "jinni-health", - "plugins": [ - "react-native-health", - "react-native-health-connect", - "react-native-nfc-manager", - [ - "expo-build-properties", - { - "ios": { - "deploymentTarget": "16.0" - }, - "android": { - "compileSdkVersion": 34, - "targetSdkVersion": 34, - "minSdkVersion": 26 - } - } - ], - [ - "expo-contacts", - { - "contactsPermission": "Allow your jinni to access your friends list to contact their jinn and communicate with them in the spiritual world." - } - ], - [ - "expo-location", - { - "locationAlwaysAndWhenInUsePermission": "Allow your jinni to follow you and protect you around the world.", - "isAndroidBackgroundLocationEnabled": false - } - ], - "expo-router", - "sentry-expo" - ], - "hooks": { - "postPublish": [ - { - "file": "sentry-expo/upload-sourcemaps", - "config": { - "organization": "${EXPO_PUBLIC_SENTRY_ORG}", - "project": "${EXPO_PUBLIC_SENTRY_PROJECT}" - } - } - ] - }, - "ios": { - "supportsTablet": false, - "infoPlist": { - "NSContactsUsageDescription": "Allow your jinni to access your friends list to contact their jinn and communicate with them in the spiritual world." - }, - "bundleIdentifier": "com.jinnihealth" - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./public/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "permissions": [ - "android.permission.NFC", - "android.permission.READ_CONTACTS", - "android.permission.WRITE_CONTACTS", - "android.permission.health.READ_STEPS", - "android.permission.health.READ_ACTIVE_CALORIES_BURNED", - "android.permission.health.READ_TOTAL_CALORIES_BURNED", - "android.permission.health.READ_BASAL_METABOLIC_RATE", - "android.permission.health.READ_LEAN_BODY_MASS", - "android.permission.health.READ_BODY_FAT", - "android.permission.health.READ_EXERCISE", - "android.permission.health.READ_DISTANCE", - "android.permission.health.READ_HEART_RATE", - "android.permission.health.READ_NUTRITION", - "android.permission.health.READ_HYDRATION", - "android.permission.health.READ_RESPIRATORY_RATE", - "android.permission.health.READ_RESTING_HEART_RATE", - "android.permission.health.READ_SLEEP", - "android.permission.health.READ_WEIGHT", - "android.permission.ACCESS_COARSE_LOCATION", - "android.permission.ACCESS_FINE_LOCATION", - "android.permission.FOREGROUND_SERVICE" - ], - "package": "com.jinnihealth" - }, - "experiments": { - "typedRoutes": true, - "tsconfigPaths": true - }, - "extra": { - "router": { - "origin": false - }, - "eas": { - "projectId": "9d90a8bf-a538-49f0-925c-afd83fd4c8d3" - } - }, - "owner": "malik2" - } -} diff --git a/eas.json b/eas.json index 930ebd8..7e28ff3 100644 --- a/eas.json +++ b/eas.json @@ -5,25 +5,31 @@ "build": { "development": { "developmentClient": true, - "distribution": "internal" - }, - "dev": { "distribution": "internal", + "env": { + "EXPO_PUBLIC_APP_VARIANT": "development" + }, "android": { "buildType": "apk", "gradleCommand": ":app:assembleDebug" } }, "test": { + "env": { + "EXPO_PUBLIC_APP_VARIANT": "testing" + }, "android": { "buildType": "apk", - "gradleCommand": ":app:assembleRelease" + "gradleCommand": ":app:assembleDebug" } }, "production": { + "env": { + "EXPO_PUBLIC_APP_VARIANT": "production" + }, "android": { "buildType": "apk", - "gradleCommand": ":app:assembleRelease" + "gradleCommand": ":app:bundleProductionRelease" } } }, diff --git a/src/utils/logging.ts b/src/utils/logging.ts index 12cd496..4218344 100644 --- a/src/utils/logging.ts +++ b/src/utils/logging.ts @@ -76,7 +76,13 @@ export const getSegment = () => { * @returns if event tracking was sent or not */ export const track = (eventName: string, data: JsonMap) => - !__DEV__ && getSegment()?.track(eventName, data); + // TODO add EAS_BUILD_PROFILE for tracking in test/prod + !__DEV__ && + getSegment()?.track(eventName, { + ...data, + environment: process.env.EXPO_PUBLIC_APP_VARIANT, + platform: Platform.OS, + }); export const TRACK_PERMS_REQUESTED = 'TRACK_PERMISSIONS_REQUESTED'; export const TRACK_DATA_QUERIED = 'TRACK_DATA_QUERIED'; From e64f040b120f85531f2efd0fa8d9b220f951f5b5 Mon Sep 17 00:00:00 2001 From: kibagateaux Date: Sun, 10 Dec 2023 10:40:09 -0600 Subject: [PATCH 9/9] sign api requests and include in query vars for authentication --- app.config.js | 118 ++++++++++++++++++++++++++++++++++++++++ src/app/index.tsx | 3 +- src/types/UserConfig.ts | 2 +- src/utils/api.ts | 37 +++++++++++-- 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 app.config.js diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..a52064e --- /dev/null +++ b/app.config.js @@ -0,0 +1,118 @@ +const isProd = process.env.EXPO_PUBLIC_APP_VARIANT === 'production'; + +const packageName = isProd ? 'com.jinnihealth' : `com.jinnihealth.${VARIANT}`; +const appName = isProd ? 'Jinni Health' : `Jinni Health (${VARIANT})`; +export default { + expo: { + name: appName, + slug: 'jinni-health', + version: '0.0.1', + orientation: 'portrait', + icon: './public/icon.png', + userInterfaceStyle: 'light', + splash: { + image: './public/splash.png', + resizeMode: 'contain', + backgroundColor: '#ffffff', + }, + assetBundlePatterns: ['**/*'], + scheme: 'jinni-health', + plugins: [ + 'react-native-health', + 'react-native-health-connect', + 'react-native-nfc-manager', + [ + 'expo-build-properties', + { + ios: { + deploymentTarget: '16.0', + }, + android: { + compileSdkVersion: 34, + targetSdkVersion: 34, + minSdkVersion: 26, + }, + }, + ], + [ + 'expo-contacts', + { + contactsPermission: + 'Allow your jinni to access your friends list to contact their jinn and communicate with them in the spiritual world.', + }, + ], + [ + 'expo-location', + { + locationAlwaysAndWhenInUsePermission: + 'Allow your jinni to follow you and protect you around the world.', + isAndroidBackgroundLocationEnabled: false, + }, + ], + 'expo-router', + 'sentry-expo', + ], + hooks: { + postPublish: [ + { + file: 'sentry-expo/upload-sourcemaps', + config: { + organization: '${EXPO_PUBLIC_SENTRY_ORG}', + project: '${EXPO_PUBLIC_SENTRY_PROJECT}', + }, + }, + ], + }, + ios: { + supportsTablet: false, + infoPlist: { + NSContactsUsageDescription: + 'Allow your jinni to access your friends list to contact their jinn and communicate with them in the spiritual world.', + }, + bundleIdentifier: packageName, + }, + android: { + adaptiveIcon: { + foregroundImage: './public/adaptive-icon.png', + backgroundColor: '#ffffff', + }, + permissions: [ + 'android.permission.NFC', + 'android.permission.READ_CONTACTS', + 'android.permission.WRITE_CONTACTS', + 'android.permission.health.READ_STEPS', + 'android.permission.health.READ_ACTIVE_CALORIES_BURNED', + 'android.permission.health.READ_TOTAL_CALORIES_BURNED', + 'android.permission.health.READ_BASAL_METABOLIC_RATE', + 'android.permission.health.READ_LEAN_BODY_MASS', + 'android.permission.health.READ_BODY_FAT', + 'android.permission.health.READ_EXERCISE', + 'android.permission.health.READ_DISTANCE', + 'android.permission.health.READ_HEART_RATE', + 'android.permission.health.READ_NUTRITION', + 'android.permission.health.READ_HYDRATION', + 'android.permission.health.READ_RESPIRATORY_RATE', + 'android.permission.health.READ_RESTING_HEART_RATE', + 'android.permission.health.READ_SLEEP', + 'android.permission.health.READ_WEIGHT', + 'android.permission.ACCESS_COARSE_LOCATION', + 'android.permission.ACCESS_FINE_LOCATION', + 'android.permission.FOREGROUND_SERVICE', + ], + package: packageName, + }, + experiments: { + typedRoutes: true, + tsconfigPaths: true, + }, + extra: { + router: { + origin: false, + }, + eas: { + projectId: '9d90a8bf-a538-49f0-925c-afd83fd4c8d3', + }, + }, + owner: 'malik2', + }, +}; diff --git a/src/app/index.tsx b/src/app/index.tsx index 5100116..948181b 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -58,10 +58,11 @@ const HomeScreen = () => { const onIntentionPress = async () => { const now = new Date(); const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - const startTime = oneDayAgo.toISOString(); + const startTime = oneDayAgo.toISOString(); // TODO last activity time const endTime = now.toISOString(); await getActivityData({ startTime, endTime }); }; + return ( diff --git a/src/types/UserConfig.ts b/src/types/UserConfig.ts index 6cace82..614870f 100644 --- a/src/types/UserConfig.ts +++ b/src/types/UserConfig.ts @@ -43,8 +43,8 @@ export type GameWidgetIds = | 'stat-stength' | 'stat-stamina' | 'stat-spirit'; -// player action portals +// player action portals export type ItemWidgetIds = | 'maliks-majik-leaderboard' // identity diff --git a/src/utils/api.ts b/src/utils/api.ts index 3f87131..4bca147 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,5 +1,6 @@ import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; import { getAppConfig } from './config'; +import { getSpellBook } from './zkpid'; // TODO persist cache to local storage for better offline use once internet connection lost? // https://www.apollographql.com/docs/react/caching/advanced-topics#persisting-the-cache @@ -17,22 +18,28 @@ export const getGqlClient = () => version: '0.0.1', })); -export const qu = (query: string) => (variables: object) => - // strip /n and /t to prevent weird error converting to byte array on server side on ecrecvoer +export const qu = (query: string) => async (variables: object) => getGqlClient().query({ + // strip /n and /t to prevent weird error converting to byte array on server side on ecrecvoer query: gql` ${query.replace(/[\n\t]/g, ' ')} `, - variables, + variables: { + ...variables, + verification: (await getSpellBook()).signMessage(query), + }, fetchPolicy: 'cache-first', // TODO add useCache: boolean to switch between query vs readQuery? }); -export const mu = (mutation: string) => (variables: object) => +export const mu = (mutation: string) => async (variables: object) => getGqlClient().mutate({ mutation: gql` - ${mutation} + ${mutation.replace(/[\n\t]/g, ' ')} `, - variables, + variables: { + ...variables, + verification: (await getSpellBook()).signMessage(mutation), + }, optimisticResponse: true, }); @@ -52,3 +59,21 @@ export const MU_ACTIVATE_JINNI = ` } } `; + +export const MU_SUBMIT_DATA = ` + mutation submit_data( + $verification: SignedRequest! + $data: [RawInputData]! + $data_provider: DataProvider! + $name: String! + ) { + submit_data( + verification: $verification, + data: $data + data_provider: $data_provider + name: $name + ) { + ID + } + } +`;