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..1d62660 --- /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: jest.fn(), + getSdkStatus: jest.fn(), + 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/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/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/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/setupTests.js b/setupTests.js new file mode 100644 index 0000000..4cead25 --- /dev/null +++ b/setupTests.js @@ -0,0 +1,2 @@ +jest.mock('sentry-expo'); +jest.mock('@segment/analytics-react-native'); 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/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/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 + } + } +`; 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..61e8ad4 --- /dev/null +++ b/src/utils/inventory/__tests__/android-health-connect.test.ts @@ -0,0 +1,130 @@ +import item from '../android-health-connect'; +import mockHealth from 'react-native-health-connect'; + +jest.mock('react-native-health-connect'); + +beforeEach(() => { + mockHealth.initialize.mockResolvedValue(true); + mockHealth.getSdkStatus.mockResolvedValue(3); +}); + +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 () => { + 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', () => { + // 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.mockResolvedValue(['Steps']); + mockHealth.getGrantedPermissions.mockResolvedValue(['Steps']); + expect(await item.initPermissions()).toBe(true); + }); + + it('should return false if permissions are not granted', async () => { + mockHealth.requestPermission.mockResolvedValue([]); + mockHealth.getGrantedPermissions.mockResolvedValue([]); + expect(await item.initPermissions()).toBe(false); + }); +}); + +describe('getPermissions', () => { + // 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.mockImplementation(mockResolvedValue(item.permissions); + mockHealth.getGrantedPermissions.mockResolvedValue([]); + expect(await item.getPermissions()).toBe(false); + }); + + it('should return true if permissions are granted', async () => { + // mockHealth.requestPermission.mockImplementation(mockResolvedValue(item.permissions); + mockHealth.getGrantedPermissions.mockResolvedValue(item.permissions); + expect(await item.getPermissions()).toBe(true); + }); +}); + +describe('equip', () => { + // 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 () => { + mockHealth.requestPermission.mockResolvedValue([]); + 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.ts b/src/utils/inventory/android-health-connect.ts index dfa3d85..d75aca7 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, @@ -30,9 +29,10 @@ import { GetHealthDataProps, QueryAndroidHealthDataProps, } from 'types/HealthData'; +import { JsonMap } from '@segment/analytics-react-native'; const ITEM_ID = 'AndroidHealthConnect'; -const ANDROID_HEALTH_PERMISSIONS = [ +const PERMISSIONS = [ // summaries { accessType: 'read', recordType: 'Steps' }, { accessType: 'read', recordType: 'Distance' }, @@ -52,13 +52,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; } @@ -72,30 +77,22 @@ 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) { - console.log('Inv:AndroidHealthConnect:checkElig: ', e); - getSentry()?.captureException(e); - return false; - } + if (!(await checkEligibility())) return false; try { 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; } }; @@ -103,31 +100,34 @@ const getPermissions = async () => { 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)) 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; - } 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(); - - return true; - } catch (e) { + return await initPermissions(); + // TODO return array of string for permissions granted + } catch (e: unknown) { console.log('Error requesting permissions: ', e); - getSentry()?.captureException(e); + debug(e); return false; } }; @@ -170,14 +170,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 +207,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,21 @@ 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) => + // 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'; 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.yaml b/sweep.yml similarity index 87% rename from sweep.yaml rename to sweep.yml index 988dc3d..f0d91ed 100644 --- a/sweep.yaml +++ b/sweep.yml @@ -3,11 +3,12 @@ # 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' @@ -23,6 +24,7 @@ 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 @@ -39,6 +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 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. @@ -49,4 +52,4 @@ sandbox: check: - trunk fmt {file_path} || return 0 - npm test:ci - - trunk check --fix --print-failures {file_path} + - trunk check --fix --print-failures {file_path} \ No newline at end of file