diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..1ae6dfb --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,62 @@ +module.exports = { + parser: '@typescript-eslint/parser', + env: { + browser: true, + es6: true, + jest: true, + node: true, + 'react-native/react-native': true + }, + extends: [ + 'plugin:react/recommended', + 'airbnb', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'prettier' + ], + plugins: ['react', 'react-native', '@typescript-eslint', 'prettier'], + ignorePatterns: ['node_modules/'], + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + rules: { + semi: [2, 'never'], + 'comma-dangle': 'off', + 'max-len': ['error', { ignoreComments: true, code: 120, ignoreStrings: true }], + 'prettier/prettier': ['error'], + 'import/no-unresolved': 'off', + 'import/prefer-default-export': 'off', + 'import/extensions': 'off', + 'no-use-before-define': 'off', + 'import/no-extraneous-dependencies': 'off', // FIXME: exclude test files + 'react/prop-types': 'off', + 'react/jsx-filename-extension': 'off', + 'react/jsx-one-expression-per-line': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react-native/no-unused-styles': 'error', + 'react-native/split-platform-components': 'error', + 'react/jsx-closing-bracket-location': 'after-props', + 'react-native/no-inline-styles': 'error', + 'react-native/no-color-literals': 'error', + 'react/jsx-closing-bracket-location': 'off', + 'react/require-default-props': 'off', + 'react-native/no-raw-text': 'off', // This does not currently work with styled components + 'react-native/no-single-element-style-arrays': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + + '@typescript-eslint/member-delimiter-style': ['error', { multiline: { delimiter: 'none' } }], + 'react/jsx-wrap-multilines': ['error', { declaration: false, assignment: false }] + }, + settings: { + 'import/resolver': { + 'babel-module': {} + }, + react: { + version: 'detect' + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d42ff18 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4914b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +ios/builds/ +ios/build/ +ios/Vendor +ios/Pods +ios/sentry.properties +ios/Intercom.framework +ios/fastlane/ +ios/GoogleService-Info +ios/AppCenter-Config.plist + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +android/app/src/main/res/raw/ +raw +android/keystores/ +android/app/src/main/res +android/fastlane +android/app/src/main/assets/ +android/app/src/main/resources/ +android/app/client_secret.json +android/fastlane/ + + +# fastlane specific +fastlane/report.xml + + +# deliver temporary files +fastlane/Preview.html + +# snapshot generated screenshots +fastlane/screenshots + +# scan temporary files +fastlane/test_output + +# Fastlane builds +builds/* + +# node.js +# +node_modules/ +.jest/ +npm-debug.log +yarn-error.log +coverage + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +ios/Pods/ + +#amplify +amplify/\#current-cloud-backend +amplify/.config/local-* +amplify/mock-data +amplify/backend/amplify-meta.json +amplify/backend/awscloudformation +amplify/team-provider-info.json +build/ +dist/ +aws-exports.js +awsconfiguration.json +amplifyconfiguration.json +amplify-gradle-config.json +amplifyxc.config +amplify/backend + +.secrets +.env +sentry.properties +android/.project +android/debug.keystore +android/.settings/org.eclipse.buildship.core.prefs +ios/Nyxo/GoogleService-Info.plist +android/app/google-services.json +android/app/.settings/org.eclipse.jdt.core.prefs +android/app/.settings/org.eclipse.buildship.core.prefs +ios/Nyxo/AppCenter-Config.plist +rnuc.xcconfig +android/app/.project diff --git a/.graphqlconfig.yml b/.graphqlconfig.yml new file mode 100644 index 0000000..c614e45 --- /dev/null +++ b/.graphqlconfig.yml @@ -0,0 +1,18 @@ +projects: + nyxoDev: + schemaPath: amplify/backend/api/nyxoDev/build/schema.graphql + includes: + - src/graphql/**/*.ts + excludes: + - ./amplify/** + extensions: + amplify: + codeGenTarget: typescript + generatedFileName: src/API.ts + docsFilePath: src/graphql + region: eu-central-1 + apiId: null + maxDepth: 2 +extensions: + amplify: + version: 3 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..3f7a5b4 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + trailingComma: 'none', + tabWidth: 2, + singleQuote: true, + printWidth: 80, + semi: false, + jsxBracketSameLine: true +} diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2e92df --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Nyxo App + +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) + +## + +## Getting started + +_Clone repository_ + +```shell +git clone +cd nyxo-app +yarn +``` + +### Setting up enviroment variables + +Nyxo configurations keys are placed in config.ts file, which then references the requirement enviroment variables from local `.env`file. + +## Troubleshooting + +#### main.jsbundle missing + +Run command `react-native bundle --entry-file index.js --platform ios --dev=false --bundle-output ios/main.jsbundle --assets-dest ios` for iOS +Run command `react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/` for Android + +## When you get a weird Xcode error about undefined symbols + +If you see something like this: + +`Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_RCTReconnectingWebSocket", referenced from: objc-class-ref in libReact.a(RCTPackagerConnection.o) ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)` + +Delete your Derived data + +## Resetting bundlers etc. + +1. Clear watchman watches: `watchman watch-del-all`. +2. Delete the `node_modules` folder: `rm -rf node_modules && npm install`. +3. Reset Metro Bundler cache: `rm -rf /tmp/metro-bundler-cache-*` or `npm start -- --reset-cache`. +4. Remove haste cache: `rm -rf /tmp/haste-map-react-native-packager-*`. diff --git a/__mocks__/@react-native-community/push-notification-ios.ts b/__mocks__/@react-native-community/push-notification-ios.ts new file mode 100644 index 0000000..01f9cd5 --- /dev/null +++ b/__mocks__/@react-native-community/push-notification-ios.ts @@ -0,0 +1,7 @@ +jest.mock('@react-native-community/push-notification-ios', () => ({ + configure: jest.fn(), + onRegister: jest.fn(), + onNotification: jest.fn(), + addEventListener: jest.fn(), + requestPermissions: jest.fn() +})) diff --git a/__mocks__/@sentry/react-native.ts b/__mocks__/@sentry/react-native.ts new file mode 100644 index 0000000..394b242 --- /dev/null +++ b/__mocks__/@sentry/react-native.ts @@ -0,0 +1,5 @@ +jest.mock('@sentry/react-native', () => ({ + setTagsContext: jest.fn(), + setExtraContext: jest.fn(), + captureBreadcrumb: jest.fn() +})) diff --git a/__mocks__/appcenter-analytics.ts b/__mocks__/appcenter-analytics.ts new file mode 100644 index 0000000..585e4cc --- /dev/null +++ b/__mocks__/appcenter-analytics.ts @@ -0,0 +1,6 @@ +jest.mock('appcenter-analytics', () => ({ + trackEvent: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(), + configure: jest.fn() +})) diff --git a/__mocks__/appcenter-push.ts b/__mocks__/appcenter-push.ts new file mode 100644 index 0000000..43a147f --- /dev/null +++ b/__mocks__/appcenter-push.ts @@ -0,0 +1,6 @@ +jest.mock('appcenter-push', () => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(), + configure: jest.fn() +})) diff --git a/__mocks__/aws-amplify.ts b/__mocks__/aws-amplify.ts new file mode 100644 index 0000000..624d01f --- /dev/null +++ b/__mocks__/aws-amplify.ts @@ -0,0 +1,5 @@ +export const Auth = { + currentSession: jest.fn(() => Promise.resolve()), + signIn: jest.fn(() => Promise.resolve()), + signOut: jest.fn(() => Promise.resolve()) +} diff --git a/__mocks__/chroma-js.ts b/__mocks__/chroma-js.ts new file mode 100644 index 0000000..4df3a48 --- /dev/null +++ b/__mocks__/chroma-js.ts @@ -0,0 +1,17 @@ +// jest.mock('chroma-js', () => ({ +// default: () => {}, +// })); + +// jest.mock('chroma-js', () => ({ +// addEventListener: jest.fn(), +// removeEventListener: jest.fn(), +// requestPermissions: jest.fn(), +// default: jest.fn(), +// hex: jest.fn(), +// alpha: jest.fn(), +// })); + +// @ts-ignore +const chroma = require('chroma-js').default + +module.exports = chroma diff --git a/__mocks__/i18n-js.ts b/__mocks__/i18n-js.ts new file mode 100644 index 0000000..9a246be --- /dev/null +++ b/__mocks__/i18n-js.ts @@ -0,0 +1,17 @@ +// jest.mock('i18n-js', () => ({ +// I18n: { +// locale: {}, +// fallbacks: true, +// translations: {}, +// currentLocale: () => {}, +// }, +// })); + +// jest.mock('i18n-js', () => ({ +// currentLocale: jest.fn(() => 'en'), +// })); + +const I18n = require('i18n-js') + +jest.genMockFromModule('i18n-js') +module.exports = I18n diff --git a/__mocks__/moment.ts b/__mocks__/moment.ts new file mode 100644 index 0000000..b52ee32 --- /dev/null +++ b/__mocks__/moment.ts @@ -0,0 +1,5 @@ +const moment = jest.requireActual('moment') + +export default (timestamp: string | 0 = 0) => { + return moment(timestamp) +} diff --git a/__mocks__/prop-types.ts b/__mocks__/prop-types.ts new file mode 100644 index 0000000..b999b1c --- /dev/null +++ b/__mocks__/prop-types.ts @@ -0,0 +1,5 @@ +jest.mock('prop-types', () => ({ + PropTypes: { + node: {} + } +})) diff --git a/__mocks__/react-native-app-auth.ts b/__mocks__/react-native-app-auth.ts new file mode 100644 index 0000000..1e26378 --- /dev/null +++ b/__mocks__/react-native-app-auth.ts @@ -0,0 +1,6 @@ +jest.mock('react-native-app-auth', () => ({ + authorize: jest.fn(), + register: jest.fn(), + revoke: jest.fn(), + refresh: jest.fn() +})) diff --git a/__mocks__/react-native-code-push.ts b/__mocks__/react-native-code-push.ts new file mode 100644 index 0000000..7e023c3 --- /dev/null +++ b/__mocks__/react-native-code-push.ts @@ -0,0 +1,8 @@ +const codePush = { + InstallMode: { ON_NEXT_RESTART: 'ON_APP_RESTART' }, + CheckFrequency: { ON_APP_RESUME: 'ON_APP_RESUME' } +} + +const cb = (_: any) => (app: any) => app +Object.assign(cb, codePush) +export default cb diff --git a/__mocks__/react-native-firebase.ts b/__mocks__/react-native-firebase.ts new file mode 100644 index 0000000..ac020ad --- /dev/null +++ b/__mocks__/react-native-firebase.ts @@ -0,0 +1,28 @@ +const firebase = { + messaging: jest.fn(() => ({ + hasPermission: jest.fn(() => Promise.resolve(true)), + subscribeToTopic: jest.fn(), + unsubscribeFromTopic: jest.fn(), + requestPermission: jest.fn(() => Promise.resolve(true)), + getToken: jest.fn(() => Promise.resolve('myMockToken')), + onTokenRefresh: jest.fn(() => Promise.resolve('myMockToken')) + })), + notifications: jest.fn(() => ({ + onNotification: jest.fn(), + onNotificationDisplayed: jest.fn(), + android: { + createChannel: jest.fn() + } + })) +} + +firebase.notifications.Android = { + Channel: jest.fn(() => ({ + setDescription: jest.fn() + })), + Importance: { + Max: {} + } +} + +export default firebase diff --git a/__mocks__/react-native-gesture-handler.ts b/__mocks__/react-native-gesture-handler.ts new file mode 100644 index 0000000..bc0f196 --- /dev/null +++ b/__mocks__/react-native-gesture-handler.ts @@ -0,0 +1,13 @@ +jest.mock('NativeModules', () => ({ + UIManager: { + RCTView: () => {} + }, + RNGestureHandlerModule: { + attachGestureHandler: jest.fn(), + createGestureHandler: jest.fn(), + dropGestureHandler: jest.fn(), + updateGestureHandler: jest.fn(), + State: {}, + Directions: {} + } +})) diff --git a/__mocks__/react-native-get-random-values.ts b/__mocks__/react-native-get-random-values.ts new file mode 100644 index 0000000..51d672d --- /dev/null +++ b/__mocks__/react-native-get-random-values.ts @@ -0,0 +1,3 @@ +jest.mock('react-native-get-random-values', () => ({ + RNGetRandomValues: jest.fn() +})) diff --git a/__mocks__/react-native-healthkit.ts b/__mocks__/react-native-healthkit.ts new file mode 100644 index 0000000..bca6997 --- /dev/null +++ b/__mocks__/react-native-healthkit.ts @@ -0,0 +1,14 @@ +import { AppleHealthKit } from 'react-native-healthkit' + +jest.mock('react-native-healthkit', () => ({ + Constants: { + Permissions: {} + }, + AppleHealthKit: { + Constants: { + Permissions: {} + } + } +})) + +export default AppleHealthKit diff --git a/__mocks__/react-native-iap.ts b/__mocks__/react-native-iap.ts new file mode 100644 index 0000000..c94b422 --- /dev/null +++ b/__mocks__/react-native-iap.ts @@ -0,0 +1,6 @@ +jest.mock('react-native-iap', () => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(), + configure: jest.fn() +})) diff --git a/__mocks__/react-native-intercom.ts b/__mocks__/react-native-intercom.ts new file mode 100644 index 0000000..092e7df --- /dev/null +++ b/__mocks__/react-native-intercom.ts @@ -0,0 +1,6 @@ +jest.mock('react-native-intercom', () => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(), + configure: jest.fn() +})) diff --git a/__mocks__/react-native-iphone-x-helper.ts b/__mocks__/react-native-iphone-x-helper.ts new file mode 100644 index 0000000..73676f7 --- /dev/null +++ b/__mocks__/react-native-iphone-x-helper.ts @@ -0,0 +1,4 @@ +jest.mock('react-native-iphone-x-helper', () => ({ + getStatusBarHeight: jest.fn(), + isIphoneX: () => true +})) diff --git a/__mocks__/react-native-localize.ts b/__mocks__/react-native-localize.ts new file mode 100644 index 0000000..f0c9315 --- /dev/null +++ b/__mocks__/react-native-localize.ts @@ -0,0 +1,44 @@ +// __mocks__/react-native-localize.js + +const getLocales = () => [ + // you can choose / add the locales you want + { countryCode: 'US', languageTag: 'en-US', languageCode: 'en', isRTL: false }, + { countryCode: 'FR', languageTag: 'fr-FR', languageCode: 'fr', isRTL: false } +] + +// use a provided translation, or return undefined to test your fallback +const findBestAvailableLanguage = () => ({ + languageTag: 'en-US', + isRTL: false +}) + +const getNumberFormatSettings = () => ({ + decimalSeparator: '.', + groupingSeparator: ',' +}) + +const getCalendar = () => 'gregorian' // or "japanese", "buddhist" +const getCountry = () => 'US' // the country code you want +const getCurrencies = () => ['USD', 'EUR'] // can be empty array +const getTemperatureUnit = () => 'celsius' // or "fahrenheit" +const getTimeZone = () => 'Europe/Paris' // the timezone you want +const uses24HourClock = () => true +const usesMetricSystem = () => true + +const addEventListener = jest.fn() +const removeEventListener = jest.fn() + +export { + findBestAvailableLanguage, + getLocales, + getNumberFormatSettings, + getCalendar, + getCountry, + getCurrencies, + getTemperatureUnit, + getTimeZone, + uses24HourClock, + usesMetricSystem, + addEventListener, + removeEventListener +} diff --git a/__mocks__/react-native-purchases.ts b/__mocks__/react-native-purchases.ts new file mode 100644 index 0000000..5855eb8 --- /dev/null +++ b/__mocks__/react-native-purchases.ts @@ -0,0 +1,31 @@ +jest.mock('react-native-purchases', () => ({ + setupPurchases: jest.fn(), + setAllowSharingStoreAccount: jest.fn(), + addAttributionData: jest.fn(), + getOfferings: jest.fn(), + getProductInfo: jest.fn(), + makePurchase: jest.fn(), + restoreTransactions: jest.fn(), + getAppUserID: jest.fn(), + createAlias: jest.fn(), + identify: jest.fn(), + setDebugLogsEnabled: jest.fn(), + getPurchaserInfo: jest.fn(), + reset: jest.fn(), + syncPurchases: jest.fn(), + setFinishTransactions: jest.fn(), + purchaseProduct: jest.fn(), + purchasePackage: jest.fn(), + isAnonymous: jest.fn(), + makeDeferredPurchase: jest.fn(), + checkTrialOrIntroductoryPriceEligibility: jest.fn(), + purchaseDiscountedPackage: jest.fn(), + purchaseDiscountedProduct: jest.fn(), + getPaymentDiscount: jest.fn(), + invalidatePurchaserInfoCache: jest.fn(), + setAttributes: jest.fn(), + setEmail: jest.fn(), + setPhoneNumber: jest.fn(), + setDisplayName: jest.fn(), + setPushToken: jest.fn() +})) diff --git a/__mocks__/react-native-reanimated.ts b/__mocks__/react-native-reanimated.ts new file mode 100644 index 0000000..aaba6be --- /dev/null +++ b/__mocks__/react-native-reanimated.ts @@ -0,0 +1,3 @@ +jest.mock('react-native-reanimated', () => + require('react-native-reanimated/mock') +) diff --git a/__mocks__/react-native-splash-screen.ts b/__mocks__/react-native-splash-screen.ts new file mode 100644 index 0000000..a2bdb52 --- /dev/null +++ b/__mocks__/react-native-splash-screen.ts @@ -0,0 +1,5 @@ +// __mocks__/react-native-splash-screen.ts +export default { + show: jest.fn().mockImplementation(() => {}), + hide: jest.fn().mockImplementation(() => {}) +} diff --git a/__mocks__/react-native-svg.ts b/__mocks__/react-native-svg.ts new file mode 100644 index 0000000..ed1f6ec --- /dev/null +++ b/__mocks__/react-native-svg.ts @@ -0,0 +1,61 @@ +// https://github.com/FormidableLabs/react-native-svg-mock +import * as React from 'react' + +const createComponent = function (name: string) { + return class extends React.Component { + // overwrite the displayName, since this is a class created dynamically + static displayName = name + + render() { + return React.createElement(name, this.props, this.props.children) + } + } +} + +// Mock all react-native-svg exports +// from https://github.com/magicismight/react-native-svg/blob/master/index.js +const Svg = createComponent('Svg') +const Circle = createComponent('Circle') +const Ellipse = createComponent('Ellipse') +const G = createComponent('G') +const Text = createComponent('Text') +const TextPath = createComponent('TextPath') +const TSpan = createComponent('TSpan') +const Path = createComponent('Path') +const Polygon = createComponent('Polygon') +const Polyline = createComponent('Polyline') +const Line = createComponent('Line') +const Rect = createComponent('Rect') +const Use = createComponent('Use') +const Image = createComponent('Image') +const Symbol = createComponent('Symbol') +const Defs = createComponent('Defs') +const LinearGradient = createComponent('LinearGradient') +const RadialGradient = createComponent('RadialGradient') +const Stop = createComponent('Stop') +const ClipPath = createComponent('ClipPath') + +export { + Svg, + Circle, + Ellipse, + G, + Text, + TextPath, + TSpan, + Path, + Polygon, + Polyline, + Line, + Rect, + Use, + Image, + Symbol, + Defs, + LinearGradient, + RadialGradient, + Stop, + ClipPath +} + +export default Svg diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts new file mode 100644 index 0000000..5f1c6b4 --- /dev/null +++ b/__mocks__/react-native.ts @@ -0,0 +1,47 @@ +jest.mock('react-native', () => ({ + StyleSheet: { + hairlineWidth: 1, + create: () => ({}), + flatten(arr: any) { + return arr.reduce((res: any, item: any) => Object.assign(res, item), {}) + } + }, + Platform: { + OS: jest.fn(() => 'android'), + version: jest.fn(() => 25) + }, + Dimensions: { + get: () => { + return { width: 100, height: 200 } + } + }, + I18nManager: { + isRTL: false + }, + NativeModules: { + RNDocumentPicker: () => {}, + RNSentry: () => jest.fn() + }, + + Easing: { + bezier: () => {} + }, + View: () => 'View', + ViewPropTypes: { + propTypes: { + style: {} + } + }, + Text: () => 'Text', + TouchableNativeFeedback: () => 'TouchableNativeFeedback', + TouchableOpacity: () => 'TouchableOpacity', + + TouchableWithoutFeedback: () => 'TouchableWithoutFeedback', + Animated: { + View: () => 'Animated.View', + interpolate: jest.fn(), + Value: jest.fn().mockImplementation(() => { + return { interpolate: jest.fn() } + }) + } +})) diff --git a/__mocks__/react-redux.ts b/__mocks__/react-redux.ts new file mode 100644 index 0000000..092e7df --- /dev/null +++ b/__mocks__/react-redux.ts @@ -0,0 +1,6 @@ +jest.mock('react-native-intercom', () => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(), + configure: jest.fn() +})) diff --git a/amplify/.config/project-config.json b/amplify/.config/project-config.json new file mode 100644 index 0000000..4e397ac --- /dev/null +++ b/amplify/.config/project-config.json @@ -0,0 +1,17 @@ +{ + "providers": [ + "awscloudformation" + ], + "projectName": "Nyxo-Cloud", + "version": "3.0", + "frontend": "javascript", + "javascript": { + "framework": "react-native", + "config": { + "SourceDir": "/", + "DistributionDir": "/", + "BuildCommand": "npm run-script build", + "StartCommand": "" + } + } +} \ No newline at end of file diff --git a/android/Gemfile b/android/Gemfile new file mode 100644 index 0000000..cdd3a6b --- /dev/null +++ b/android/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "fastlane" + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/android/Gemfile.lock b/android/Gemfile.lock new file mode 100644 index 0000000..bb42eb9 --- /dev/null +++ b/android/Gemfile.lock @@ -0,0 +1,181 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.332.0) + aws-sdk-core (3.100.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.34.1) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.69.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.3) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.10) + declarative-option (0.1.0) + digest-crc (0.5.1) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.5) + emoji_regex (1.0.1) + excon (0.75.0) + faraday (1.0.1) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.1.7) + fastlane (2.149.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.2, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 2.0) + excon (>= 0.71.0, < 1.0.0) + faraday (>= 0.17, < 2.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (>= 0.13.1, < 2.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (~> 2.1.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_xml (~> 0.5) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + public_suffix (~> 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-increment_version_code (0.4.3) + gh_inspector (1.1.3) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-cloud-core (1.5.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.3.2) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.26.2) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-api-client (~> 0.33) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.13.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.3.0) + jwt (2.1.0) + memoist (0.16.2) + mini_magick (4.10.1) + mini_mime (1.0.2) + multi_json (1.14.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nanaimo (0.2.6) + naturally (2.2.0) + os (1.1.0) + plist (3.5.0) + public_suffix (2.0.5) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rouge (2.0.7) + rubyzip (1.3.0) + security (0.1.3) + signet (0.14.0) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.0) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.16.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.0) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + fastlane-plugin-increment_version_code + +BUNDLED WITH + 2.1.4 diff --git a/android/app/.classpath b/android/app/.classpath new file mode 100644 index 0000000..32d6691 --- /dev/null +++ b/android/app/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/_BUCK b/android/app/_BUCK new file mode 100644 index 0000000..473c1dc --- /dev/null +++ b/android/app/_BUCK @@ -0,0 +1,55 @@ +# To learn about Buck see [Docs](https://buckbuild.com/). +# To run your application with Buck: +# - install Buck +# - `npm start` - to start the packager +# - `cd android` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` +# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck +# - `buck install -r android/app` - compile, install and run application +# + +load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") + +lib_deps = [] + +create_aar_targets(glob(["libs/*.aar"])) + +create_jar_targets(glob(["libs/*.jar"])) + +android_library( + name = "all-libs", + exported_deps = lib_deps, +) + +android_library( + name = "app-code", + srcs = glob([ + "src/main/java/**/*.java", + ]), + deps = [ + ":all-libs", + ":build_config", + ":res", + ], +) + +android_build_config( + name = "build_config", + package = "fi.nyxo.app", +) + +android_resource( + name = "res", + package = "fi.nyxo.app", + res = "src/main/res", +) + +android_binary( + name = "app", + keystore = "//android/keystores:debug", + manifest = "src/main/AndroidManifest.xml", + package_type = "debug", + deps = [ + ":app-code", + ], +) diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..fcef3c8 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,271 @@ +apply plugin: "com.android.application" +apply from: '../../node_modules/react-native-unimodules/gradle.groovy' +import com.android.build.OutputFile + +// Load keystore +def keystorePropertiesFile = rootProject.file("keystores/release.keystore.properties"); +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // whether to disable dev mode in custom build variants (by default only disabled in release) + * // for example: to disable dev mode in the staging build type (if configured) + * devDisabledInStaging: true, + * // The configuration property can be in the following formats + * // 'devDisabledIn${productFlavor}${buildType}' + * // 'devDisabledIn${buildType}' + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"], + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +project.ext.react = [ + entryFile: "index.js", + enableHermes: true, + + bundleInDebug: project.hasProperty("bundleInDebug") ? project.getProperty("bundleInDebug") : false, + + // If you use build variants it has to be like this - put your own names in there + bundleInRelease: project.hasProperty("bundleInRelease") ? project.getProperty("bundleInRelease") : false +] +apply from: "../../node_modules/react-native/react.gradle" +apply from: "../../node_modules/react-native-ultimate-config/android/rnuc.gradle" + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore. + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +/** + * Whether to enable the Hermes VM. + * + * This should be set on project.ext.react and mirrored here. If it is not set + * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode + * and the benefits of using Hermes will therefore be sharply reduced. + */ +def enableHermes = project.ext.react.get("enableHermes", true); + + + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { + applicationId "fi.nyxo.app" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 53 + versionName "1.4.0" + multiDexEnabled true + manifestPlaceholders = [ + appAuthRedirectScheme: 'fi.nyxo.app' + ] + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86", "arm64-v8a" + } + } + signingConfigs { + debug { + storeFile file(keystoreProperties['NYXO_UPLOAD_STORE_FILE']) + storePassword keystoreProperties['NYXO_UPLOAD_STORE_PASSWORD'] + keyAlias keystoreProperties['MYAPP_RELEASE_KEY_ALIAS'] + keyPassword keystoreProperties['NYXO_UPLOAD_KEY_ALIAS'] + } + release { + storeFile file(keystoreProperties['NYXO_UPLOAD_STORE_FILE']) + storePassword keystoreProperties['NYXO_UPLOAD_STORE_PASSWORD'] + keyAlias keystoreProperties['MYAPP_RELEASE_KEY_ALIAS'] + keyPassword keystoreProperties['NYXO_UPLOAD_KEY_ALIAS'] + } + } + buildTypes { + debug { + buildConfigField "String", "CODEPUSH_KEY", '"Y26psKSBIfzb2ta6JgrEC_ZXlxeIHJ4jvmmMB"' + signingConfig signingConfigs.debug + } + release { + buildConfigField "String", "CODEPUSH_KEY", '"Y26psKSBIfzb2ta6JgrEC_ZXlxeIHJ4jvmmMB"' + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + signingConfig signingConfigs.release + + } + } + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + } + } + + packagingOptions { + pickFirst "lib/armeabi-v7a/libc++_shared.so" + pickFirst "lib/arm64-v8a/libc++_shared.so" + pickFirst "lib/x86/libc++_shared.so" + pickFirst "lib/x86_64/libc++_shared.so" + } +} + +dependencies { + // implementation "org.webkit:android-jsc:r241213" + // implementation project(':amazon-cognito-identity-js') + // implementation project(':react-native-screens') + implementation fileTree(dir: "libs", include: ["*.jar"]) + addUnimodulesDependencies([exclude: ['expo-face-detector']]) + + implementation fileTree(dir: "libs", include: ["*.jar"]) + // implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0-rc01' + implementation "com.facebook.react:react-native:+" // From node_modules + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + implementation project(':react-native-background-fetch') + implementation project(':appcenter') + implementation project(':appcenter-analytics') + implementation project(':appcenter-crashes') + implementation 'com.google.firebase:firebase-core:17.0.0' + implementation project(':react-native-firebase') + implementation 'com.google.firebase:firebase-messaging:9.2.1' + implementation 'io.intercom.android:intercom-sdk-base:5.+' + implementation ("com.google.android.gms:play-services-fitness:+") + implementation ("com.google.android.gms:play-services-auth:+") + implementation project(':react-native-google-fit') + implementation project(':@sentry') + implementation (project(':react-native-google-fit'), { + exclude group: "com.google.android.gms" + }) + implementation "com.google.firebase:firebase-messaging:18.0.0" + implementation 'me.leolin:ShortcutBadger:1.1.21@aar' // <-- Add this line if you wish to use badge on Android + + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + } + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + } + + + // required if your app is in the Google Play Store (tip: avoid using bundled play services libs) + implementation 'com.google.firebase:firebase-appindexing:19.0.0' // App indexing + implementation 'com.google.android.gms:play-services-ads:16+' // GAID matching + + + if (enableHermes) { + def hermesPath = "../../node_modules/hermes-engine/android/"; + debugImplementation files(hermesPath + "hermes-debug.aar") + releaseImplementation files(hermesPath + "hermes-release.aar") + } else { + implementation jscFlavor + } +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) +apply plugin: 'com.google.gms.google-services' +com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true // takes care of bug between Codepush and Firebase + diff --git a/android/app/build_defs.bzl b/android/app/build_defs.bzl new file mode 100644 index 0000000..fff270f --- /dev/null +++ b/android/app/build_defs.bzl @@ -0,0 +1,19 @@ +"""Helper definitions to glob .aar and .jar targets""" + +def create_aar_targets(aarfiles): + for aarfile in aarfiles: + name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] + lib_deps.append(":" + name) + android_prebuilt_aar( + name = name, + aar = aarfile, + ) + +def create_jar_targets(jarfiles): + for jarfile in jarfiles: + name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] + lib_deps.append(":" + name) + prebuilt_jar( + name = name, + binary_jar = jarfile, + ) diff --git a/android/app/debug/AndroidManifest.xml b/android/app/debug/AndroidManifest.xml new file mode 100644 index 0000000..7feb33d --- /dev/null +++ b/android/app/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/debug/java/fi/nyxo/app/ReactNativeFlipper.java b/android/app/debug/java/fi/nyxo/app/ReactNativeFlipper.java new file mode 100644 index 0000000..011978c --- /dev/null +++ b/android/app/debug/java/fi/nyxo/app/ReactNativeFlipper.java @@ -0,0 +1,66 @@ +package fi.nyxo.app; + +import android.content.Context; +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; +import com.facebook.flipper.plugins.react.ReactFlipperPlugin; +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.network.NetworkingModule; +import okhttp3.OkHttpClient; + +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + if (FlipperUtils.shouldEnableFlipper(context)) { + final FlipperClient client = AndroidFlipperClient.getInstance(context); + + client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); + client.addPlugin(new ReactFlipperPlugin()); + client.addPlugin(new DatabasesFlipperPlugin(context)); + client.addPlugin(new SharedPreferencesFlipperPlugin(context)); + client.addPlugin(CrashReporterPlugin.getInstance()); + + NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); + } + }); + client.addPlugin(networkFlipperPlugin); + client.start(); + + // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized + // Hence we run if after all native modules have been initialized + ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); + if (reactContext == null) { + reactInstanceManager.addReactInstanceEventListener( + new ReactInstanceManager.ReactInstanceEventListener() { + @Override + public void onReactContextInitialized(ReactContext reactContext) { + reactInstanceManager.removeReactInstanceEventListener(this); + reactContext.runOnNativeModulesQueueThread( + new Runnable() { + @Override + public void run() { + client.addPlugin(new FrescoFlipperPlugin()); + } + }); + } + }); + } else { + client.addPlugin(new FrescoFlipperPlugin()); + } + } + } +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..f8378ee --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,26 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +-keep public class com.dylanvann.fastimage.* {*;} +-keep public class com.dylanvann.fastimage.** {*;} +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..51908f6 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/fi/nyxo/app/MainActivity.java b/android/app/src/main/java/fi/nyxo/app/MainActivity.java new file mode 100644 index 0000000..b6fab5c --- /dev/null +++ b/android/app/src/main/java/fi/nyxo/app/MainActivity.java @@ -0,0 +1,37 @@ +package fi.nyxo.app; + +import android.os.Bundle; +import com.facebook.react.ReactFragmentActivity; +import org.devio.rn.splashscreen.SplashScreen; +import com.facebook.react.ReactActivityDelegate; +import com.facebook.react.ReactRootView; +import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; + +public class MainActivity extends ReactFragmentActivity { + + /** + * Returns the name of the main component registered from JavaScript. This is + * used to schedule rendering of the component. + */ + + @Override + protected void onCreate(Bundle savedInstanceState) { + SplashScreen.show(this); // here + super.onCreate(null); + } + + @Override + protected String getMainComponentName() { + return "Nyxo"; + } + + @Override + protected ReactActivityDelegate createReactActivityDelegate() { + return new ReactActivityDelegate(this, getMainComponentName()) { + @Override + protected ReactRootView createRootView() { + return new RNGestureHandlerEnabledRootView(MainActivity.this); + } + }; + } +} diff --git a/android/app/src/main/java/fi/nyxo/app/MainApplication.java b/android/app/src/main/java/fi/nyxo/app/MainApplication.java new file mode 100644 index 0000000..d6ab0a2 --- /dev/null +++ b/android/app/src/main/java/fi/nyxo/app/MainApplication.java @@ -0,0 +1,117 @@ +package fi.nyxo.app; + +import android.app.Application; + +import com.facebook.react.PackageList; +import android.content.Context; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.soloader.SoLoader; +import fi.nyxo.app.generated.BasePackageList; +import io.sentry.RNSentryPackage; + +import org.unimodules.adapters.react.ModuleRegistryAdapter; +import org.unimodules.adapters.react.ReactModuleRegistryProvider; +import org.unimodules.core.interfaces.SingletonModule; +import fi.nyxo.app.R; +import com.reactnative.googlefit.GoogleFitPackage; +import io.intercom.android.sdk.Intercom; +import io.invertase.firebase.RNFirebasePackage; +import io.invertase.firebase.auth.RNFirebaseAuthPackage; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Arrays; +import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; +import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage; +import com.reactnativeultimateconfig.UltimateConfigModule; + +public class MainApplication extends Application implements ReactApplication { + private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider( + new BasePackageList().getPackageList(), Arrays.asList()); + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + + @Override + public boolean getUseDeveloperSupport() { + // return true; + return BuildConfig.DEBUG; + // return BuildConfig.DEBUG; THERES something wrong here + } + + @Override + protected List getPackages() { + + List unimodules = Arrays.asList(new ModuleRegistryAdapter(mModuleRegistryProvider)); + + @SuppressWarnings("UnnecessaryLocalVariable") + List packages = new PackageList(this).getPackages(); + // packages.add(new CodePush("Y26psKSBIfzb2ta6JgrEC_ZXlxeIHJ4jvmmMB", + // getApplicationContext(), BuildConfig.DEBUG)); + // packages.add(new RNBackgroundFetchPackage()); + packages.add(new GoogleFitPackage(BuildConfig.APPLICATION_ID)); + packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider)); + packages.add(new RNFirebaseMessagingPackage()); // <-- Add this line + packages.add(new RNFirebaseNotificationsPackage()); // <-- Add this line + // packages.add(new RNSentryPackage()); + + // packages.add(new GFPackage()); + // packages.addAll(unimodules); + + // packages.add(new IntercomPackage()); + // Packages that cannot be autolinked yet can be added manually here, for + // example: Y26psKSBIfzb2ta6JgrEC_ZXlxeIHJ4jvmmMB + // packages.add(new MyReactNativePackage()); + return packages; + + } + + @Override + protected String getJSMainModuleName() { + return "index"; + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } + + @Override + public void onCreate() { + super.onCreate(); + UltimateConfigModule.setBuildConfig(BuildConfig.class); + SoLoader.init(this, /* native exopackage */ false); + Intercom.initialize(this, BuildConfig.INTERCOM_KEY_ANDROID, BuildConfig.INTERCOM_ID); + initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + + } + + /** + * Loads Flipper in React Native templates. + * + * @param context + */ + private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + if (BuildConfig.DEBUG) { + try { + /* + * We use reflection here to pick up the class that initializes Flipper, since + * Flipper library is not available in release mode + */ + Class aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper"); + aClass.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class).invoke(null, context, + reactInstanceManager); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } +} diff --git a/android/app/src/main/java/fi/nyxo/app/MainMessagingService.java b/android/app/src/main/java/fi/nyxo/app/MainMessagingService.java new file mode 100644 index 0000000..dfb8d3d --- /dev/null +++ b/android/app/src/main/java/fi/nyxo/app/MainMessagingService.java @@ -0,0 +1,33 @@ +package fi.nyxo.app; + +import io.invertase.firebase.messaging.*; +import android.content.Intent; +import android.content.Context; +import io.intercom.android.sdk.push.IntercomPushClient; +import io.invertase.firebase.messaging.RNFirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import android.util.Log; +import java.util.Map; + +public class MainMessagingService extends RNFirebaseMessagingService { + private static final String TAG = "MainMessagingService"; + private final IntercomPushClient intercomPushClient = new IntercomPushClient(); + + @Override + public void onNewToken(String refreshedToken) { + intercomPushClient.sendTokenToIntercom(getApplication(), refreshedToken); + // DO HOST LOGIC HERE + } + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + Map message = remoteMessage.getData(); + + if (intercomPushClient.isIntercomPush(message)) { + Log.d(TAG, "Intercom message received"); + intercomPushClient.handlePush(getApplication(), message); + } else { + super.onMessageReceived(remoteMessage); + } + } +} diff --git a/android/app/src/main/java/fi/nyxo/app/generated/BasePackageList.java b/android/app/src/main/java/fi/nyxo/app/generated/BasePackageList.java new file mode 100644 index 0000000..5f05954 --- /dev/null +++ b/android/app/src/main/java/fi/nyxo/app/generated/BasePackageList.java @@ -0,0 +1,16 @@ +package fi.nyxo.app.generated; + +import java.util.Arrays; +import java.util.List; +import org.unimodules.core.interfaces.Package; + +public class BasePackageList { + public List getPackageList() { + return Arrays.asList( + new expo.modules.constants.ConstantsPackage(), + new expo.modules.filesystem.FileSystemPackage(), + new expo.modules.imageloader.ImageLoaderPackage(), + new expo.modules.permissions.PermissionsPackage() + ); + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d3644da --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,53 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "28.0.3" + minSdkVersion = 21 + compileSdkVersion = 29 + targetSdkVersion = 28 + supportLibVersion = "28.0.0" + } + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.google.gms:google-services:4.2.0' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + google() + jcenter() + maven { url 'https://www.jitpack.io' } + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } + maven { url "https://maven.google.com" } + maven { + // Local Maven repo containing AARs with JSC library built for Android + url "$rootDir/../node_modules/jsc-android/dist" + } + maven { + url "$rootDir/../node_modules/react-native-background-fetch/android/libs" + } + } +} + + +wrapper { + gradleVersion = '4.7' + distributionUrl = distributionUrl.replace("bin", "all") +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..7c4e2ca --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,28 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Version of flipper SDK to use with React Native +FLIPPER_VERSION=0.33.1 \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0fabfdd --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 13 13:43:05 EET 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..fe2fbed --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="-Xmx64m" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..bde6776 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,28 @@ +rootProject.name = 'nyxo' +apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) +apply from: '../node_modules/react-native-unimodules/gradle.groovy' +includeUnimodulesProjects() + +include ':react-native-background-fetch' +project(':react-native-background-fetch').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-fetch/android') + +include ':appcenter' +project(':appcenter').projectDir = new File(rootProject.projectDir, '../node_modules/appcenter/android') + +include ':appcenter-crashes' +project(':appcenter-crashes').projectDir = new File(rootProject.projectDir, '../node_modules/appcenter-crashes/android') + +include ':appcenter-analytics' +project(':appcenter-analytics').projectDir = new File(rootProject.projectDir, '../node_modules/appcenter-analytics/android') + +include ':react-native-google-fit' +project(':react-native-google-fit').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-fit/android') + +include ':react-native-firebase' +project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-firebase/android') + +include ':@sentry' +project(':@sentry').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android') + + +include ':app' diff --git a/assets/appIcons/fitbit-app-icon.png b/assets/appIcons/fitbit-app-icon.png new file mode 100755 index 0000000..4a8f7bf Binary files /dev/null and b/assets/appIcons/fitbit-app-icon.png differ diff --git a/assets/appIcons/google-fit-icon.png b/assets/appIcons/google-fit-icon.png new file mode 100644 index 0000000..4c8c710 Binary files /dev/null and b/assets/appIcons/google-fit-icon.png differ diff --git a/assets/appIcons/oura.jpg b/assets/appIcons/oura.jpg new file mode 100644 index 0000000..85ce81e Binary files /dev/null and b/assets/appIcons/oura.jpg differ diff --git a/assets/appIcons/withings-icon.png b/assets/appIcons/withings-icon.png new file mode 100644 index 0000000..1484daf Binary files /dev/null and b/assets/appIcons/withings-icon.png differ diff --git a/assets/fonts/Domine-Bold.ttf b/assets/fonts/Domine-Bold.ttf new file mode 100755 index 0000000..329288c Binary files /dev/null and b/assets/fonts/Domine-Bold.ttf differ diff --git a/assets/fonts/Domine-Regular.ttf b/assets/fonts/Domine-Regular.ttf new file mode 100755 index 0000000..c387575 Binary files /dev/null and b/assets/fonts/Domine-Regular.ttf differ diff --git a/assets/fonts/FontAwesome.ttf b/assets/fonts/FontAwesome.ttf new file mode 100644 index 0000000..b219a1f Binary files /dev/null and b/assets/fonts/FontAwesome.ttf differ diff --git a/assets/fonts/Montserrat-Black.ttf b/assets/fonts/Montserrat-Black.ttf new file mode 100755 index 0000000..8747a33 Binary files /dev/null and b/assets/fonts/Montserrat-Black.ttf differ diff --git a/assets/fonts/Montserrat-BlackItalic.ttf b/assets/fonts/Montserrat-BlackItalic.ttf new file mode 100755 index 0000000..5359c86 Binary files /dev/null and b/assets/fonts/Montserrat-BlackItalic.ttf differ diff --git a/assets/fonts/Montserrat-Bold.ttf b/assets/fonts/Montserrat-Bold.ttf new file mode 100755 index 0000000..1a1edbf Binary files /dev/null and b/assets/fonts/Montserrat-Bold.ttf differ diff --git a/assets/fonts/Montserrat-BoldItalic.ttf b/assets/fonts/Montserrat-BoldItalic.ttf new file mode 100755 index 0000000..f14f818 Binary files /dev/null and b/assets/fonts/Montserrat-BoldItalic.ttf differ diff --git a/assets/fonts/Montserrat-ExtraBold.ttf b/assets/fonts/Montserrat-ExtraBold.ttf new file mode 100755 index 0000000..236f910 Binary files /dev/null and b/assets/fonts/Montserrat-ExtraBold.ttf differ diff --git a/assets/fonts/Montserrat-ExtraBoldItalic.ttf b/assets/fonts/Montserrat-ExtraBoldItalic.ttf new file mode 100755 index 0000000..1f7c55a Binary files /dev/null and b/assets/fonts/Montserrat-ExtraBoldItalic.ttf differ diff --git a/assets/fonts/Montserrat-ExtraLight.ttf b/assets/fonts/Montserrat-ExtraLight.ttf new file mode 100755 index 0000000..d334011 Binary files /dev/null and b/assets/fonts/Montserrat-ExtraLight.ttf differ diff --git a/assets/fonts/Montserrat-ExtraLightItalic.ttf b/assets/fonts/Montserrat-ExtraLightItalic.ttf new file mode 100755 index 0000000..ab1256b Binary files /dev/null and b/assets/fonts/Montserrat-ExtraLightItalic.ttf differ diff --git a/assets/fonts/Montserrat-Italic.ttf b/assets/fonts/Montserrat-Italic.ttf new file mode 100755 index 0000000..75d74e0 Binary files /dev/null and b/assets/fonts/Montserrat-Italic.ttf differ diff --git a/assets/fonts/Montserrat-Light.ttf b/assets/fonts/Montserrat-Light.ttf new file mode 100755 index 0000000..b27ed85 Binary files /dev/null and b/assets/fonts/Montserrat-Light.ttf differ diff --git a/assets/fonts/Montserrat-LightItalic.ttf b/assets/fonts/Montserrat-LightItalic.ttf new file mode 100755 index 0000000..854e8a8 Binary files /dev/null and b/assets/fonts/Montserrat-LightItalic.ttf differ diff --git a/assets/fonts/Montserrat-Medium.ttf b/assets/fonts/Montserrat-Medium.ttf new file mode 100755 index 0000000..51a8d65 Binary files /dev/null and b/assets/fonts/Montserrat-Medium.ttf differ diff --git a/assets/fonts/Montserrat-MediumItalic.ttf b/assets/fonts/Montserrat-MediumItalic.ttf new file mode 100755 index 0000000..5b19beb Binary files /dev/null and b/assets/fonts/Montserrat-MediumItalic.ttf differ diff --git a/assets/fonts/Montserrat-Regular.ttf b/assets/fonts/Montserrat-Regular.ttf new file mode 100755 index 0000000..f7d9761 Binary files /dev/null and b/assets/fonts/Montserrat-Regular.ttf differ diff --git a/assets/fonts/Montserrat-SemiBold.ttf b/assets/fonts/Montserrat-SemiBold.ttf new file mode 100755 index 0000000..b4a169c Binary files /dev/null and b/assets/fonts/Montserrat-SemiBold.ttf differ diff --git a/assets/fonts/Montserrat-SemiBoldItalic.ttf b/assets/fonts/Montserrat-SemiBoldItalic.ttf new file mode 100755 index 0000000..79973b6 Binary files /dev/null and b/assets/fonts/Montserrat-SemiBoldItalic.ttf differ diff --git a/assets/fonts/Montserrat-Thin.ttf b/assets/fonts/Montserrat-Thin.ttf new file mode 100755 index 0000000..ec41b06 Binary files /dev/null and b/assets/fonts/Montserrat-Thin.ttf differ diff --git a/assets/fonts/Montserrat-ThinItalic.ttf b/assets/fonts/Montserrat-ThinItalic.ttf new file mode 100755 index 0000000..13f3800 Binary files /dev/null and b/assets/fonts/Montserrat-ThinItalic.ttf differ diff --git a/assets/healthkitIcon.png b/assets/healthkitIcon.png new file mode 100644 index 0000000..115fa64 Binary files /dev/null and b/assets/healthkitIcon.png differ diff --git a/assets/profilePictures/eeva.png b/assets/profilePictures/eeva.png new file mode 100644 index 0000000..1e3f9c4 Binary files /dev/null and b/assets/profilePictures/eeva.png differ diff --git a/assets/profilePictures/perttu.jpg b/assets/profilePictures/perttu.jpg new file mode 100644 index 0000000..a0d43ce Binary files /dev/null and b/assets/profilePictures/perttu.jpg differ diff --git a/assets/profilePictures/pietari.png b/assets/profilePictures/pietari.png new file mode 100644 index 0000000..f7a7308 Binary files /dev/null and b/assets/profilePictures/pietari.png differ diff --git a/assets/source-images/SourceImages.ts b/assets/source-images/SourceImages.ts new file mode 100644 index 0000000..30e04e5 --- /dev/null +++ b/assets/source-images/SourceImages.ts @@ -0,0 +1,8 @@ +const sources = { + fitbit: require('./fitbit.png'), + garmin: require('./garmin.png'), + suunto: require('./suunto.png'), + withings: require('./withings.jpg') +} + +export default sources diff --git a/assets/source-images/fitbit.png b/assets/source-images/fitbit.png new file mode 100755 index 0000000..4a8f7bf Binary files /dev/null and b/assets/source-images/fitbit.png differ diff --git a/assets/source-images/garmin.png b/assets/source-images/garmin.png new file mode 100755 index 0000000..eefdd46 Binary files /dev/null and b/assets/source-images/garmin.png differ diff --git a/assets/source-images/suunto.png b/assets/source-images/suunto.png new file mode 100644 index 0000000..9f002bb Binary files /dev/null and b/assets/source-images/suunto.png differ diff --git a/assets/source-images/withings.jpg b/assets/source-images/withings.jpg new file mode 100644 index 0000000..3c14a43 Binary files /dev/null and b/assets/source-images/withings.jpg differ diff --git a/assets/svgs.tsx b/assets/svgs.tsx new file mode 100644 index 0000000..4b36e36 --- /dev/null +++ b/assets/svgs.tsx @@ -0,0 +1,795 @@ +import * as React from 'react' +import { + Circle, + G, + Line, + Linecap, + Linejoin, + Path, + Polygon, + Polyline, + Rect +} from 'react-native-svg' // import whichever features are used in your SVGs + +const strokeLinecap: Linecap = 'round' as Linecap +const strokeLinejoin: Linejoin = 'round' as Linejoin + +const defaultProps = { + strokeLinecap, + strokeLinejoin, + strokeWidth: '1.5px' +} +// tslint:disable:max-line-length +export enum Icon { + settingsLight = 'settingsLight', + settingsBold = 'settingsLight' +} +interface icons { + any: Icon +} + +export const icons = { + settingsLight: ( + + + + + ), + settingsBold: ( + + + + ), + userBold: ( + + ), + userLight: ( + + + + + ), + clockBold: ( + + + + ), + clockRegular: ( + + + + + + + ), + schoolPhysicalBold: ( + + + + + + ), + schoolPhysical: ( + + + + + + + ), + + emailUnreadRegular: ( + + + + + ), + bookLamp: ( + + + + + + + + + + + ), + nightMoonBegin: ( + + + + + + + + ), + nightMoonEnd: ( + + + + + + + + ), + + doubleBed: ( + + + + + + + + + ), + + // Smileys!! + + smileyEyesOnly: ( + + + + + + ), + smileyIndifferent: ( + + + + + + + ), + smileyIndifferentBold: ( + + + + + + + ), + smileySmirkGlasses: ( + + + + + + ), + + smileyUnhappy: ( + + + + + + + ), + smileySadBold: ( + + + + + + + ), + smileySmirkBold: ( + + + + + + + ), + + // Interface icons + + chevronLeft: ( + + + + ), + chevronRight: ( + + + + ), + informationCircle: ( + + + + ), + shovel: ( + + + + + + ), + lockCircle: ( + + + + ), + closeCircle: ( + + + + ), + navigationCircleRight: ( + + + + + ), + statsGraphCircle: ( + + + + + ), + circleAlternate: ( + + + + ), + circle: ( + + + + ), + circleHelp: ( + + + + ), + smartWatchCircleGraph: ( + + + + + + + ), + likeCircle: ( + + + + ), + loveItCircle: ( + + + + ), + browserDotCom: ( + + + + + + + + ), + envelope: ( + + + + + + + + ), + astronomyMoon: ( + + + + + + + ), + moonIcon: ( + + + + ), + powerButton: ( + + + + + ), + syncCloud: ( + + + + + + ), + trophyStar: ( + + + + + ), + taskListEdit: ( + + + + + + + + ), + targetCenter: ( + + + + + + ), + circleCheck: ( + + + + ), + checkMark: ( + + + + ), + circleUncheck: ( + + + + ), + emailUnread: ( + + + + + ), + singleManCircle: ( + + + + ), + dialPad: ( + + + + + + + + + + ), + bedDoubleBold: ( + + + + + ), + moodMoody: ( + + + + + + ), + smileyDisappointed: ( + + + + + + + + + ), + smileyEyesOnlyBold: ( + + + + + + ), + + // Play and stop buttons + play: ( + + + + ), + stop: ( + + + + + ), + record: ( + + + + + ), + pause: ( + + + + + + ), + charging: ( + + + + ), + circleAdd: ( + + + + + ), + circleSubtract: ( + + + + + ), + checklist: ( + + + + + + + + ), + crown: ( + + + + + ), + listAdd: ( + + + + + + + + + + ), + listRemove: ( + + + + + + + + + + ), + chat: ( + + + + + ), + multiUsers: ( + + + + + + + + + + + ), + yoga: ( + + + + + + + ), + daySunrise: ( + + + + + + + + + + + + + ), + sun: ( + + + + ), + daySunset: ( + + + + + + + + + + + + + ), + bedWindow: ( + + + + + + + + + + ), + tag: ( + + + + + ), + scale: ( + + + + ), + phoneTouch: ( + + + + ), + compass: ( + + + + ), + arrowLeft: ( + + + + ), + arrowLineLeft: ( + + + + ), + arrowRight: ( + + + + ), + refresh: ( + + + + ), + arrowCircleRight: ( + + + + ), + customerSupport: ( + + + + ), + smileyThrilled: ( + + + + ), + receipt: ( + + + + ), + facebook: ( + + + + ), + instagram: ( + + + + ), + twitter: ( + + + + ), + handshake: ( + + + + ), + addRatingIcon: ( + + + + ), + flame: ( + + + + ), + bin: ( + + + + ), + archive: ( + + + + ), + star: ( + + + + ), + questionMarkCircle: ( + + + + ), + alarmBell: ( + + + + ), + wrench: ( + + + + ), + calendar: ( + + + + ) +} + +export default icons diff --git a/assets/terveystalo-logo.svg b/assets/terveystalo-logo.svg new file mode 100644 index 0000000..9e97f31 --- /dev/null +++ b/assets/terveystalo-logo.svg @@ -0,0 +1,23 @@ + + + + + Tunnus_Terveystalo_vaaka_RGB + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/texts/rating.ts b/assets/texts/rating.ts new file mode 100644 index 0000000..159629d --- /dev/null +++ b/assets/texts/rating.ts @@ -0,0 +1,25 @@ +export const rating = { + RATE_NIGHT_BUTTON: 'RATE NIGHT', + RATING_TITLE: 'How rested do you feel?', + + RATE_BAD_ICON: 'smileySadBold', + RATE_BAD_TEXT: 'Bad', + + RATE_OK_ICON: 'smileyIndifferentBold', + RATE_OK_TEXT: 'Okay', + + RATE_GOOD_ICON: 'smileySmirkBold', + RATE_GOOD_TEXT: 'Good', + + RATING_GREAT_ICON: 'smileyThrilled', + RATING_GREAT_TEXT: 'Great', + + RATE_UNRATED_ICON: 'addRatingIcon', + RATE_UNRATED_TEXT: 'Unrated', + + RATING_UNRATED_VALUE: 0, + RATING_BAD_VALUE: 1, + RATING_OK_VALUE: 2, + RATING_GOOD_VALUE: 3, + RATING_GREAT_VALUE: 4 +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..8414a6d --- /dev/null +++ b/babel.config.js @@ -0,0 +1,33 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], + plugins: [ + 'babel-plugin-styled-components', + [ + 'babel-plugin-inline-import', + { + extensions: ['.svg'] + } + ], + [ + 'module-resolver', + { + root: ['./src/'], + extensions: [ + '.ios.ts', + '.android.ts', + '.ts', + '.ios.tsx', + '.android.tsx', + '.tsx', + '.jsx', + '.js', + '.json' + ], + alias: { + '@actions': './src/actions/', + '@reducers': './src/store/Reducers' + } + } + ] + ] +} diff --git a/docs/CHANGELOG b/docs/CHANGELOG new file mode 100644 index 0000000..17a3809 --- /dev/null +++ b/docs/CHANGELOG @@ -0,0 +1,22 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.2] +- Lessons now contain example habits for you to try out +- We made improvements to the Android Version +- Nyxo now supports direct import from Fitbit. To use use this feature navigate to settings page and link your Fitbit account with Nyxo. +- We updated some backend stuff +- We added sections to lessons to they are now better grouped +- We added ability rate the lesson so you can provide feedback lessons +- You can now see how many days you have left of the week + + + + +## [1.1.7] - 2019-10-16 +### Added +- Functionality to manually add nights + diff --git a/docs/Docs.md b/docs/Docs.md new file mode 100644 index 0000000..2c20283 --- /dev/null +++ b/docs/Docs.md @@ -0,0 +1,12 @@ +# Materiaalia + +## Uni ikkuna + +[Uni ikkuna doc](https://docs.google.com/document/d/1XDX_mFA5Cd7J1sBWCFc-94tuSd9kWVqky8xntA8EOrs/edit) +Nyxon uni-ikkuna voidaan esittää jaa laskea seuraavalla tavalla: + +Healthkitistä haettavasta datasta napataan “INSLEEP” arvon alkamisajankohdat maksimistaan viimeiseltä seitsemältä päivältä, jotka ovat välilä 20:00-02:00. Näistä ajoista lasketaan keskiarvo, joka painottaa arkipäivien mittauksia. Uni-ikkuna on ihmisen sirkkadiaanise nrytmin mukaisesti 90 minuuttia, jossa 45 minuuttia eli puoliväli kuvaa rytmin matalinta kohtaa eli nukahtamisen kannalta otollisinta. Kun lasketun keskiarvon ympärille piirretään 45 minuutin ajat, saadaan lähes oikea sirkkadiaaniseen rytmiin perustuva uni-ikkuna. + +## Nyxo sovelluksen kuvitus + +[Kuvitus dokumentti](https://docs.google.com/document/d/189DA-9XjssrzdetZBV9F3ZeGNk8sc7ePc3rhrgnQv4Y/edit#) diff --git a/docs/Habits.md b/docs/Habits.md new file mode 100644 index 0000000..4aa45de --- /dev/null +++ b/docs/Habits.md @@ -0,0 +1,80 @@ +# How Habits should work in Nyxo + +- Users can perform CRUD (create, read, update, delete) operations on habits locally. +- When logging in, local habits will be synced using amplify mutations. +- The Habit syncing processes work like the way Git works at some points. + +## Habit syncing process + +Below are steps that describe how the app handles Habit-related processes. + +### 1. Handling loggin process + +- The local `habitState.habits` is always the source of truth for habits. +- User creates habits locally -> those habits are stored at `habitState.habits`. +- User logins with an account -> we detect if there are any changes between `habitState.habits` (1) and `habitState.subHabits` (2). +- If there are changes, prompt a dialog asking whether user wants to merge changes. If choosing Yes, detected local habits will + be synced to the cloud and the app merges remote habits and local habits into one local habits stored in `habitState.habits`. + The old local habits (1) will be copied to (2) to make sure the next time user logs out, the app will display a list of previously created habits. By using the copy method to `habitState.subHabits`, we can make sure the app hardly displays 0 habits (unless user intentionally deletes all local habits). +- If choosing No, `habitState.habits` will be loaded by the remote habits and the old local habits (1) will be transfered to + `habitState.subHabits`. + +### 2. Handling syncing process + +- When logging in with an account, user can have his/her created habits saved into the cloud. +- There are 4 ways to perform Habit syncing processes: + Periodically every 15mins by using background fetch. + When user intentionally srolls the main screen to trigger refresh control in `main.tsx`. + When user logs out. + At "cold" starts +- Whenever a logged-in user performs a CRUD operation with a habit, the habit will be stashed into `habitState.unsyncedHabits`. + Habits in `habitState.unsyncedHabits` are used to synced back to the cloud in the above processes. Any successfully synced habits will be + removed from `habitState.unsyncedHabits` and any unsuccessfully synced habits will remain for the next syncing process. + +## Use Cases Documentations & Manual Testings + +### 1. Create a habit + +- Navigate to "Sleep" screen (with sleep clocks). +- Click "+" button to open up the modal. +- Fill in "Title" field is mandatory. "Title" field must have at least 3 characters (not including whitespaces). +- The gray circle on the right side of each field indicates the total characters of that field. When the circle is fully coloured, it means the field can no more have any character. +- Choose "Time of day". +- Click "Create this habit" button to create. This step dispatches an action to Redux reducer to add the created habit to `habitState.habits`. +- In the "Sleep" screen, there should be the created habit. +- If the creating habit's title exists already, there will be a dialog informing that (trimmed title so that whitespaces make no difference). + +- To save the habit for later use, click "Save" button. This button allows the habit's creating process to be saved and the modal will load all inputs when clicking "+" button again. + +### 2. Edit a habit + +- Click on the habit that you want to edit in the "Sleep" screen +- Bring up the editting modal. +- Under the top row of the modal, there should be a section containing information about the current day streak and the longest day streak of the habit. +- Edit the habit by adjusting "Title", "Description" and "Time of day" fields. +- Click "Save" to finish editting. +- In the "Sleep" screen, the habit should be updated with new data. + +### 3. Delete a habit + +- In the "Sleep" screen, swipe the wanted habit to the right to bring up "Delete" button. +- Hit the button and the habit should be gone. + +### 4. Archive a habit + +- In the "Sleep" screen, swipe the wanted habit to the right to bring up "Archive" button. +- Hit the button and the habit should be listed in "Archived" section when clicking "Show all" button. + +### 5. Complete a habit + +- In the "Sleep" screen, swipe the wanted habit to the left to bring up "Complete" button. +- Hit the button and the habit should be shown as completed. +- The habit's card title will be crossed and there will be a check mark indicating the habit's completion. +- When completing the habit, the value of its day streak will be incremented by 1, as well as the value of its longest day streak if in case, the day streak value is higher than the longest day streak value. + +### 6. Uncomplete a habit + +- Follow the procedure in 5 to reproduce the process. +- When uncompleting the habit, the value of its day streak will be decreased by 1. However, its longest streak value stays the same. + +### 7. Handle syncing habits + +- In the "Sleep" screen, when logged in, you can scroll the view down to bring up the loading indicator. +- The app will perform appropriate mutations of stashed unsynced habits in `habitState.unsyncedHabits`. +- After performing mutations, the app will then retrieve saved-on-cloud habits to ensure the case that an account can be logged at multiple different devices and the user performs habit CRUD operations on different devices as well. (considering using AWS Amplify Subscriptions?) diff --git a/docs/Notifications.md b/docs/Notifications.md new file mode 100644 index 0000000..5bf0261 --- /dev/null +++ b/docs/Notifications.md @@ -0,0 +1,26 @@ +# Notifications in Nyxo + +All notification processes will be rescheduled every 15 minutes by using react-native-background-fetch. + +## Bedtime Window notification + +- User is notified 15 minutes before their bedtime window + +## Coaching Notifications + +- Notification for reminding a user has an ongoing lesson +- notification for reminding a user has uncompleted lessons in the ongoing week + +**Other notes:** + +- The notification for reminding ongoing lesson applies to all lessons, which means if the user's ongoing week is week 1 but he/she is reading a lesson in week 3 and hasn't finished it yet, the app will fire a notification about that lesson. +- The notification for reminding uncompleted lessons in the ongoing week will stop notify when users finish all lessons in the week. + +## Customer Support Notifications + +Handled by intercom, currently we can't really affect these + +## Important stuff + +- Android version needs specific icons for each type of notifications. +- Must polish notifying texts so that they deliver meaningful and engaging messages. diff --git a/getContentfulEnvironment.js b/getContentfulEnvironment.js new file mode 100644 index 0000000..7c50b9a --- /dev/null +++ b/getContentfulEnvironment.js @@ -0,0 +1,13 @@ +/* eslint-disable func-names */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const contentfulManagement = require('contentful-management') + +module.exports = function () { + const contentfulClient = contentfulManagement.createClient({ + accessToken: '' + }) + + return contentfulClient + .getSpace('') + .then((space) => space.getEnvironment('master')) +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..52ea753 --- /dev/null +++ b/index.js @@ -0,0 +1,20 @@ +import React from 'react' +import { AppRegistry } from 'react-native' +import 'react-native-gesture-handler' +import { Provider } from 'react-redux' +import { PersistGate } from 'redux-persist/integration/react' +import App from './src/App' +import { persistor, store } from './src/store' +import 'react-native-get-random-values' + +const Index = () => { + return ( + + + + + + ) +} + +AppRegistry.registerComponent('Nyxo', () => Index) diff --git a/ios/Gemfile b/ios/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/ios/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 0000000..d622297 --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,179 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.332.0) + aws-sdk-core (3.100.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.34.1) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.69.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.3) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.10) + declarative-option (0.1.0) + digest-crc (0.5.1) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.5) + emoji_regex (1.0.1) + excon (0.75.0) + faraday (1.0.1) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.1.7) + fastlane (2.149.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.2, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 2.0) + excon (>= 0.71.0, < 1.0.0) + faraday (>= 0.17, < 2.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (>= 0.13.1, < 2.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (~> 2.1.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_xml (~> 0.5) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + public_suffix (~> 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-cloud-core (1.5.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.3.2) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.26.2) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-api-client (~> 0.33) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.13.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.3.0) + jwt (2.1.0) + memoist (0.16.2) + mini_magick (4.10.1) + mini_mime (1.0.2) + multi_json (1.14.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nanaimo (0.2.6) + naturally (2.2.0) + os (1.1.0) + plist (3.5.0) + public_suffix (2.0.5) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rouge (2.0.7) + rubyzip (1.3.0) + security (0.1.3) + signet (0.14.0) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.0) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.16.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.0) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.1.4 diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist new file mode 100644 index 0000000..35fe563 --- /dev/null +++ b/ios/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 476920454814-ok8d19hcejrjacfp3md9mm2m6lml48ie.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.476920454814-ok8d19hcejrjacfp3md9mm2m6lml48ie + ANDROID_CLIENT_ID + 476920454814-3ld7nbt03r554sdk6bcqhe4485dca66o.apps.googleusercontent.com + API_KEY + AIzaSyABwxP3HSDrzYxiXqgxZ5jOCz5OaKAiixg + GCM_SENDER_ID + 476920454814 + PLIST_VERSION + 1 + BUNDLE_ID + app.sleepcircle.application + PROJECT_ID + nyxo-android + STORAGE_BUCKET + nyxo-android.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:476920454814:ios:b7ace1023a27fc9e99258f + DATABASE_URL + https://nyxo-android.firebaseio.com + + \ No newline at end of file diff --git a/ios/LaunchScreen.xib b/ios/LaunchScreen.xib new file mode 100644 index 0000000..aef9133 --- /dev/null +++ b/ios/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Nyxo.xcodeproj/project.pbxproj b/ios/Nyxo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a18073b --- /dev/null +++ b/ios/Nyxo.xcodeproj/project.pbxproj @@ -0,0 +1,1682 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5B090C42235912AC00FD7E9B /* Domine-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5B090C40235912AC00FD7E9B /* Domine-Bold.ttf */; }; + 5B090C43235912AC00FD7E9B /* Domine-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5B090C41235912AC00FD7E9B /* Domine-Regular.ttf */; }; + 5B20692D224E434B00257043 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B20692C224E434B00257043 /* StoreKit.framework */; }; + 5B33130921DFB5B800698A4A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B33130121DFB5B800698A4A /* AppDelegate.m */; }; + 5B33130A21DFB5B800698A4A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B33130221DFB5B800698A4A /* main.m */; }; + 5B40961F242BAE0000169B4C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B40961E242BAE0000169B4C /* GoogleService-Info.plist */; }; + 5B5476582233C6650027A9A0 /* Intercom.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5476152233C6640027A9A0 /* Intercom.framework */; }; + 5B5476592233C6650027A9A0 /* Intercom.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5476152233C6640027A9A0 /* Intercom.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5B70355823E998420068DB97 /* Nyxo (DEV).plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B70355723E998420068DB97 /* Nyxo (DEV).plist */; }; + 5B7820E024EEC2360074BAFC /* rnuc.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 5B7820DF24EEC2360074BAFC /* rnuc.xcconfig */; }; + 5BA0F90621E2411B00D3D56E /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 5BA0F90521E2411B00D3D56E /* main.jsbundle */; }; + 5BA9115E22E1E2350098700A /* CommandStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9115D22E1E2350098700A /* CommandStatus.swift */; }; + 5BD58B9822D71D7600D1CD2D /* Montserrat-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8622D71D7400D1CD2D /* Montserrat-LightItalic.ttf */; }; + 5BD58B9922D71D7600D1CD2D /* Montserrat-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8722D71D7400D1CD2D /* Montserrat-ExtraBoldItalic.ttf */; }; + 5BD58B9A22D71D7600D1CD2D /* Montserrat-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8822D71D7500D1CD2D /* Montserrat-SemiBoldItalic.ttf */; }; + 5BD58B9B22D71D7600D1CD2D /* Montserrat-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8922D71D7500D1CD2D /* Montserrat-Black.ttf */; }; + 5BD58B9C22D71D7600D1CD2D /* Montserrat-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8A22D71D7500D1CD2D /* Montserrat-Italic.ttf */; }; + 5BD58B9D22D71D7600D1CD2D /* Montserrat-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8B22D71D7500D1CD2D /* Montserrat-ThinItalic.ttf */; }; + 5BD58B9E22D71D7600D1CD2D /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8C22D71D7500D1CD2D /* Montserrat-Medium.ttf */; }; + 5BD58B9F22D71D7600D1CD2D /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8D22D71D7500D1CD2D /* Montserrat-Regular.ttf */; }; + 5BD58BA022D71D7600D1CD2D /* Montserrat-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8E22D71D7500D1CD2D /* Montserrat-MediumItalic.ttf */; }; + 5BD58BA122D71D7600D1CD2D /* Montserrat-ExtraLightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8F22D71D7500D1CD2D /* Montserrat-ExtraLightItalic.ttf */; }; + 5BD58BA222D71D7600D1CD2D /* Montserrat-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9022D71D7500D1CD2D /* Montserrat-BoldItalic.ttf */; }; + 5BD58BA322D71D7600D1CD2D /* Montserrat-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9122D71D7500D1CD2D /* Montserrat-SemiBold.ttf */; }; + 5BD58BA422D71D7600D1CD2D /* Montserrat-BlackItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9222D71D7500D1CD2D /* Montserrat-BlackItalic.ttf */; }; + 5BD58BA522D71D7600D1CD2D /* Montserrat-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9322D71D7500D1CD2D /* Montserrat-ExtraLight.ttf */; }; + 5BD58BA622D71D7600D1CD2D /* Montserrat-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9422D71D7600D1CD2D /* Montserrat-ExtraBold.ttf */; }; + 5BD58BA722D71D7600D1CD2D /* Montserrat-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9522D71D7600D1CD2D /* Montserrat-Light.ttf */; }; + 5BD58BA822D71D7600D1CD2D /* Montserrat-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9622D71D7600D1CD2D /* Montserrat-Thin.ttf */; }; + 5BD58BA922D71D7600D1CD2D /* Montserrat-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9722D71D7600D1CD2D /* Montserrat-Bold.ttf */; }; + 5BE7E22E23E98F21000FC447 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B33130221DFB5B800698A4A /* main.m */; }; + 5BE7E22F23E98F21000FC447 /* CommandStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9115D22E1E2350098700A /* CommandStatus.swift */; }; + 5BE7E23023E98F21000FC447 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B33130121DFB5B800698A4A /* AppDelegate.m */; }; + 5BE7E23223E98F21000FC447 /* AppCenterReactNativeShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B50DCC82218829A0036DFDD /* AppCenterReactNativeShared.framework */; }; + 5BE7E23323E98F21000FC447 /* AppCenterCrashes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B50DCC4221882830036DFDD /* AppCenterCrashes.framework */; }; + 5BE7E23423E98F21000FC447 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED297162215061F000B7C4FE /* JavaScriptCore.framework */; }; + 5BE7E23523E98F21000FC447 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B20692C224E434B00257043 /* StoreKit.framework */; }; + 5BE7E23623E98F21000FC447 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B95CBA9B2099AEB300243A25 /* HealthKit.framework */; }; + 5BE7E23723E98F21000FC447 /* Intercom.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5476152233C6640027A9A0 /* Intercom.framework */; }; + 5BE7E23823E98F21000FC447 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B75EE392188A8920070EB69 /* Fabric.framework */; }; + 5BE7E23923E98F21000FC447 /* AppCenterAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B50DCC2221882830036DFDD /* AppCenterAnalytics.framework */; }; + 5BE7E23A23E98F21000FC447 /* AppCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B50DCC3221882830036DFDD /* AppCenter.framework */; }; + 5BE7E23B23E98F21000FC447 /* AppCenterPush.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B50DD33221884EB0036DFDD /* AppCenterPush.framework */; }; + 5BE7E23D23E98F21000FC447 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BAC1FA2BBE14987AE9C5E6D /* libz.tbd */; }; + 5BE7E24023E98F21000FC447 /* Montserrat-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8622D71D7400D1CD2D /* Montserrat-LightItalic.ttf */; }; + 5BE7E24123E98F21000FC447 /* Montserrat-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8722D71D7400D1CD2D /* Montserrat-ExtraBoldItalic.ttf */; }; + 5BE7E24223E98F21000FC447 /* Montserrat-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8822D71D7500D1CD2D /* Montserrat-SemiBoldItalic.ttf */; }; + 5BE7E24323E98F21000FC447 /* Montserrat-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8922D71D7500D1CD2D /* Montserrat-Black.ttf */; }; + 5BE7E24423E98F21000FC447 /* Montserrat-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8A22D71D7500D1CD2D /* Montserrat-Italic.ttf */; }; + 5BE7E24523E98F21000FC447 /* Montserrat-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8B22D71D7500D1CD2D /* Montserrat-ThinItalic.ttf */; }; + 5BE7E24623E98F21000FC447 /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8C22D71D7500D1CD2D /* Montserrat-Medium.ttf */; }; + 5BE7E24723E98F21000FC447 /* Domine-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5B090C41235912AC00FD7E9B /* Domine-Regular.ttf */; }; + 5BE7E24823E98F21000FC447 /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8D22D71D7500D1CD2D /* Montserrat-Regular.ttf */; }; + 5BE7E24923E98F21000FC447 /* Montserrat-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8E22D71D7500D1CD2D /* Montserrat-MediumItalic.ttf */; }; + 5BE7E24A23E98F21000FC447 /* Montserrat-ExtraLightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B8F22D71D7500D1CD2D /* Montserrat-ExtraLightItalic.ttf */; }; + 5BE7E24B23E98F21000FC447 /* Montserrat-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9022D71D7500D1CD2D /* Montserrat-BoldItalic.ttf */; }; + 5BE7E24C23E98F21000FC447 /* Montserrat-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9122D71D7500D1CD2D /* Montserrat-SemiBold.ttf */; }; + 5BE7E24D23E98F21000FC447 /* Montserrat-BlackItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9222D71D7500D1CD2D /* Montserrat-BlackItalic.ttf */; }; + 5BE7E24E23E98F21000FC447 /* Montserrat-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9322D71D7500D1CD2D /* Montserrat-ExtraLight.ttf */; }; + 5BE7E24F23E98F21000FC447 /* Montserrat-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9422D71D7600D1CD2D /* Montserrat-ExtraBold.ttf */; }; + 5BE7E25023E98F21000FC447 /* Montserrat-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9522D71D7600D1CD2D /* Montserrat-Light.ttf */; }; + 5BE7E25123E98F21000FC447 /* Montserrat-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9622D71D7600D1CD2D /* Montserrat-Thin.ttf */; }; + 5BE7E25223E98F21000FC447 /* Montserrat-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5BD58B9722D71D7600D1CD2D /* Montserrat-Bold.ttf */; }; + 5BE7E25323E98F21000FC447 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 5BA0F90521E2411B00D3D56E /* main.jsbundle */; }; + 5BE7E25423E98F21000FC447 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9811C22D2052900509ED1 /* Images.xcassets */; }; + 5BE7E25523E98F21000FC447 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B3312FB21DFB5B700698A4A /* Images.xcassets */; }; + 5BE7E25623E98F21000FC447 /* Domine-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5B090C40235912AC00FD7E9B /* Domine-Bold.ttf */; }; + 5BE7E25723E98F21000FC447 /* FontAwesome5_Pro_Brands.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 796FDDB5089B4D6FBFF9AF7D /* FontAwesome5_Pro_Brands.ttf */; }; + 5BE7E25823E98F21000FC447 /* AppCenter-Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = B64D42BFD4E244D9863E00B7 /* AppCenter-Config.plist */; }; + 5BE7E25D23E98F21000FC447 /* Intercom.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5476152233C6640027A9A0 /* Intercom.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5BF9816322D2052900509ED1 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9811C22D2052900509ED1 /* Images.xcassets */; }; + 5C56F98D642F4D5BA2168C40 /* AppCenter-Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = B64D42BFD4E244D9863E00B7 /* AppCenter-Config.plist */; }; + 6EF70286CEF4580632B0E50F /* libPods-Nyxo Dev.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 140C71F65113D98617CEF82C /* libPods-Nyxo Dev.a */; }; + B95CBA9C2099AEB300243A25 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B95CBA9B2099AEB300243A25 /* HealthKit.framework */; }; + C6BF152484FF448FA7435F49 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BAC1FA2BBE14987AE9C5E6D /* libz.tbd */; }; + E1DF9C1DF40E9169E37562FD /* libPods-Nyxo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E89B28DB570F1784E077C6A0 /* libPods-Nyxo.a */; }; + ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED297162215061F000B7C4FE /* JavaScriptCore.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = Nyxo; + }; + 5BB5735C21DE4024008895BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BB5733A21DE4024008895BB /* RNDeviceInfo.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = E72EC1401F7ABB5A0001BC90; + remoteInfo = "RNDeviceInfo-tvOS"; + }; + 5BB573A521DE40B8008895BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BB5733A21DE4024008895BB /* RNDeviceInfo.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DA5891D81BA9A9FC002B4DB2; + remoteInfo = RNDeviceInfo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5B54765A2233C6650027A9A0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5B5476592233C6650027A9A0 /* Intercom.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5B6E854A21406B9A0013E7B5 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE7E25A23E98F21000FC447 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE7E25C23E98F21000FC447 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5BE7E25D23E98F21000FC447 /* Intercom.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00814AB27B8A4287577099B1 /* Pods-Nyxo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nyxo.release.xcconfig"; path = "Target Support Files/Pods-Nyxo/Pods-Nyxo.release.xcconfig"; sourceTree = ""; }; + 00E356EE1AD99517003FC87E /* NyxoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NyxoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* Nyxo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nyxo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 140C71F65113D98617CEF82C /* libPods-Nyxo Dev.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nyxo Dev.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 4BAC1FA2BBE14987AE9C5E6D /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + 4F61ADB20DA8429D927A20BC /* FontAwesome5_Pro_Solid.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Pro_Solid.ttf; path = ../assets/fonts/FontAwesome5_Pro_Solid.ttf; sourceTree = ""; }; + 5B090C40235912AC00FD7E9B /* Domine-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Domine-Bold.ttf"; path = "../assets/fonts/Domine-Bold.ttf"; sourceTree = ""; }; + 5B090C41235912AC00FD7E9B /* Domine-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Domine-Regular.ttf"; path = "../assets/fonts/Domine-Regular.ttf"; sourceTree = ""; }; + 5B20692C224E434B00257043 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 5B2069A4224FB24200257043 /* Lato-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-Regular.ttf"; path = "../assets/fonts/Lato-Regular.ttf"; sourceTree = ""; }; + 5B2B308921EF5C82007E2982 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = ""; }; + 5B3312FB21DFB5B700698A4A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nyxo/Images.xcassets; sourceTree = ""; }; + 5B3312FE21DFB5B800698A4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = LaunchScreen.old.xib; sourceTree = ""; }; + 5B33130021DFB5B800698A4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = LaunchScreen.xib; sourceTree = ""; }; + 5B33130121DFB5B800698A4A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Nyxo/AppDelegate.m; sourceTree = ""; }; + 5B33130221DFB5B800698A4A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Nyxo/main.m; sourceTree = ""; }; + 5B33130321DFB5B800698A4A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nyxo/Info.plist; sourceTree = ""; }; + 5B33130421DFB5B800698A4A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Nyxo/AppDelegate.h; sourceTree = ""; }; + 5B33136421DFB73A00698A4A /* Nyxo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Nyxo.entitlements; path = Nyxo/Nyxo.entitlements; sourceTree = ""; }; + 5B37A97122CE0B6700820944 /* Lato-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-Black.ttf"; path = "../assets/fonts/Lato-Black.ttf"; sourceTree = ""; }; + 5B37A97222CE0B6700820944 /* Lato-HairlineItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-HairlineItalic.ttf"; path = "../assets/fonts/Lato-HairlineItalic.ttf"; sourceTree = ""; }; + 5B37A97322CE0B6700820944 /* Lato-BlackItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-BlackItalic.ttf"; path = "../assets/fonts/Lato-BlackItalic.ttf"; sourceTree = ""; }; + 5B37A97422CE0B6700820944 /* Lato-Hairline.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-Hairline.ttf"; path = "../assets/fonts/Lato-Hairline.ttf"; sourceTree = ""; }; + 5B37A97522CE0B6700820944 /* Lato-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-BoldItalic.ttf"; path = "../assets/fonts/Lato-BoldItalic.ttf"; sourceTree = ""; }; + 5B37A97622CE0B6700820944 /* Lato-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-Bold.ttf"; path = "../assets/fonts/Lato-Bold.ttf"; sourceTree = ""; }; + 5B37A97722CE0B6700820944 /* Lato-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-Light.ttf"; path = "../assets/fonts/Lato-Light.ttf"; sourceTree = ""; }; + 5B37A97822CE0B6700820944 /* Lato-LightItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato-LightItalic.ttf"; path = "../assets/fonts/Lato-LightItalic.ttf"; sourceTree = ""; }; + 5B37A97922CE0B6C00820944 /* Lato Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Lato Italic.ttf"; path = "../assets/fonts/Lato Italic.ttf"; sourceTree = ""; }; + 5B37A97A22CE1A4700820944 /* Montserrat-Thin.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Thin.ttf"; path = "../assets/fonts/Montserrat-Thin.ttf"; sourceTree = ""; }; + 5B37A97B22CE1A4800820944 /* Montserrat-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraLight.ttf"; path = "../assets/fonts/Montserrat-ExtraLight.ttf"; sourceTree = ""; }; + 5B37A97C22CE1A4800820944 /* Montserrat-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraBoldItalic.ttf"; path = "../assets/fonts/Montserrat-ExtraBoldItalic.ttf"; sourceTree = ""; }; + 5B37A97D22CE1A4800820944 /* Montserrat-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-SemiBoldItalic.ttf"; path = "../assets/fonts/Montserrat-SemiBoldItalic.ttf"; sourceTree = ""; }; + 5B37A97E22CE1A4800820944 /* Montserrat-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Italic.ttf"; path = "../assets/fonts/Montserrat-Italic.ttf"; sourceTree = ""; }; + 5B37A97F22CE1A4800820944 /* Montserrat-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-BoldItalic.ttf"; path = "../assets/fonts/Montserrat-BoldItalic.ttf"; sourceTree = ""; }; + 5B37A98022CE1A4800820944 /* Montserrat-ExtraLightItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraLightItalic.ttf"; path = "../assets/fonts/Montserrat-ExtraLightItalic.ttf"; sourceTree = ""; }; + 5B37A98122CE1A4800820944 /* Montserrat-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-SemiBold.ttf"; path = "../assets/fonts/Montserrat-SemiBold.ttf"; sourceTree = ""; }; + 5B37A98222CE1A4800820944 /* Montserrat-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ThinItalic.ttf"; path = "../assets/fonts/Montserrat-ThinItalic.ttf"; sourceTree = ""; }; + 5B37A98322CE1A4800820944 /* Montserrat-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Light.ttf"; path = "../assets/fonts/Montserrat-Light.ttf"; sourceTree = ""; }; + 5B37A98422CE1A4900820944 /* Montserrat-MediumItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-MediumItalic.ttf"; path = "../assets/fonts/Montserrat-MediumItalic.ttf"; sourceTree = ""; }; + 5B37A98522CE1A4900820944 /* Montserrat-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Bold.ttf"; path = "../assets/fonts/Montserrat-Bold.ttf"; sourceTree = ""; }; + 5B37A98622CE1A4900820944 /* Montserrat-LightItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-LightItalic.ttf"; path = "../assets/fonts/Montserrat-LightItalic.ttf"; sourceTree = ""; }; + 5B37A98722CE1A4900820944 /* Montserrat-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraBold.ttf"; path = "../assets/fonts/Montserrat-ExtraBold.ttf"; sourceTree = ""; }; + 5B37A98822CE1A4900820944 /* Montserrat-BlackItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-BlackItalic.ttf"; path = "../assets/fonts/Montserrat-BlackItalic.ttf"; sourceTree = ""; }; + 5B37A98922CE1A4900820944 /* Montserrat-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Black.ttf"; path = "../assets/fonts/Montserrat-Black.ttf"; sourceTree = ""; }; + 5B37A98A22CE1A4900820944 /* Montserrat-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Medium.ttf"; path = "../assets/fonts/Montserrat-Medium.ttf"; sourceTree = ""; }; + 5B37A98B22CE1A4A00820944 /* Montserrat-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Regular.ttf"; path = "../assets/fonts/Montserrat-Regular.ttf"; sourceTree = ""; }; + 5B40961E242BAE0000169B4C /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 5B48388421FA22EE003855DF /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = ../fi.lproj/LaunchScreen.old.strings; sourceTree = ""; }; + 5B48388521FA22EE003855DF /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = ../fi.lproj/LaunchScreen.strings; sourceTree = ""; }; + 5B50DCC2221882830036DFDD /* AppCenterAnalytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenterAnalytics.framework; path = "Vendor/AppCenter-SDK-Apple/iOS/AppCenterAnalytics.framework"; sourceTree = ""; }; + 5B50DCC3221882830036DFDD /* AppCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenter.framework; path = "Vendor/AppCenter-SDK-Apple/iOS/AppCenter.framework"; sourceTree = ""; }; + 5B50DCC4221882830036DFDD /* AppCenterCrashes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenterCrashes.framework; path = "Vendor/AppCenter-SDK-Apple/iOS/AppCenterCrashes.framework"; sourceTree = ""; }; + 5B50DCC82218829A0036DFDD /* AppCenterReactNativeShared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenterReactNativeShared.framework; path = Vendor/AppCenterReactNativeShared/AppCenterReactNativeShared.framework; sourceTree = ""; }; + 5B50DD33221884EB0036DFDD /* AppCenterPush.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenterPush.framework; path = "Vendor/AppCenter-SDK-Apple/iOS/AppCenterPush.framework"; sourceTree = ""; }; + 5B5476152233C6640027A9A0 /* Intercom.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Intercom.framework; sourceTree = ""; }; + 5B70355723E998420068DB97 /* Nyxo (DEV).plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Nyxo (DEV).plist"; sourceTree = ""; }; + 5B75EE392188A8920070EB69 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = ""; }; + 5B75EE3A2188A8920070EB69 /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Crashlytics.framework; sourceTree = ""; }; + 5B7820DF24EEC2360074BAFC /* rnuc.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = rnuc.xcconfig; sourceTree = ""; }; + 5B8EDB59242B6F4800A3796F /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "GoogleService-Info.plist"; path = "../../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; + 5BA0F90521E2411B00D3D56E /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 5BA9115B22E1E2350098700A /* Nyxo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Nyxo-Bridging-Header.h"; sourceTree = ""; }; + 5BA9115C22E1E2350098700A /* Nyxo Watch-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Nyxo Watch-Bridging-Header.h"; sourceTree = ""; }; + 5BA9115D22E1E2350098700A /* CommandStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandStatus.swift; sourceTree = ""; }; + 5BB5732C21DE4024008895BB /* CODE_OF_CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CODE_OF_CONDUCT.md; sourceTree = ""; }; + 5BB5732D21DE4024008895BB /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 5BB5732E21DE4024008895BB /* RNDeviceInfo.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = RNDeviceInfo.podspec; sourceTree = ""; }; + 5BB5732F21DE4024008895BB /* deviceinfo.js.flow */ = {isa = PBXFileReference; lastKnownFileType = text; path = deviceinfo.js.flow; sourceTree = ""; }; + 5BB5733021DE4024008895BB /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 5BB5733221DE4024008895BB /* index.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = ""; }; + 5BB5733321DE4024008895BB /* deviceinfo.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = deviceinfo.js; sourceTree = ""; }; + 5BB5733621DE4024008895BB /* RNDeviceInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNDeviceInfo.m; sourceTree = ""; }; + 5BB5733721DE4024008895BB /* DeviceUID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeviceUID.h; sourceTree = ""; }; + 5BB5733821DE4024008895BB /* DeviceUID.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeviceUID.m; sourceTree = ""; }; + 5BB5733921DE4024008895BB /* RNDeviceInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNDeviceInfo.h; sourceTree = ""; }; + 5BB5733A21DE4024008895BB /* RNDeviceInfo.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = RNDeviceInfo.xcodeproj; sourceTree = ""; }; + 5BB5733D21DE4024008895BB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 5BB5733E21DE4024008895BB /* deviceinfo.d.ts */ = {isa = PBXFileReference; lastKnownFileType = text; path = deviceinfo.d.ts; sourceTree = ""; }; + 5BB5733F21DE4024008895BB /* yarn.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = yarn.lock; sourceTree = ""; }; + 5BB5734021DE4024008895BB /* package.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = package.json; sourceTree = ""; }; + 5BB5734121DE4024008895BB /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; + 5BB5734321DE4024008895BB /* build.gradle */ = {isa = PBXFileReference; lastKnownFileType = text; path = build.gradle; sourceTree = ""; }; + 5BB5734621DE4024008895BB /* AndroidManifest.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = AndroidManifest.xml; sourceTree = ""; }; + 5BB5734B21DE4024008895BB /* RNDeviceReceiver.java */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.java; path = RNDeviceReceiver.java; sourceTree = ""; }; + 5BB5734C21DE4024008895BB /* RNDeviceInfo.java */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.java; path = RNDeviceInfo.java; sourceTree = ""; }; + 5BB5734D21DE4024008895BB /* RNDeviceModule.java */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.java; path = RNDeviceModule.java; sourceTree = ""; }; + 5BB5735021DE4024008895BB /* RNDeviceInfoModule.cs */ = {isa = PBXFileReference; lastKnownFileType = text; path = RNDeviceInfoModule.cs; sourceTree = ""; }; + 5BB5735221DE4024008895BB /* AssemblyInfo.cs */ = {isa = PBXFileReference; lastKnownFileType = text; path = AssemblyInfo.cs; sourceTree = ""; }; + 5BB5735321DE4024008895BB /* RNDeviceInfo.rd.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = RNDeviceInfo.rd.xml; sourceTree = ""; }; + 5BB5735421DE4024008895BB /* RNDeviceInfo.csproj */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = RNDeviceInfo.csproj; sourceTree = ""; }; + 5BB5735521DE4024008895BB /* RNDeviceInfoPackage.cs */ = {isa = PBXFileReference; lastKnownFileType = text; path = RNDeviceInfoPackage.cs; sourceTree = ""; }; + 5BB5735621DE4024008895BB /* RNDeviceInfo.sln */ = {isa = PBXFileReference; lastKnownFileType = text; path = RNDeviceInfo.sln; sourceTree = ""; }; + 5BB5735721DE4024008895BB /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + 5BD58B8622D71D7400D1CD2D /* Montserrat-LightItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-LightItalic.ttf"; path = "../assets/fonts/Montserrat-LightItalic.ttf"; sourceTree = ""; }; + 5BD58B8722D71D7400D1CD2D /* Montserrat-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraBoldItalic.ttf"; path = "../assets/fonts/Montserrat-ExtraBoldItalic.ttf"; sourceTree = ""; }; + 5BD58B8822D71D7500D1CD2D /* Montserrat-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-SemiBoldItalic.ttf"; path = "../assets/fonts/Montserrat-SemiBoldItalic.ttf"; sourceTree = ""; }; + 5BD58B8922D71D7500D1CD2D /* Montserrat-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Black.ttf"; path = "../assets/fonts/Montserrat-Black.ttf"; sourceTree = ""; }; + 5BD58B8A22D71D7500D1CD2D /* Montserrat-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Italic.ttf"; path = "../assets/fonts/Montserrat-Italic.ttf"; sourceTree = ""; }; + 5BD58B8B22D71D7500D1CD2D /* Montserrat-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ThinItalic.ttf"; path = "../assets/fonts/Montserrat-ThinItalic.ttf"; sourceTree = ""; }; + 5BD58B8C22D71D7500D1CD2D /* Montserrat-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Medium.ttf"; path = "../assets/fonts/Montserrat-Medium.ttf"; sourceTree = ""; }; + 5BD58B8D22D71D7500D1CD2D /* Montserrat-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Regular.ttf"; path = "../assets/fonts/Montserrat-Regular.ttf"; sourceTree = ""; }; + 5BD58B8E22D71D7500D1CD2D /* Montserrat-MediumItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-MediumItalic.ttf"; path = "../assets/fonts/Montserrat-MediumItalic.ttf"; sourceTree = ""; }; + 5BD58B8F22D71D7500D1CD2D /* Montserrat-ExtraLightItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraLightItalic.ttf"; path = "../assets/fonts/Montserrat-ExtraLightItalic.ttf"; sourceTree = ""; }; + 5BD58B9022D71D7500D1CD2D /* Montserrat-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-BoldItalic.ttf"; path = "../assets/fonts/Montserrat-BoldItalic.ttf"; sourceTree = ""; }; + 5BD58B9122D71D7500D1CD2D /* Montserrat-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-SemiBold.ttf"; path = "../assets/fonts/Montserrat-SemiBold.ttf"; sourceTree = ""; }; + 5BD58B9222D71D7500D1CD2D /* Montserrat-BlackItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-BlackItalic.ttf"; path = "../assets/fonts/Montserrat-BlackItalic.ttf"; sourceTree = ""; }; + 5BD58B9322D71D7500D1CD2D /* Montserrat-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraLight.ttf"; path = "../assets/fonts/Montserrat-ExtraLight.ttf"; sourceTree = ""; }; + 5BD58B9422D71D7600D1CD2D /* Montserrat-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-ExtraBold.ttf"; path = "../assets/fonts/Montserrat-ExtraBold.ttf"; sourceTree = ""; }; + 5BD58B9522D71D7600D1CD2D /* Montserrat-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Light.ttf"; path = "../assets/fonts/Montserrat-Light.ttf"; sourceTree = ""; }; + 5BD58B9622D71D7600D1CD2D /* Montserrat-Thin.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Thin.ttf"; path = "../assets/fonts/Montserrat-Thin.ttf"; sourceTree = ""; }; + 5BD58B9722D71D7600D1CD2D /* Montserrat-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Montserrat-Bold.ttf"; path = "../assets/fonts/Montserrat-Bold.ttf"; sourceTree = ""; }; + 5BE7E26423E98F21000FC447 /* Nyxo DEV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Nyxo DEV.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BF9811C22D2052900509ED1 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nyxo/Images.xcassets; sourceTree = ""; }; + 6CF46B6888304B68952AC827 /* Dosis-Medium.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Dosis-Medium.ttf"; path = "../assets/fonts/Dosis-Medium.ttf"; sourceTree = ""; }; + 796FDDB5089B4D6FBFF9AF7D /* FontAwesome5_Pro_Brands.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Pro_Brands.ttf; path = ../assets/fonts/FontAwesome5_Pro_Brands.ttf; sourceTree = ""; }; + 8299F6F8C97B11A56957F31C /* Pods-Nyxo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nyxo.debug.xcconfig"; path = "Target Support Files/Pods-Nyxo/Pods-Nyxo.debug.xcconfig"; sourceTree = ""; }; + 9F67C385EC2C483E6892E5F9 /* Pods-Nyxo Dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nyxo Dev.debug.xcconfig"; path = "Target Support Files/Pods-Nyxo Dev/Pods-Nyxo Dev.debug.xcconfig"; sourceTree = ""; }; + B64D42BFD4E244D9863E00B7 /* AppCenter-Config.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "AppCenter-Config.plist"; path = "Nyxo/AppCenter-Config.plist"; sourceTree = ""; }; + B959F1EA20BD3D8E00AC734C /* Dosis-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Dosis-Bold.ttf"; path = "../assets/fonts/Dosis-Bold.ttf"; sourceTree = ""; }; + B959F21820BD455C00AC734C /* fa-regular-400.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fa-regular-400.ttf"; sourceTree = ""; }; + B95CBA9B2099AEB300243A25 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; + BC20C0ADAE8344ED9E90B05C /* FontAwesome5_Pro_Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Pro_Light.ttf; path = ../assets/fonts/FontAwesome5_Pro_Light.ttf; sourceTree = ""; }; + C5FD1B15D7409AEE375B867C /* Pods-Nyxo Dev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nyxo Dev.release.xcconfig"; path = "Target Support Files/Pods-Nyxo Dev/Pods-Nyxo Dev.release.xcconfig"; sourceTree = ""; }; + E28D9DBBB35A494F8B7F80E4 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = ../assets/fonts/FontAwesome.ttf; sourceTree = ""; }; + E763E2F2E04548318B376785 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; + E89B28DB570F1784E077C6A0 /* libPods-Nyxo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nyxo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; + EDBCD013F22D4256B775A881 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; + F39D20A4A03A4E13BB614E17 /* FontAwesome5_Pro_Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Pro_Regular.ttf; path = ../assets/fonts/FontAwesome5_Pro_Regular.ttf; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */, + 5B20692D224E434B00257043 /* StoreKit.framework in Frameworks */, + B95CBA9C2099AEB300243A25 /* HealthKit.framework in Frameworks */, + 5B5476582233C6650027A9A0 /* Intercom.framework in Frameworks */, + C6BF152484FF448FA7435F49 /* libz.tbd in Frameworks */, + E1DF9C1DF40E9169E37562FD /* libPods-Nyxo.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE7E23123E98F21000FC447 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BE7E23223E98F21000FC447 /* AppCenterReactNativeShared.framework in Frameworks */, + 5BE7E23323E98F21000FC447 /* AppCenterCrashes.framework in Frameworks */, + 5BE7E23423E98F21000FC447 /* JavaScriptCore.framework in Frameworks */, + 5BE7E23523E98F21000FC447 /* StoreKit.framework in Frameworks */, + 5BE7E23623E98F21000FC447 /* HealthKit.framework in Frameworks */, + 5BE7E23723E98F21000FC447 /* Intercom.framework in Frameworks */, + 5BE7E23823E98F21000FC447 /* Fabric.framework in Frameworks */, + 5BE7E23923E98F21000FC447 /* AppCenterAnalytics.framework in Frameworks */, + 5BE7E23A23E98F21000FC447 /* AppCenter.framework in Frameworks */, + 5BE7E23B23E98F21000FC447 /* AppCenterPush.framework in Frameworks */, + 5BE7E23D23E98F21000FC447 /* libz.tbd in Frameworks */, + 6EF70286CEF4580632B0E50F /* libPods-Nyxo Dev.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E356EF1AD99517003FC87E /* NyxoTests */ = { + isa = PBXGroup; + children = ( + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = NyxoTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* Nyxo */ = { + isa = PBXGroup; + children = ( + 5B8EDB59242B6F4800A3796F /* GoogleService-Info.plist */, + 5B70355723E998420068DB97 /* Nyxo (DEV).plist */, + 5B33130421DFB5B800698A4A /* AppDelegate.h */, + 5B33130121DFB5B800698A4A /* AppDelegate.m */, + 5B3312FC21DFB5B800698A4A /* Base.lproj */, + 5B3312FB21DFB5B700698A4A /* Images.xcassets */, + 5B33130321DFB5B800698A4A /* Info.plist */, + 5BA0F90521E2411B00D3D56E /* main.jsbundle */, + 5B33130221DFB5B800698A4A /* main.m */, + 5B2B308921EF5C82007E2982 /* assets */, + 5B33136421DFB73A00698A4A /* Nyxo.entitlements */, + ); + name = Nyxo; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5B20692C224E434B00257043 /* StoreKit.framework */, + 5BB5732B21DE4024008895BB /* react-native-device-info */, + B95CBA9B2099AEB300243A25 /* HealthKit.framework */, + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + ED2971642150620600B7C4FE /* JavaScriptCore.framework */, + 2D16E6891FA4F8E400B85C8A /* libReact.a */, + 4BAC1FA2BBE14987AE9C5E6D /* libz.tbd */, + 140C71F65113D98617CEF82C /* libPods-Nyxo Dev.a */, + E89B28DB570F1784E077C6A0 /* libPods-Nyxo.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5B3312FC21DFB5B800698A4A /* Base.lproj */ = { + isa = PBXGroup; + children = ( + 5B3312FD21DFB5B800698A4A /* LaunchScreen.old.xib */, + 5B3312FF21DFB5B800698A4A /* LaunchScreen.xib */, + ); + name = Base.lproj; + path = Nyxo/Base.lproj; + sourceTree = ""; + }; + 5B50DCC1221882130036DFDD /* Vendor */ = { + isa = PBXGroup; + children = ( + 5B50DD33221884EB0036DFDD /* AppCenterPush.framework */, + 5B50DCC82218829A0036DFDD /* AppCenterReactNativeShared.framework */, + 5B50DCC3221882830036DFDD /* AppCenter.framework */, + 5B50DCC2221882830036DFDD /* AppCenterAnalytics.framework */, + 5B50DCC4221882830036DFDD /* AppCenterCrashes.framework */, + ); + name = Vendor; + sourceTree = ""; + }; + 5BA9115A22E1E2070098700A /* Shared */ = { + isa = PBXGroup; + children = ( + 5BA9115D22E1E2350098700A /* CommandStatus.swift */, + 5BA9115B22E1E2350098700A /* Nyxo-Bridging-Header.h */, + 5BA9115C22E1E2350098700A /* Nyxo Watch-Bridging-Header.h */, + ); + path = Shared; + sourceTree = ""; + }; + 5BB5732B21DE4024008895BB /* react-native-device-info */ = { + isa = PBXGroup; + children = ( + 5BB5732C21DE4024008895BB /* CODE_OF_CONDUCT.md */, + 5BB5732D21DE4024008895BB /* LICENSE */, + 5BB5732E21DE4024008895BB /* RNDeviceInfo.podspec */, + 5BB5732F21DE4024008895BB /* deviceinfo.js.flow */, + 5BB5733021DE4024008895BB /* CHANGELOG.md */, + 5BB5733121DE4024008895BB /* web */, + 5BB5733321DE4024008895BB /* deviceinfo.js */, + 5BB5733421DE4024008895BB /* ios */, + 5BB5733D21DE4024008895BB /* README.md */, + 5BB5733E21DE4024008895BB /* deviceinfo.d.ts */, + 5BB5733F21DE4024008895BB /* yarn.lock */, + 5BB5734021DE4024008895BB /* package.json */, + 5BB5734121DE4024008895BB /* CONTRIBUTING.md */, + 5BB5734221DE4024008895BB /* android */, + 5BB5734E21DE4024008895BB /* windows */, + ); + name = "react-native-device-info"; + path = "../react-native-device-info"; + sourceTree = ""; + }; + 5BB5733121DE4024008895BB /* web */ = { + isa = PBXGroup; + children = ( + 5BB5733221DE4024008895BB /* index.js */, + ); + path = web; + sourceTree = ""; + }; + 5BB5733421DE4024008895BB /* ios */ = { + isa = PBXGroup; + children = ( + 5BB5733521DE4024008895BB /* RNDeviceInfo */, + 5BB5733A21DE4024008895BB /* RNDeviceInfo.xcodeproj */, + ); + path = ios; + sourceTree = ""; + }; + 5BB5733521DE4024008895BB /* RNDeviceInfo */ = { + isa = PBXGroup; + children = ( + 5BB5733621DE4024008895BB /* RNDeviceInfo.m */, + 5BB5733721DE4024008895BB /* DeviceUID.h */, + 5BB5733821DE4024008895BB /* DeviceUID.m */, + 5BB5733921DE4024008895BB /* RNDeviceInfo.h */, + ); + path = RNDeviceInfo; + sourceTree = ""; + }; + 5BB5733B21DE4024008895BB /* Products */ = { + isa = PBXGroup; + children = ( + 5BB573A621DE40B8008895BB /* libRNDeviceInfo.a */, + 5BB5735D21DE4024008895BB /* libRNDeviceInfo-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; + 5BB5734221DE4024008895BB /* android */ = { + isa = PBXGroup; + children = ( + 5BB5734321DE4024008895BB /* build.gradle */, + 5BB5734421DE4024008895BB /* src */, + ); + path = android; + sourceTree = ""; + }; + 5BB5734421DE4024008895BB /* src */ = { + isa = PBXGroup; + children = ( + 5BB5734521DE4024008895BB /* main */, + ); + path = src; + sourceTree = ""; + }; + 5BB5734521DE4024008895BB /* main */ = { + isa = PBXGroup; + children = ( + 5BB5734621DE4024008895BB /* AndroidManifest.xml */, + 5BB5734721DE4024008895BB /* java */, + ); + path = main; + sourceTree = ""; + }; + 5BB5734721DE4024008895BB /* java */ = { + isa = PBXGroup; + children = ( + 5BB5734821DE4024008895BB /* com */, + ); + path = java; + sourceTree = ""; + }; + 5BB5734821DE4024008895BB /* com */ = { + isa = PBXGroup; + children = ( + 5BB5734921DE4024008895BB /* learnium */, + ); + path = com; + sourceTree = ""; + }; + 5BB5734921DE4024008895BB /* learnium */ = { + isa = PBXGroup; + children = ( + 5BB5734A21DE4024008895BB /* RNDeviceInfo */, + ); + path = learnium; + sourceTree = ""; + }; + 5BB5734A21DE4024008895BB /* RNDeviceInfo */ = { + isa = PBXGroup; + children = ( + 5BB5734B21DE4024008895BB /* RNDeviceReceiver.java */, + 5BB5734C21DE4024008895BB /* RNDeviceInfo.java */, + 5BB5734D21DE4024008895BB /* RNDeviceModule.java */, + ); + path = RNDeviceInfo; + sourceTree = ""; + }; + 5BB5734E21DE4024008895BB /* windows */ = { + isa = PBXGroup; + children = ( + 5BB5734F21DE4024008895BB /* RNDeviceInfo */, + 5BB5735721DE4024008895BB /* .gitignore */, + ); + path = windows; + sourceTree = ""; + }; + 5BB5734F21DE4024008895BB /* RNDeviceInfo */ = { + isa = PBXGroup; + children = ( + 5BB5735021DE4024008895BB /* RNDeviceInfoModule.cs */, + 5BB5735121DE4024008895BB /* Properties */, + 5BB5735421DE4024008895BB /* RNDeviceInfo.csproj */, + 5BB5735521DE4024008895BB /* RNDeviceInfoPackage.cs */, + 5BB5735621DE4024008895BB /* RNDeviceInfo.sln */, + ); + path = RNDeviceInfo; + sourceTree = ""; + }; + 5BB5735121DE4024008895BB /* Properties */ = { + isa = PBXGroup; + children = ( + 5BB5735221DE4024008895BB /* AssemblyInfo.cs */, + 5BB5735321DE4024008895BB /* RNDeviceInfo.rd.xml */, + ); + path = Properties; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 5B7820DF24EEC2360074BAFC /* rnuc.xcconfig */, + 5B40961E242BAE0000169B4C /* GoogleService-Info.plist */, + 5BA9115A22E1E2070098700A /* Shared */, + 5BD58B8922D71D7500D1CD2D /* Montserrat-Black.ttf */, + 5BD58B9222D71D7500D1CD2D /* Montserrat-BlackItalic.ttf */, + 5BD58B9722D71D7600D1CD2D /* Montserrat-Bold.ttf */, + 5BD58B9022D71D7500D1CD2D /* Montserrat-BoldItalic.ttf */, + 5BD58B9422D71D7600D1CD2D /* Montserrat-ExtraBold.ttf */, + 5BD58B8722D71D7400D1CD2D /* Montserrat-ExtraBoldItalic.ttf */, + 5BD58B9322D71D7500D1CD2D /* Montserrat-ExtraLight.ttf */, + 5BD58B8F22D71D7500D1CD2D /* Montserrat-ExtraLightItalic.ttf */, + 5B090C40235912AC00FD7E9B /* Domine-Bold.ttf */, + 5B090C41235912AC00FD7E9B /* Domine-Regular.ttf */, + 5BD58B8A22D71D7500D1CD2D /* Montserrat-Italic.ttf */, + 5BD58B9522D71D7600D1CD2D /* Montserrat-Light.ttf */, + 5BD58B8622D71D7400D1CD2D /* Montserrat-LightItalic.ttf */, + 5BD58B8C22D71D7500D1CD2D /* Montserrat-Medium.ttf */, + 5BD58B8E22D71D7500D1CD2D /* Montserrat-MediumItalic.ttf */, + 5BD58B8D22D71D7500D1CD2D /* Montserrat-Regular.ttf */, + 5BD58B9122D71D7500D1CD2D /* Montserrat-SemiBold.ttf */, + 5BD58B8822D71D7500D1CD2D /* Montserrat-SemiBoldItalic.ttf */, + 5BD58B9622D71D7600D1CD2D /* Montserrat-Thin.ttf */, + 5BD58B8B22D71D7500D1CD2D /* Montserrat-ThinItalic.ttf */, + 5BF9811C22D2052900509ED1 /* Images.xcassets */, + 5B5476152233C6640027A9A0 /* Intercom.framework */, + 5B50DCC1221882130036DFDD /* Vendor */, + 13B07FAE1A68108700A75B9A /* Nyxo */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* NyxoTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + B95CBA5D2099AE3200243A25 /* Recovered References */, + 94F1E8A8CFBD46088A2BB76D /* Resources */, + B64D42BFD4E244D9863E00B7 /* AppCenter-Config.plist */, + A166C399641673F389718B26 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Nyxo.app */, + 00E356EE1AD99517003FC87E /* NyxoTests.xctest */, + 5BE7E26423E98F21000FC447 /* Nyxo DEV.app */, + ); + name = Products; + sourceTree = ""; + }; + 94F1E8A8CFBD46088A2BB76D /* Resources */ = { + isa = PBXGroup; + children = ( + 5B75EE3A2188A8920070EB69 /* Crashlytics.framework */, + 5B75EE392188A8920070EB69 /* Fabric.framework */, + B959F1EA20BD3D8E00AC734C /* Dosis-Bold.ttf */, + B959F21820BD455C00AC734C /* fa-regular-400.ttf */, + EDBCD013F22D4256B775A881 /* FontAwesome.ttf */, + 5B2069A4224FB24200257043 /* Lato-Regular.ttf */, + E763E2F2E04548318B376785 /* Ionicons.ttf */, + 5B37A98922CE1A4900820944 /* Montserrat-Black.ttf */, + 5B37A98822CE1A4900820944 /* Montserrat-BlackItalic.ttf */, + 5B37A98522CE1A4900820944 /* Montserrat-Bold.ttf */, + 5B37A97F22CE1A4800820944 /* Montserrat-BoldItalic.ttf */, + 5B37A98722CE1A4900820944 /* Montserrat-ExtraBold.ttf */, + 5B37A97C22CE1A4800820944 /* Montserrat-ExtraBoldItalic.ttf */, + 5B37A97B22CE1A4800820944 /* Montserrat-ExtraLight.ttf */, + 5B37A98022CE1A4800820944 /* Montserrat-ExtraLightItalic.ttf */, + 5B37A97E22CE1A4800820944 /* Montserrat-Italic.ttf */, + 5B37A98322CE1A4800820944 /* Montserrat-Light.ttf */, + 5B37A98622CE1A4900820944 /* Montserrat-LightItalic.ttf */, + 5B37A98A22CE1A4900820944 /* Montserrat-Medium.ttf */, + 5B37A98422CE1A4900820944 /* Montserrat-MediumItalic.ttf */, + 5B37A98B22CE1A4A00820944 /* Montserrat-Regular.ttf */, + 5B37A98122CE1A4800820944 /* Montserrat-SemiBold.ttf */, + 5B37A97D22CE1A4800820944 /* Montserrat-SemiBoldItalic.ttf */, + 5B37A97A22CE1A4700820944 /* Montserrat-Thin.ttf */, + 5B37A98222CE1A4800820944 /* Montserrat-ThinItalic.ttf */, + 6CF46B6888304B68952AC827 /* Dosis-Medium.ttf */, + 796FDDB5089B4D6FBFF9AF7D /* FontAwesome5_Pro_Brands.ttf */, + BC20C0ADAE8344ED9E90B05C /* FontAwesome5_Pro_Light.ttf */, + 5B37A97122CE0B6700820944 /* Lato-Black.ttf */, + 5B37A97322CE0B6700820944 /* Lato-BlackItalic.ttf */, + 5B37A97922CE0B6C00820944 /* Lato Italic.ttf */, + 5B37A97622CE0B6700820944 /* Lato-Bold.ttf */, + 5B37A97522CE0B6700820944 /* Lato-BoldItalic.ttf */, + 5B37A97422CE0B6700820944 /* Lato-Hairline.ttf */, + 5B37A97222CE0B6700820944 /* Lato-HairlineItalic.ttf */, + 5B37A97722CE0B6700820944 /* Lato-Light.ttf */, + 5B37A97822CE0B6700820944 /* Lato-LightItalic.ttf */, + F39D20A4A03A4E13BB614E17 /* FontAwesome5_Pro_Regular.ttf */, + 4F61ADB20DA8429D927A20BC /* FontAwesome5_Pro_Solid.ttf */, + E28D9DBBB35A494F8B7F80E4 /* FontAwesome.ttf */, + ); + name = Resources; + sourceTree = ""; + }; + A166C399641673F389718B26 /* Pods */ = { + isa = PBXGroup; + children = ( + 8299F6F8C97B11A56957F31C /* Pods-Nyxo.debug.xcconfig */, + 00814AB27B8A4287577099B1 /* Pods-Nyxo.release.xcconfig */, + 9F67C385EC2C483E6892E5F9 /* Pods-Nyxo Dev.debug.xcconfig */, + C5FD1B15D7409AEE375B867C /* Pods-Nyxo Dev.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + B95CBA5D2099AE3200243A25 /* Recovered References */ = { + isa = PBXGroup; + children = ( + ); + name = "Recovered References"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* NyxoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "NyxoTests" */; + buildPhases = ( + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = NyxoTests; + productName = NyxoTests; + productReference = 00E356EE1AD99517003FC87E /* NyxoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* Nyxo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nyxo" */; + buildPhases = ( + BF842D7B9E4CC79A1B9C718D /* [CP] Check Pods Manifest.lock */, + 5BBCCCD22472B0940047CD86 /* ShellScript */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 5B6E854A21406B9A0013E7B5 /* Embed Watch Content */, + 5B54765A2233C6650027A9A0 /* Embed Frameworks */, + 5B5476C22233EC8A0027A9A0 /* Run Script */, + 6C3BA05A2308A4078CE786D2 /* [CP] Copy Pods Resources */, + DED22F96C89D448186375252 /* Upload Debug Symbols to Sentry */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Nyxo; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* Nyxo.app */; + productType = "com.apple.product-type.application"; + }; + 5BE7E22B23E98F21000FC447 /* Nyxo Dev */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BE7E26123E98F21000FC447 /* Build configuration list for PBXNativeTarget "Nyxo Dev" */; + buildPhases = ( + 882FBEE2C42D54BF5ABCD2A6 /* [CP] Check Pods Manifest.lock */, + 5BE7E22D23E98F21000FC447 /* Sources */, + 5BE7E23123E98F21000FC447 /* Frameworks */, + 5BE7E23F23E98F21000FC447 /* Resources */, + 5BE7E25923E98F21000FC447 /* Bundle React Native code and images */, + 5BE7E25A23E98F21000FC447 /* Embed Watch Content */, + 5BE7E25B23E98F21000FC447 /* ShellScript */, + 5BE7E25C23E98F21000FC447 /* Embed Frameworks */, + 5BE7E25E23E98F21000FC447 /* Run Script */, + 5BE7E26023E98F21000FC447 /* Upload Debug Symbols to Sentry */, + BFBB280D8235AFF843AC9293 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Nyxo Dev"; + productName = "Hello World"; + productReference = 5BE7E26423E98F21000FC447 /* Nyxo DEV.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 940; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = RPKZ2YP3VZ; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + 13B07F861A680F5B00A75B9A = { + DevelopmentTeam = RPKZ2YP3VZ; + LastSwiftMigration = 1020; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.HealthKit = { + enabled = 1; + }; + com.apple.InAppPurchase = { + enabled = 1; + }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.Push = { + enabled = 1; + }; + }; + }; + 5BE7E22B23E98F21000FC447 = { + DevelopmentTeam = RPKZ2YP3VZ; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nyxo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + Base, + fi, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 5BB5733B21DE4024008895BB /* Products */; + ProjectRef = 5BB5733A21DE4024008895BB /* RNDeviceInfo.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Nyxo */, + 00E356ED1AD99517003FC87E /* NyxoTests */, + 5BE7E22B23E98F21000FC447 /* Nyxo Dev */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 5BB5735D21DE4024008895BB /* libRNDeviceInfo-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRNDeviceInfo-tvOS.a"; + remoteRef = 5BB5735C21DE4024008895BB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 5BB573A621DE40B8008895BB /* libRNDeviceInfo.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNDeviceInfo.a; + remoteRef = 5BB573A521DE40B8008895BB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BD58B9822D71D7600D1CD2D /* Montserrat-LightItalic.ttf in Resources */, + 5BD58B9922D71D7600D1CD2D /* Montserrat-ExtraBoldItalic.ttf in Resources */, + 5BD58B9A22D71D7600D1CD2D /* Montserrat-SemiBoldItalic.ttf in Resources */, + 5BD58B9B22D71D7600D1CD2D /* Montserrat-Black.ttf in Resources */, + 5BD58B9C22D71D7600D1CD2D /* Montserrat-Italic.ttf in Resources */, + 5BD58B9D22D71D7600D1CD2D /* Montserrat-ThinItalic.ttf in Resources */, + 5BD58B9E22D71D7600D1CD2D /* Montserrat-Medium.ttf in Resources */, + 5B090C43235912AC00FD7E9B /* Domine-Regular.ttf in Resources */, + 5BD58B9F22D71D7600D1CD2D /* Montserrat-Regular.ttf in Resources */, + 5BD58BA022D71D7600D1CD2D /* Montserrat-MediumItalic.ttf in Resources */, + 5BD58BA122D71D7600D1CD2D /* Montserrat-ExtraLightItalic.ttf in Resources */, + 5B40961F242BAE0000169B4C /* GoogleService-Info.plist in Resources */, + 5BD58BA222D71D7600D1CD2D /* Montserrat-BoldItalic.ttf in Resources */, + 5BD58BA322D71D7600D1CD2D /* Montserrat-SemiBold.ttf in Resources */, + 5BD58BA422D71D7600D1CD2D /* Montserrat-BlackItalic.ttf in Resources */, + 5BD58BA522D71D7600D1CD2D /* Montserrat-ExtraLight.ttf in Resources */, + 5BD58BA622D71D7600D1CD2D /* Montserrat-ExtraBold.ttf in Resources */, + 5BD58BA722D71D7600D1CD2D /* Montserrat-Light.ttf in Resources */, + 5BD58BA822D71D7600D1CD2D /* Montserrat-Thin.ttf in Resources */, + 5BD58BA922D71D7600D1CD2D /* Montserrat-Bold.ttf in Resources */, + 5B7820E024EEC2360074BAFC /* rnuc.xcconfig in Resources */, + 5BA0F90621E2411B00D3D56E /* main.jsbundle in Resources */, + 5BF9816322D2052900509ED1 /* Images.xcassets in Resources */, + 5B090C42235912AC00FD7E9B /* Domine-Bold.ttf in Resources */, + 5C56F98D642F4D5BA2168C40 /* AppCenter-Config.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE7E23F23E98F21000FC447 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B70355823E998420068DB97 /* Nyxo (DEV).plist in Resources */, + 5BE7E24023E98F21000FC447 /* Montserrat-LightItalic.ttf in Resources */, + 5BE7E24123E98F21000FC447 /* Montserrat-ExtraBoldItalic.ttf in Resources */, + 5BE7E24223E98F21000FC447 /* Montserrat-SemiBoldItalic.ttf in Resources */, + 5BE7E24323E98F21000FC447 /* Montserrat-Black.ttf in Resources */, + 5BE7E24423E98F21000FC447 /* Montserrat-Italic.ttf in Resources */, + 5BE7E24523E98F21000FC447 /* Montserrat-ThinItalic.ttf in Resources */, + 5BE7E24623E98F21000FC447 /* Montserrat-Medium.ttf in Resources */, + 5BE7E24723E98F21000FC447 /* Domine-Regular.ttf in Resources */, + 5BE7E24823E98F21000FC447 /* Montserrat-Regular.ttf in Resources */, + 5BE7E24923E98F21000FC447 /* Montserrat-MediumItalic.ttf in Resources */, + 5BE7E24A23E98F21000FC447 /* Montserrat-ExtraLightItalic.ttf in Resources */, + 5BE7E24B23E98F21000FC447 /* Montserrat-BoldItalic.ttf in Resources */, + 5BE7E24C23E98F21000FC447 /* Montserrat-SemiBold.ttf in Resources */, + 5BE7E24D23E98F21000FC447 /* Montserrat-BlackItalic.ttf in Resources */, + 5BE7E24E23E98F21000FC447 /* Montserrat-ExtraLight.ttf in Resources */, + 5BE7E24F23E98F21000FC447 /* Montserrat-ExtraBold.ttf in Resources */, + 5BE7E25023E98F21000FC447 /* Montserrat-Light.ttf in Resources */, + 5BE7E25123E98F21000FC447 /* Montserrat-Thin.ttf in Resources */, + 5BE7E25223E98F21000FC447 /* Montserrat-Bold.ttf in Resources */, + 5BE7E25323E98F21000FC447 /* main.jsbundle in Resources */, + 5BE7E25423E98F21000FC447 /* Images.xcassets in Resources */, + 5BE7E25523E98F21000FC447 /* Images.xcassets in Resources */, + 5BE7E25623E98F21000FC447 /* Domine-Bold.ttf in Resources */, + 5BE7E25723E98F21000FC447 /* FontAwesome5_Pro_Brands.ttf in Resources */, + 5BE7E25823E98F21000FC447 /* AppCenter-Config.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 12; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport NODE_BINARY=NODE\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh\n"; + }; + 5B5476C22233EC8A0027A9A0 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nbash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Intercom.framework/strip-frameworks.sh\"\n"; + }; + 5BBCCCD22472B0940047CD86 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + }; + 5BE7E25923E98F21000FC447 /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport NODE_BINARY=node\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh\n"; + }; + 5BE7E25B23E98F21000FC447 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 12; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "./Fabric.framework/run 421e3a33c9d2fd64e229fdc00d5238c59e9b66fa 2e33ae811ee810ee4ea132096a6d77d57066444e3fbb1a29d1d98bce60d8c19e\n"; + }; + 5BE7E25E23E98F21000FC447 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nbash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Intercom.framework/strip-frameworks.sh\"\n"; + }; + 5BE7E26023E98F21000FC447 /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=sentry.properties\n../node_modules/@sentry/cli/bin/sentry-cli upload-dsym"; + }; + 6C3BA05A2308A4078CE786D2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Nyxo/Pods-Nyxo-resources.sh", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/Intercom.bundle", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/IntercomTranslations.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Intercom.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IntercomTranslations.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nyxo/Pods-Nyxo-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 882FBEE2C42D54BF5ABCD2A6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Nyxo Dev-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BF842D7B9E4CC79A1B9C718D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Nyxo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BFBB280D8235AFF843AC9293 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Nyxo Dev/Pods-Nyxo Dev-resources.sh", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/Intercom.bundle", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/IntercomTranslations.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Intercom.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IntercomTranslations.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nyxo Dev/Pods-Nyxo Dev-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DED22F96C89D448186375252 /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=sentry.properties\n../node_modules/@sentry/cli/bin/sentry-cli upload-dsym"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B33130A21DFB5B800698A4A /* main.m in Sources */, + 5BA9115E22E1E2350098700A /* CommandStatus.swift in Sources */, + 5B33130921DFB5B800698A4A /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE7E22D23E98F21000FC447 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BE7E22E23E98F21000FC447 /* main.m in Sources */, + 5BE7E22F23E98F21000FC447 /* CommandStatus.swift in Sources */, + 5BE7E23023E98F21000FC447 /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* Nyxo */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 5B3312FD21DFB5B800698A4A /* LaunchScreen.old.xib */ = { + isa = PBXVariantGroup; + children = ( + 5B3312FE21DFB5B800698A4A /* Base */, + 5B48388421FA22EE003855DF /* fi */, + ); + name = LaunchScreen.old.xib; + sourceTree = ""; + }; + 5B3312FF21DFB5B800698A4A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 5B33130021DFB5B800698A4A /* Base */, + 5B48388521FA22EE003855DF /* fi */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../node_modules/rn-apple-healthkit/RCTAppleHealthKit", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-haptic-feedback/ios", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/../node_modules/appcenter/ios/AppCenterReactNative", + "$(SRCROOT)/../node_modules/appcenter-analytics/ios/AppCenterReactNativeAnalytics", + "$(SRCROOT)/../node_modules/appcenter-crashes/ios/AppCenterReactNativeCrashes", + "$(SRCROOT)/../node_modules/appcenter-push/ios/AppCenterReactNativePush", + "$(SRCROOT)/../node_modules/react-native-background-fetch/ios/RNBackgroundFetch/**", + "$(SRCROOT)/../node_modules/react-native-intercom/iOS", + "$(SRCROOT)/../node_modules/react-native-reanimated/ios/**", + "$(SRCROOT)/../node_modules/react-native-svg/ios/**", + "$(SRCROOT)/../node_modules/react-native-screens/ios", + ); + INFOPLIST_FILE = NyxoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nyxo.app/Nyxo"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../node_modules/rn-apple-healthkit/RCTAppleHealthKit", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-haptic-feedback/ios", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/../node_modules/appcenter/ios/AppCenterReactNative", + "$(SRCROOT)/../node_modules/appcenter-analytics/ios/AppCenterReactNativeAnalytics", + "$(SRCROOT)/../node_modules/appcenter-crashes/ios/AppCenterReactNativeCrashes", + "$(SRCROOT)/../node_modules/appcenter-push/ios/AppCenterReactNativePush", + "$(SRCROOT)/../node_modules/react-native-background-fetch/ios/RNBackgroundFetch/**", + "$(SRCROOT)/../node_modules/react-native-intercom/iOS", + "$(SRCROOT)/../node_modules/react-native-reanimated/ios/**", + "$(SRCROOT)/../node_modules/react-native-svg/ios/**", + "$(SRCROOT)/../node_modules/react-native-screens/ios", + ); + INFOPLIST_FILE = NyxoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "d1d3a31a-831f-44c7-be9d-541c01656f20"; + PROVISIONING_PROFILE_SPECIFIER = "app.sleepcircle.application AppStore"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nyxo.app/Nyxo"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8299F6F8C97B11A56957F31C /* Pods-Nyxo.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = Launch; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Nyxo/Nyxo.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 41; + DEAD_CODE_STRIPPING = NO; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/React", + "$(SRCROOT)/../node_modules/react-native/Libraries", + "$(inherited)", + ); + INFOPLIST_FILE = Nyxo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.4.0; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.sleepcircle.application; + PRODUCT_NAME = Nyxo; + PROVISIONING_PROFILE_SPECIFIER = "match Development app.sleepcircle.application"; + SWIFT_OBJC_BRIDGING_HEADER = "Shared/Nyxo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 00814AB27B8A4287577099B1 /* Pods-Nyxo.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = Launch; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Nyxo/Nyxo.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 41; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/React", + "$(SRCROOT)/../node_modules/react-native/Libraries", + "$(inherited)", + ); + INFOPLIST_FILE = Nyxo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.4.0; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.sleepcircle.application; + PRODUCT_NAME = Nyxo; + PROVISIONING_PROFILE = "d1d3a31a-831f-44c7-be9d-541c01656f20"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore app.sleepcircle.application"; + SWIFT_OBJC_BRIDGING_HEADER = "Shared/Nyxo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 5BE7E26223E98F21000FC447 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9F67C385EC2C483E6892E5F9 /* Pods-Nyxo Dev.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = Launch; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Nyxo/Nyxo.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 41; + DEAD_CODE_STRIPPING = NO; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/Vendor/AppCenter-SDK-Apple/iOS", + "$(PROJECT_DIR)/Vendor/AppCenterReactNativeShared", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/React", + "$(SRCROOT)/../node_modules/react-native/Libraries", + "$(inherited)", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-fabric/ios/SMXCrashlytics", + "$(SRCROOT)/../node_modules/react-native-haptic-feedback/ios", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/../node_modules/amazon-cognito-identity-js/ios", + "$(SRCROOT)/../node_modules/appcenter/ios/AppCenterReactNative", + "$(SRCROOT)/../node_modules/appcenter-analytics/ios/AppCenterReactNativeAnalytics", + "$(SRCROOT)/../node_modules/appcenter-crashes/ios/AppCenterReactNativeCrashes", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/appcenter-push/ios/AppCenterReactNativePush", + "$(SRCROOT)/../node_modules/react-native-background-fetch/ios/RNBackgroundFetch/**", + "$(SRCROOT)/../node_modules/react-native-intercom/iOS", + "$(SRCROOT)/../node_modules/react-native-reanimated/ios/**", + "$(SRCROOT)/../node_modules/react-native-svg/ios/**", + "$(SRCROOT)/../node_modules/react-native-screens/ios", + "$(SRCROOT)/../node_modules/react-native-healthkit/RCTAppleHealthKit", + ); + INFOPLIST_FILE = "$(SRCROOT)/Nyxo (DEV).plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.4.0; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.sleepcircle.application.dev; + PRODUCT_NAME = "Nyxo DEV"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Shared/Nyxo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 5BE7E26323E98F21000FC447 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C5FD1B15D7409AEE375B867C /* Pods-Nyxo Dev.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = Launch; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Nyxo/Nyxo.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 41; + DEVELOPMENT_TEAM = RPKZ2YP3VZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/Vendor/AppCenter-SDK-Apple/iOS", + "$(PROJECT_DIR)/Vendor/AppCenterReactNativeShared", + "$(PROJECT_DIR)/../node_modules/react-native-background-fetch/ios/**", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/React", + "$(SRCROOT)/../node_modules/react-native/Libraries", + "$(inherited)", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-fabric/ios/SMXCrashlytics", + "$(SRCROOT)/../node_modules/react-native-haptic-feedback/ios", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/../node_modules/amazon-cognito-identity-js/ios", + "$(SRCROOT)/../node_modules/appcenter/ios/AppCenterReactNative", + "$(SRCROOT)/../node_modules/appcenter-analytics/ios/AppCenterReactNativeAnalytics", + "$(SRCROOT)/../node_modules/appcenter-crashes/ios/AppCenterReactNativeCrashes", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/react-native-code-push/ios", + "$(SRCROOT)/../node_modules/appcenter-push/ios/AppCenterReactNativePush", + "$(SRCROOT)/../node_modules/react-native-background-fetch/ios/RNBackgroundFetch/**", + "$(SRCROOT)/../node_modules/react-native-intercom/iOS", + "$(SRCROOT)/../node_modules/react-native-reanimated/ios/**", + "$(SRCROOT)/../node_modules/react-native-svg/ios/**", + "$(SRCROOT)/../node_modules/react-native-screens/ios", + "$(SRCROOT)/../node_modules/react-native-healthkit/RCTAppleHealthKit", + ); + INFOPLIST_FILE = "$(SRCROOT)/Nyxo (DEV).plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.4.0; + OTHER_CFLAGS = ( + "$(inherited)", + "-DFB_SONARKIT_ENABLED=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.sleepcircle.application.dev; + PRODUCT_NAME = "Nyxo DEV"; + PROVISIONING_PROFILE = "d1d3a31a-831f-44c7-be9d-541c01656f20"; + PROVISIONING_PROFILE_SPECIFIER = "app.sleepcircle.application AppStore"; + SWIFT_OBJC_BRIDGING_HEADER = "Shared/Nyxo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5B7820DF24EEC2360074BAFC /* rnuc.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5B7820DF24EEC2360074BAFC /* rnuc.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "NyxoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nyxo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BE7E26123E98F21000FC447 /* Build configuration list for PBXNativeTarget "Nyxo Dev" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BE7E26223E98F21000FC447 /* Debug */, + 5BE7E26323E98F21000FC447 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nyxo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/Nyxo.xcodeproj/xcshareddata/xcschemes/Nyxo.xcscheme b/ios/Nyxo.xcodeproj/xcshareddata/xcschemes/Nyxo.xcscheme new file mode 100644 index 0000000..c26ddf3 --- /dev/null +++ b/ios/Nyxo.xcodeproj/xcshareddata/xcschemes/Nyxo.xcscheme @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Nyxo.xcworkspace/contents.xcworkspacedata b/ios/Nyxo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0016569 --- /dev/null +++ b/ios/Nyxo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Nyxo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Nyxo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Nyxo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Nyxo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Nyxo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..6b30c74 --- /dev/null +++ b/ios/Nyxo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + BuildSystemType + Original + PreviewsEnabled + + + diff --git a/ios/Nyxo/AppDelegate.h b/ios/Nyxo/AppDelegate.h new file mode 100644 index 0000000..6910b4e --- /dev/null +++ b/ios/Nyxo/AppDelegate.h @@ -0,0 +1,14 @@ + +#import +#import +#import +#import "RNAppAuthAuthorizationFlowManager.h" + + +@interface AppDelegate : UIResponder + +@property(nonatomic, weak)idauthorizationFlowManagerDelegate; +@property(nonatomic, strong) UIWindow *window; +@property(nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; + +@end diff --git a/ios/Nyxo/AppDelegate.m b/ios/Nyxo/AppDelegate.m new file mode 100644 index 0000000..fd35f3e --- /dev/null +++ b/ios/Nyxo/AppDelegate.m @@ -0,0 +1,198 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + +/* react-native-firebase */ + #import + #import "RNFirebaseNotifications.h" + +/* react-native-community/push-notification-ios */ +#import +#import +/* react-native-community/push-notification-ios */ + +#import "AppDelegate.h" +#import +#import +#import + + +#import +#import +#import +#import + +#import "AppCenterReactNative.h" +#import "AppCenterReactNativeAnalytics.h" +#import "AppCenterReactNativeCrashes.h" + +#import +#import +#import "Intercom/intercom.h" +#import + +#import +#import +#import + +#import "RNSplashScreen.h" + +#if DEBUG +#import +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + + + +@implementation AppDelegate + +/* react-native-community/push-notification-ios */ +// Required to register for notifications +- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings +{ + [RNCPushNotificationIOS didRegisterUserNotificationSettings:notificationSettings]; +} + +// Required for the register event. +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + // Intercom + [Intercom setDeviceToken:deviceToken]; +} + +// Required for the notification event. You must call the completion handler after handling the remote notification. +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +// Required for the registrationError event. +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; +} + +// Required for the localNotification event. +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification +{ +// [RNCPushNotificationIOS didReceiveLocalNotification:notification]; + [[RNFirebaseNotifications instance] didReceiveLocalNotification:notification]; +} +/* react-native-community/push-notification-ios */ + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + /* react-native-firebase */ + [FIRApp configure]; + [RNFirebaseNotifications configure]; + + #if DEBUG +// InitializeFlipper(application); + #endif + + self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge + moduleName:@"Nyxo" + initialProperties:nil]; + + + NSURL *jsCodeLocation; + // Initialize BackgroundFetch + [[TSBackgroundFetch sharedInstance] didFinishLaunching]; + + [AppCenterReactNative register]; // Initialize AppCenter + [AppCenterReactNativeCrashes registerWithAutomaticProcessing]; // Initialize AppCenter crashes + [AppCenterReactNativeAnalytics registerWithInitiallyEnabled:false]; // Initialize AppCenter analytics + + + [Intercom setApiKey:INTERCOM_KEY_IOS forAppId:INTERCOM_ID]; // Initialize Intercom + + rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; + + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + [RNSplashScreen show]; + + /* react-native-community/push-notification-ios */ + // Define UNUserNotificationCenter + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = self; + /* react-native-community/push-notification-ios */ + + + + + return YES; + +} + + + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + NSArray> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge]; + // You can inject any extra modules that you would like here, more information at: + // https://facebook.github.io/react-native/docs/native-modules-ios.html#dependency-injection + return extraModules; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Add this above `@end`: +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity + restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +/* react-native-community/push-notification-ios */ +// Called when a notification is delivered to a foreground app. + -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler + { + completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge); + } +/* react-native-community/push-notification-ios */ + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:app openURL:url options:options]; +} +@end + + diff --git a/ios/Nyxo/Base.lproj/LaunchScreen.xib b/ios/Nyxo/Base.lproj/LaunchScreen.xib new file mode 100644 index 0000000..eea7d9b --- /dev/null +++ b/ios/Nyxo/Base.lproj/LaunchScreen.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..34debcb --- /dev/null +++ b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,149 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon_20pt@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon_20pt@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon_29pt.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon_29pt@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon_29pt@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon_40pt@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon_40pt@3x.png", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon_60pt@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon_60pt@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "Icon-1.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon-1.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon-1.png new file mode 100644 index 0000000..3edcada Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon-1.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 0000000..7a62b66 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png new file mode 100644 index 0000000..3f6633d Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png new file mode 100644 index 0000000..d89d08c Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt.png new file mode 100644 index 0000000..e880eb1 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png new file mode 100644 index 0000000..74072b7 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png new file mode 100644 index 0000000..0bcd49e Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png new file mode 100644 index 0000000..56e3b32 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png new file mode 100644 index 0000000..10105c8 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png new file mode 100644 index 0000000..10105c8 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png differ diff --git a/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png new file mode 100644 index 0000000..04f8094 Binary files /dev/null and b/ios/Nyxo/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png differ diff --git a/ios/Nyxo/Images.xcassets/Contents.json b/ios/Nyxo/Images.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/ios/Nyxo/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/Nyxo/Images.xcassets/Launch.launchimage/Contents.json b/ios/Nyxo/Images.xcassets/Launch.launchimage/Contents.json new file mode 100644 index 0000000..b02f5d9 --- /dev/null +++ b/ios/Nyxo/Images.xcassets/Launch.launchimage/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "2688h", + "filename" : "iPhone XS Max.png", + "minimum-system-version" : "12.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "12.0", + "subtype" : "2688h", + "scale" : "3x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "12.0", + "subtype" : "1792h", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "12.0", + "subtype" : "1792h", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "2436h", + "filename" : "iPhone XS.png", + "minimum-system-version" : "11.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "subtype" : "2436h", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "736h", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "736h", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "Retina HD 4.7.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/Nyxo/Images.xcassets/Launch.launchimage/Retina HD 4.7.png b/ios/Nyxo/Images.xcassets/Launch.launchimage/Retina HD 4.7.png new file mode 100644 index 0000000..06ae34b Binary files /dev/null and b/ios/Nyxo/Images.xcassets/Launch.launchimage/Retina HD 4.7.png differ diff --git a/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS Max.png b/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS Max.png new file mode 100644 index 0000000..d33cbad Binary files /dev/null and b/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS Max.png differ diff --git a/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS.png b/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS.png new file mode 100644 index 0000000..14678ba Binary files /dev/null and b/ios/Nyxo/Images.xcassets/Launch.launchimage/iPhone XS.png differ diff --git a/ios/Nyxo/Info.plist b/ios/Nyxo/Info.plist new file mode 100644 index 0000000..7934ee9 --- /dev/null +++ b/ios/Nyxo/Info.plist @@ -0,0 +1,134 @@ + + + + + BGTaskSchedulerPermittedIdentifiers + + com.transistorsoft.fetch + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Nyxo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + nyxo + CFBundleURLSchemes + + nyxo + + + + CFBundleVersion + 41 + Fabric + + APIKey + 421e3a33c9d2fd64e229fdc00d5238c59e9b66fa + Kits + + + KitInfo + + KitName + Crashlytics + + + + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + NSCalendarsUsageDescription + Allow $(PRODUCT_NAME) to access your calendar + NSCameraUsageDescription + Send photos to help resolve issues + NSContactsUsageDescription + Allow $(PRODUCT_NAME) to access your contacts + NSHealthShareUsageDescription + Allow Nyxo to use active energy and heart rate data to analyse sleep. + NSHealthUpdateUsageDescription + Allow Nyxo to write sleep data. + NSLocationAlwaysAndWhenInUseUsageDescription + Allow $(PRODUCT_NAME) to use your location + NSLocationAlwaysUsageDescription + Let Nyxo to add location data to your nights. + NSLocationUsageDescription + Let Nyxo to add location data to your nights. + NSLocationWhenInUseUsageDescription + Let Nyxo to add location data to your nights. + NSMicrophoneUsageDescription + Allow $(PRODUCT_NAME) to access your microphone + NSMotionUsageDescription + Allow Nyxo to access motion related data in order to improve sleep detection. + NSPhotoLibraryAddUsageDescription + Give $(PRODUCT_NAME) permission to save photos + NSPhotoLibraryUsageDescription + Let Nyxo use your photos for profile picture + NSRemindersUsageDescription + Allow $(PRODUCT_NAME) to access your reminders + UIAppFonts + + Montserrat-Black.ttf + Montserrat-BlackItalic.ttf + Montserrat-Bold.ttf + Montserrat-BoldItalic.ttf + Montserrat-Hairline.ttf + Montserrat-HairlineItalic.ttf + Montserrat-Italic.ttf + Montserrat-Light.ttf + Montserrat-LightItalic.ttf + Montserrat-Regular.ttf + Montserrat-Medium.ttf + Domine-Bold.ttf + Domine-Regular.ttf + + UIBackgroundModes + + fetch + remote-notification + processing + + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Nyxo/Nyxo.entitlements b/ios/Nyxo/Nyxo.entitlements new file mode 100644 index 0000000..0c798d4 --- /dev/null +++ b/ios/Nyxo/Nyxo.entitlements @@ -0,0 +1,27 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:get-nyxo.app.link + applinks:get-nyxo-alternate.app.link + applinks:get-nyxo.test-app.link + applinks:get.nyxo.fi + applinks:get-nyxo.app.link + applinks:auth.nyxo.app + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + health-records + + keychain-access-groups + + $(AppIdentifierPrefix)app.sleepcircle.application + + + diff --git a/ios/Nyxo/fi.lproj/LaunchScreen.strings b/ios/Nyxo/fi.lproj/LaunchScreen.strings new file mode 100644 index 0000000..e11eb92 --- /dev/null +++ b/ios/Nyxo/fi.lproj/LaunchScreen.strings @@ -0,0 +1,6 @@ + +/* Class = "UILabel"; text = "Powered by React Native"; ObjectID = "8ie-xW-0ye"; */ +"8ie-xW-0ye.text" = "Powered by React Native"; + +/* Class = "UILabel"; text = "Nyxo"; ObjectID = "kId-c2-rCX"; */ +"kId-c2-rCX.text" = "Nyxo"; diff --git a/ios/Nyxo/main.m b/ios/Nyxo/main.m new file mode 100644 index 0000000..c316cf8 --- /dev/null +++ b/ios/Nyxo/main.m @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/ios/NyxoTests/Info.plist b/ios/NyxoTests/Info.plist new file mode 100644 index 0000000..9462e70 --- /dev/null +++ b/ios/NyxoTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.1.2 + CFBundleSignature + ???? + CFBundleVersion + 41 + + diff --git a/ios/NyxoTests/NyxoTests.m b/ios/NyxoTests/NyxoTests.m new file mode 100644 index 0000000..1663cba --- /dev/null +++ b/ios/NyxoTests/NyxoTests.m @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#import +#import + +#define TIMEOUT_SECONDS 600 +#define TEXT_TO_LOOK_FOR @"Welcome to React Native!" + +@interface NyxoTests : XCTestCase + +@end + +@implementation NyxoTests + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRendersWelcomeScreen +{ + UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + + __block NSString *redboxError = nil; + RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { + return YES; + } + return NO; + }]; + } + + RCTSetLogFunction(RCTDefaultLogFunction); + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); +} + + +@end diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..4a196ef --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,118 @@ +require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' +require_relative '../node_modules/react-native-unimodules/cocoapods.rb' + +pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" +pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" +pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" +pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" + +pod 'React', :path => '../node_modules/react-native/' +pod 'React-Core', :path => '../node_modules/react-native/' +pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' +pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' +pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' +pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' +pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' +pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' +pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' +pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' +pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' +pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' +pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' +pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' + +pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' +pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' +pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' +pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' +pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon" +pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" +pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true +pod 'RNPurchases', :path => '../node_modules/react-native-purchases', :inhibit_warnings => true + +pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' +pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' +pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' + +pod 'Firebase/Core', '~> 6.9.0' +pod 'Firebase/Messaging', '~> 6.9.0' + +pod 'OpenSSL-Universal' + + +def add_flipper_pods!(versions = {}) + versions['Flipper'] ||= '~> 0.33.1' + versions['DoubleConversion'] ||= '1.1.7' + versions['Flipper-Folly'] ||= '~> 2.1' + versions['Flipper-Glog'] ||= '0.3.6' + versions['Flipper-PeerTalk'] ||= '~> 0.0.4' + versions['Flipper-RSocket'] ||= '~> 1.0' + pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug' + # List all transitive dependencies for FlipperKit pods + # to avoid them being linked in Release builds + pod 'Flipper', versions['Flipper'], :configuration => 'Debug' + pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug' + pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug' + pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug' + pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug' + pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug' + pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug' + pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug' +end + + + +platform :ios, '10.0' +target 'Nyxo' do + + pod 'Intercom', '~> 6.0.0' + pod 'AppAuth', '>= 1.2.0' + use_unimodules!(exclude: ['expo-face-detector']) + use_native_modules! + + add_flipper_pods! + +end + + +target 'Nyxo Dev' do + +pod 'Intercom', '~> 6.0.0' +pod 'AppAuth', '>= 1.2.0' +use_unimodules!(exclude: ['expo-face-detector']) +use_native_modules! + +post_install do |installer| + +installer.pods_project.targets.each do |target| + + if target.name == 'YogaKit' + target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '4.1' + end + end + + if target.name == 'react-native-config' + phase = target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) + phase.shell_script = "cd ../../"\ + " && RNC_ROOT=./node_modules/react-native-config/"\ + " && export SYMROOT=$RNC_ROOT/ios/ReactNativeConfig"\ + " && ruby $RNC_ROOT/ios/ReactNativeConfig/BuildDotenvConfig.rb" + + target.build_phases << phase + target.build_phases.move(phase,0) + end +end +end +end + diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..a55e586 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,949 @@ +PODS: + - AppAuth (1.2.0): + - AppAuth/Core (= 1.2.0) + - AppAuth/ExternalUserAgent (= 1.2.0) + - AppAuth/Core (1.2.0) + - AppAuth/ExternalUserAgent (1.2.0) + - appcenter-analytics (3.0.2): + - AppCenter/Analytics + - AppCenterReactNativeShared + - React + - appcenter-core (3.0.2): + - AppCenterReactNativeShared + - React + - appcenter-crashes (3.0.2): + - AppCenter/Crashes + - AppCenterReactNativeShared + - React + - AppCenter/Analytics (3.2.0): + - AppCenter/Core + - AppCenter/Core (3.2.0) + - AppCenter/Crashes (3.2.0): + - AppCenter/Core + - AppCenterReactNativeShared (3.0.3): + - AppCenter/Core (= 3.2.0) + - boost-for-react-native (1.63.0) + - BVLinearGradient (2.5.6): + - React + - CocoaAsyncSocket (7.6.4) + - CocoaLibEvent (1.0.0) + - Crashlytics (3.14.0): + - Fabric (~> 1.10.2) + - DoubleConversion (1.1.6) + - EXBlur (8.1.2): + - UMCore + - EXConstants (9.0.0): + - UMConstantsInterface + - UMCore + - EXFileSystem (8.1.0): + - UMCore + - UMFileSystemInterface + - EXImageLoader (1.0.1): + - React-Core + - UMCore + - UMImageLoaderInterface + - EXPermissions (8.1.0): + - UMCore + - UMPermissionsInterface + - Fabric (1.10.2) + - FBLazyVector (0.62.2) + - FBReactNativeSpec (0.62.2): + - Folly (= 2018.10.22.00) + - RCTRequired (= 0.62.2) + - RCTTypeSafety (= 0.62.2) + - React-Core (= 0.62.2) + - React-jsi (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - Firebase/Core (6.9.0): + - Firebase/CoreOnly + - FirebaseAnalytics (= 6.1.2) + - Firebase/CoreOnly (6.9.0): + - FirebaseCore (= 6.3.0) + - Firebase/Messaging (6.9.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 4.1.4) + - FirebaseAnalytics (6.1.2): + - FirebaseCore (~> 6.3) + - FirebaseInstanceID (~> 4.2) + - GoogleAppMeasurement (= 6.1.2) + - GoogleUtilities/AppDelegateSwizzler (~> 6.0) + - GoogleUtilities/MethodSwizzler (~> 6.0) + - GoogleUtilities/Network (~> 6.0) + - "GoogleUtilities/NSData+zlib (~> 6.0)" + - nanopb (~> 0.3) + - FirebaseAnalyticsInterop (1.5.0) + - FirebaseCore (6.3.0): + - FirebaseCoreDiagnostics (~> 1.0) + - FirebaseCoreDiagnosticsInterop (~> 1.0) + - GoogleUtilities/Environment (~> 6.2) + - GoogleUtilities/Logger (~> 6.2) + - FirebaseCoreDiagnostics (1.2.4): + - FirebaseCoreDiagnosticsInterop (~> 1.2) + - GoogleDataTransportCCTSupport (~> 3.0) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Logger (~> 6.5) + - nanopb (~> 0.3.901) + - FirebaseCoreDiagnosticsInterop (1.2.0) + - FirebaseInstanceID (4.2.7): + - FirebaseCore (~> 6.0) + - GoogleUtilities/Environment (~> 6.0) + - GoogleUtilities/UserDefaults (~> 6.0) + - FirebaseMessaging (4.1.10): + - FirebaseAnalyticsInterop (~> 1.3) + - FirebaseCore (~> 6.2) + - FirebaseInstanceID (~> 4.1) + - GoogleUtilities/AppDelegateSwizzler (~> 6.2) + - GoogleUtilities/Environment (~> 6.2) + - GoogleUtilities/Reachability (~> 6.2) + - GoogleUtilities/UserDefaults (~> 6.2) + - Protobuf (>= 3.9.2, ~> 3.9) + - Flipper (0.33.1): + - Flipper-Folly (~> 2.1) + - Flipper-RSocket (~> 1.0) + - Flipper-DoubleConversion (1.1.7) + - Flipper-Folly (2.2.0): + - boost-for-react-native + - CocoaLibEvent (~> 1.0) + - Flipper-DoubleConversion + - Flipper-Glog + - OpenSSL-Universal (= 1.0.2.19) + - Flipper-Glog (0.3.6) + - Flipper-PeerTalk (0.0.4) + - Flipper-RSocket (1.1.0): + - Flipper-Folly (~> 2.2) + - FlipperKit (0.33.1): + - FlipperKit/Core (= 0.33.1) + - FlipperKit/Core (0.33.1): + - Flipper (~> 0.33.1) + - FlipperKit/CppBridge + - FlipperKit/FBCxxFollyDynamicConvert + - FlipperKit/FBDefines + - FlipperKit/FKPortForwarding + - FlipperKit/CppBridge (0.33.1): + - Flipper (~> 0.33.1) + - FlipperKit/FBCxxFollyDynamicConvert (0.33.1): + - Flipper-Folly (~> 2.1) + - FlipperKit/FBDefines (0.33.1) + - FlipperKit/FKPortForwarding (0.33.1): + - CocoaAsyncSocket (~> 7.6) + - Flipper-PeerTalk (~> 0.0.4) + - FlipperKit/FlipperKitHighlightOverlay (0.33.1) + - FlipperKit/FlipperKitLayoutPlugin (0.33.1): + - FlipperKit/Core + - FlipperKit/FlipperKitHighlightOverlay + - FlipperKit/FlipperKitLayoutTextSearchable + - YogaKit (~> 1.18) + - FlipperKit/FlipperKitLayoutTextSearchable (0.33.1) + - FlipperKit/FlipperKitNetworkPlugin (0.33.1): + - FlipperKit/Core + - FlipperKit/FlipperKitReactPlugin (0.33.1): + - FlipperKit/Core + - FlipperKit/FlipperKitUserDefaultsPlugin (0.33.1): + - FlipperKit/Core + - FlipperKit/SKIOSNetworkPlugin (0.33.1): + - FlipperKit/Core + - FlipperKit/FlipperKitNetworkPlugin + - Folly (2018.10.22.00): + - boost-for-react-native + - DoubleConversion + - Folly/Default (= 2018.10.22.00) + - glog + - Folly/Default (2018.10.22.00): + - boost-for-react-native + - DoubleConversion + - glog + - glog (0.3.5) + - GoogleAppMeasurement (6.1.2): + - GoogleUtilities/AppDelegateSwizzler (~> 6.0) + - GoogleUtilities/MethodSwizzler (~> 6.0) + - GoogleUtilities/Network (~> 6.0) + - "GoogleUtilities/NSData+zlib (~> 6.0)" + - nanopb (~> 0.3) + - GoogleDataTransport (6.2.1) + - GoogleDataTransportCCTSupport (3.0.0): + - GoogleDataTransport (~> 6.0) + - nanopb (~> 0.3.901) + - GoogleUtilities/AppDelegateSwizzler (6.6.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (6.6.0): + - PromisesObjC (~> 1.2) + - GoogleUtilities/Logger (6.6.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (6.6.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (6.6.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (6.6.0)" + - GoogleUtilities/Reachability (6.6.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (6.6.0): + - GoogleUtilities/Logger + - Intercom (6.0.2) + - JKBigInteger2 (0.0.5) + - libwebp (1.1.0): + - libwebp/demux (= 1.1.0) + - libwebp/mux (= 1.1.0) + - libwebp/webp (= 1.1.0) + - libwebp/demux (1.1.0): + - libwebp/webp + - libwebp/mux (1.1.0): + - libwebp/demux + - libwebp/webp (1.1.0) + - nanopb (0.3.9011): + - nanopb/decode (= 0.3.9011) + - nanopb/encode (= 0.3.9011) + - nanopb/decode (0.3.9011) + - nanopb/encode (0.3.9011) + - OpenSSL-Universal (1.0.2.19): + - OpenSSL-Universal/Static (= 1.0.2.19) + - OpenSSL-Universal/Static (1.0.2.19) + - PromisesObjC (1.2.9) + - Protobuf (3.12.0) + - Purchases (3.4.0) + - PurchasesHybridCommon (1.2.0): + - Purchases (= 3.4.0) + - RCTAppleHealthKit (0.6.7): + - React + - RCTRequired (0.62.2) + - RCTTypeSafety (0.62.2): + - FBLazyVector (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTRequired (= 0.62.2) + - React-Core (= 0.62.2) + - React (0.62.2): + - React-Core (= 0.62.2) + - React-Core/DevSupport (= 0.62.2) + - React-Core/RCTWebSocket (= 0.62.2) + - React-RCTActionSheet (= 0.62.2) + - React-RCTAnimation (= 0.62.2) + - React-RCTBlob (= 0.62.2) + - React-RCTImage (= 0.62.2) + - React-RCTLinking (= 0.62.2) + - React-RCTNetwork (= 0.62.2) + - React-RCTSettings (= 0.62.2) + - React-RCTText (= 0.62.2) + - React-RCTVibration (= 0.62.2) + - React-Core (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default (= 0.62.2) + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/CoreModulesHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/Default (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/DevSupport (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default (= 0.62.2) + - React-Core/RCTWebSocket (= 0.62.2) + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - React-jsinspector (= 0.62.2) + - Yoga + - React-Core/RCTActionSheetHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTAnimationHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTBlobHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTImageHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTLinkingHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTNetworkHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTSettingsHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTTextHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTVibrationHeaders (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-Core/RCTWebSocket (0.62.2): + - Folly (= 2018.10.22.00) + - glog + - React-Core/Default (= 0.62.2) + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsiexecutor (= 0.62.2) + - Yoga + - React-CoreModules (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTTypeSafety (= 0.62.2) + - React-Core/CoreModulesHeaders (= 0.62.2) + - React-RCTImage (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-cxxreact (0.62.2): + - boost-for-react-native (= 1.63.0) + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-jsinspector (= 0.62.2) + - React-jsi (0.62.2): + - boost-for-react-native (= 1.63.0) + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-jsi/Default (= 0.62.2) + - React-jsi/Default (0.62.2): + - boost-for-react-native (= 1.63.0) + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-jsiexecutor (0.62.2): + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - React-jsinspector (0.62.2) + - react-native-app-auth (5.1.2): + - AppAuth (= 1.2.0) + - React + - react-native-get-random-values (1.4.0): + - React + - react-native-intercom (15.0.0): + - Intercom (~> 6.0.0) + - React + - react-native-maps (0.27.1): + - React + - react-native-netinfo (5.9.4): + - React + - react-native-safe-area-context (1.0.2): + - React + - react-native-splash-screen (3.2.0): + - React + - react-native-ultimate-config (3.2.3): + - React + - react-native-webview (9.4.0): + - React + - React-RCTActionSheet (0.62.2): + - React-Core/RCTActionSheetHeaders (= 0.62.2) + - React-RCTAnimation (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTTypeSafety (= 0.62.2) + - React-Core/RCTAnimationHeaders (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTBlob (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - React-Core/RCTBlobHeaders (= 0.62.2) + - React-Core/RCTWebSocket (= 0.62.2) + - React-jsi (= 0.62.2) + - React-RCTNetwork (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTImage (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTTypeSafety (= 0.62.2) + - React-Core/RCTImageHeaders (= 0.62.2) + - React-RCTNetwork (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTLinking (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - React-Core/RCTLinkingHeaders (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTNetwork (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTTypeSafety (= 0.62.2) + - React-Core/RCTNetworkHeaders (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTSettings (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - RCTTypeSafety (= 0.62.2) + - React-Core/RCTSettingsHeaders (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - React-RCTText (0.62.2): + - React-Core/RCTTextHeaders (= 0.62.2) + - React-RCTVibration (0.62.2): + - FBReactNativeSpec (= 0.62.2) + - Folly (= 2018.10.22.00) + - React-Core/RCTVibrationHeaders (= 0.62.2) + - ReactCommon/turbomodule/core (= 0.62.2) + - ReactCommon/callinvoker (0.62.2): + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-cxxreact (= 0.62.2) + - ReactCommon/turbomodule/core (0.62.2): + - DoubleConversion + - Folly (= 2018.10.22.00) + - glog + - React-Core (= 0.62.2) + - React-cxxreact (= 0.62.2) + - React-jsi (= 0.62.2) + - ReactCommon/callinvoker (= 0.62.2) + - RNAWSCognito (4.3.2): + - JKBigInteger2 (= 0.0.5) + - React + - RNBackgroundFetch (3.1.0): + - React + - RNCAsyncStorage (1.11.0): + - React + - RNCMaskedView (0.1.10): + - React + - RNCPushNotificationIOS (1.2.2): + - React + - RNDeviceInfo (5.6.1): + - React + - RNFastImage (8.1.5): + - React + - SDWebImage (~> 5.0) + - SDWebImageWebPCoder (~> 0.4.1) + - RNFirebase (5.6.0): + - Firebase/Core + - React + - RNFirebase/Crashlytics (= 5.6.0) + - RNFirebase/Crashlytics (5.6.0): + - Crashlytics + - Fabric + - Firebase/Core + - React + - RNGestureHandler (1.6.1): + - React + - RNKeychain (6.1.1): + - React + - RNLocalize (1.4.0): + - React + - RNPurchases (3.3.1): + - PurchasesHybridCommon (= 1.2.0) + - React + - RNRate (1.2.1): + - React + - RNReactNativeHapticFeedback (1.10.0): + - React + - RNReanimated (1.9.0): + - React + - RNScreens (2.9.0): + - React + - RNSentry (1.5.0): + - React + - Sentry (~> 5.1.4) + - RNSVG (12.1.0): + - React + - SDWebImage (5.8.2): + - SDWebImage/Core (= 5.8.2) + - SDWebImage/Core (5.8.2) + - SDWebImageWebPCoder (0.4.1): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.5) + - Sentry (5.1.6): + - Sentry/Core (= 5.1.6) + - Sentry/Core (5.1.6) + - UMAppLoader (1.0.2) + - UMBarCodeScannerInterface (5.1.0) + - UMCameraInterface (5.1.0) + - UMConstantsInterface (5.1.0) + - UMCore (5.1.2) + - UMFaceDetectorInterface (5.1.0) + - UMFileSystemInterface (5.1.0) + - UMFontInterface (5.1.0) + - UMImageLoaderInterface (5.1.0) + - UMPermissionsInterface (5.1.0): + - UMCore + - UMReactNativeAdapter (5.2.0): + - React-Core + - UMCore + - UMFontInterface + - UMSensorsInterface (5.1.0) + - UMTaskManagerInterface (5.1.0) + - Yoga (1.14.0) + - YogaKit (1.18.1): + - Yoga (~> 1.14) + +DEPENDENCIES: + - AppAuth (>= 1.2.0) + - appcenter-analytics (from `../node_modules/appcenter-analytics/ios`) + - appcenter-core (from `../node_modules/appcenter/ios`) + - appcenter-crashes (from `../node_modules/appcenter-crashes/ios`) + - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXBlur (from `../node_modules/expo-blur/ios`) + - EXConstants (from `../node_modules/expo-constants/ios`) + - EXFileSystem (from `../node_modules/expo-file-system/ios`) + - EXImageLoader (from `../node_modules/expo-image-loader/ios`) + - EXPermissions (from `../node_modules/expo-permissions/ios`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`) + - Firebase/Core (~> 6.9.0) + - Firebase/Messaging (~> 6.9.0) + - Flipper (~> 0.33.1) + - Flipper-DoubleConversion (= 1.1.7) + - Flipper-Folly (~> 2.1) + - Flipper-Glog (= 0.3.6) + - Flipper-PeerTalk (~> 0.0.4) + - Flipper-RSocket (~> 1.0) + - FlipperKit (~> 0.33.1) + - FlipperKit/Core (~> 0.33.1) + - FlipperKit/CppBridge (~> 0.33.1) + - FlipperKit/FBCxxFollyDynamicConvert (~> 0.33.1) + - FlipperKit/FBDefines (~> 0.33.1) + - FlipperKit/FKPortForwarding (~> 0.33.1) + - FlipperKit/FlipperKitHighlightOverlay (~> 0.33.1) + - FlipperKit/FlipperKitLayoutPlugin (~> 0.33.1) + - FlipperKit/FlipperKitLayoutTextSearchable (~> 0.33.1) + - FlipperKit/FlipperKitNetworkPlugin (~> 0.33.1) + - FlipperKit/FlipperKitReactPlugin (~> 0.33.1) + - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.33.1) + - FlipperKit/SKIOSNetworkPlugin (~> 0.33.1) + - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - Intercom (~> 6.0.0) + - OpenSSL-Universal + - RCTAppleHealthKit (from `../node_modules/react-native-healthkit`) + - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/DevSupport (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) + - react-native-app-auth (from `../node_modules/react-native-app-auth`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-intercom (from `../node_modules/react-native-intercom`) + - react-native-maps (from `../node_modules/react-native-maps`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) + - react-native-ultimate-config (from `../node_modules/react-native-ultimate-config`) + - react-native-webview (from `../node_modules/react-native-webview`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNAWSCognito (from `../node_modules/amazon-cognito-identity-js`) + - RNBackgroundFetch (from `../node_modules/react-native-background-fetch`) + - "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)" + - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" + - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" + - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - RNFastImage (from `../node_modules/react-native-fast-image`) + - RNFirebase (from `../node_modules/react-native-firebase/ios`) + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNKeychain (from `../node_modules/react-native-keychain`) + - RNLocalize (from `../node_modules/react-native-localize`) + - RNPurchases (from `../node_modules/react-native-purchases`) + - RNRate (from `../node_modules/react-native-rate`) + - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - "RNSentry (from `../node_modules/@sentry/react-native`)" + - RNSVG (from `../node_modules/react-native-svg`) + - UMAppLoader (from `../node_modules/unimodules-app-loader/ios`) + - UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`) + - UMCameraInterface (from `../node_modules/unimodules-camera-interface/ios`) + - UMConstantsInterface (from `../node_modules/unimodules-constants-interface/ios`) + - "UMCore (from `../node_modules/@unimodules/core/ios`)" + - UMFaceDetectorInterface (from `../node_modules/unimodules-face-detector-interface/ios`) + - UMFileSystemInterface (from `../node_modules/unimodules-file-system-interface/ios`) + - UMFontInterface (from `../node_modules/unimodules-font-interface/ios`) + - UMImageLoaderInterface (from `../node_modules/unimodules-image-loader-interface/ios`) + - UMPermissionsInterface (from `../node_modules/unimodules-permissions-interface/ios`) + - "UMReactNativeAdapter (from `../node_modules/@unimodules/react-native-adapter/ios`)" + - UMSensorsInterface (from `../node_modules/unimodules-sensors-interface/ios`) + - UMTaskManagerInterface (from `../node_modules/unimodules-task-manager-interface/ios`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - AppAuth + - AppCenter + - AppCenterReactNativeShared + - boost-for-react-native + - CocoaAsyncSocket + - CocoaLibEvent + - Crashlytics + - Fabric + - Firebase + - FirebaseAnalytics + - FirebaseAnalyticsInterop + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCoreDiagnosticsInterop + - FirebaseInstanceID + - FirebaseMessaging + - Flipper + - Flipper-DoubleConversion + - Flipper-Folly + - Flipper-Glog + - Flipper-PeerTalk + - Flipper-RSocket + - FlipperKit + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleDataTransportCCTSupport + - GoogleUtilities + - Intercom + - JKBigInteger2 + - libwebp + - nanopb + - OpenSSL-Universal + - PromisesObjC + - Protobuf + - Purchases + - PurchasesHybridCommon + - SDWebImage + - SDWebImageWebPCoder + - Sentry + - YogaKit + +EXTERNAL SOURCES: + appcenter-analytics: + :path: "../node_modules/appcenter-analytics/ios" + appcenter-core: + :path: "../node_modules/appcenter/ios" + appcenter-crashes: + :path: "../node_modules/appcenter-crashes/ios" + BVLinearGradient: + :path: "../node_modules/react-native-linear-gradient" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXBlur: + :path: "../node_modules/expo-blur/ios" + EXConstants: + :path: "../node_modules/expo-constants/ios" + EXFileSystem: + :path: "../node_modules/expo-file-system/ios" + EXImageLoader: + :path: "../node_modules/expo-image-loader/ios" + EXPermissions: + :path: "../node_modules/expo-permissions/ios" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../node_modules/react-native/Libraries/FBReactNativeSpec" + Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + RCTAppleHealthKit: + :path: "../node_modules/react-native-healthkit" + RCTRequired: + :path: "../node_modules/react-native/Libraries/RCTRequired" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector" + react-native-app-auth: + :path: "../node_modules/react-native-app-auth" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" + react-native-intercom: + :path: "../node_modules/react-native-intercom" + react-native-maps: + :path: "../node_modules/react-native-maps" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + react-native-splash-screen: + :path: "../node_modules/react-native-splash-screen" + react-native-ultimate-config: + :path: "../node_modules/react-native-ultimate-config" + react-native-webview: + :path: "../node_modules/react-native-webview" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNAWSCognito: + :path: "../node_modules/amazon-cognito-identity-js" + RNBackgroundFetch: + :path: "../node_modules/react-native-background-fetch" + RNCAsyncStorage: + :path: "../node_modules/@react-native-community/async-storage" + RNCMaskedView: + :path: "../node_modules/@react-native-community/masked-view" + RNCPushNotificationIOS: + :path: "../node_modules/@react-native-community/push-notification-ios" + RNDeviceInfo: + :path: "../node_modules/react-native-device-info" + RNFastImage: + :path: "../node_modules/react-native-fast-image" + RNFirebase: + :path: "../node_modules/react-native-firebase/ios" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNKeychain: + :path: "../node_modules/react-native-keychain" + RNLocalize: + :path: "../node_modules/react-native-localize" + RNPurchases: + :path: "../node_modules/react-native-purchases" + RNRate: + :path: "../node_modules/react-native-rate" + RNReactNativeHapticFeedback: + :path: "../node_modules/react-native-haptic-feedback" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + RNSentry: + :path: "../node_modules/@sentry/react-native" + RNSVG: + :path: "../node_modules/react-native-svg" + UMAppLoader: + :path: "../node_modules/unimodules-app-loader/ios" + UMBarCodeScannerInterface: + :path: "../node_modules/unimodules-barcode-scanner-interface/ios" + UMCameraInterface: + :path: "../node_modules/unimodules-camera-interface/ios" + UMConstantsInterface: + :path: "../node_modules/unimodules-constants-interface/ios" + UMCore: + :path: "../node_modules/@unimodules/core/ios" + UMFaceDetectorInterface: + :path: "../node_modules/unimodules-face-detector-interface/ios" + UMFileSystemInterface: + :path: "../node_modules/unimodules-file-system-interface/ios" + UMFontInterface: + :path: "../node_modules/unimodules-font-interface/ios" + UMImageLoaderInterface: + :path: "../node_modules/unimodules-image-loader-interface/ios" + UMPermissionsInterface: + :path: "../node_modules/unimodules-permissions-interface/ios" + UMReactNativeAdapter: + :path: "../node_modules/@unimodules/react-native-adapter/ios" + UMSensorsInterface: + :path: "../node_modules/unimodules-sensors-interface/ios" + UMTaskManagerInterface: + :path: "../node_modules/unimodules-task-manager-interface/ios" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4 + AppCenter: ca66175050d538b157959382dd43f1ab96cdab84 + appcenter-analytics: f819aa9bd41c43b8b9ae53aa5dfd862b48a65f50 + appcenter-core: 74a3db7d48a720fc3a70e0cf5cbda720decac008 + appcenter-crashes: 0aea510872e4f9186553941b75ba0e9e6c7d9e07 + AppCenterReactNativeShared: d1218878427a69d4b76f73883256455ed20f12b3 + boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c + BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 + CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 + CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f + Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df + DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 + EXBlur: b622dc0f8a8fe74f05fb437cfb1c5ee16259dab1 + EXConstants: 5304709b1bea70a4828f48ba4c7fc3ec3b2d9b17 + EXFileSystem: cf4232ba7c62dc49b78c2d36005f97b6fddf0b01 + EXImageLoader: 5ad6896fa1ef2ee814b551873cbf7a7baccc694a + EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964 + Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 + FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245 + FBReactNativeSpec: 5465d51ccfeecb7faa12f9ae0024f2044ce4044e + Firebase: 2d750c54cda57d5a6ae31212cfe5cc813c6be7e4 + FirebaseAnalytics: 5d9ccbf46ed25d3ec9304d263f85bddf1e93e2d2 + FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae + FirebaseCore: 8b2765c445d40db7137989b7146a3aa3f91b5529 + FirebaseCoreDiagnostics: b59c024493a409f8aecba02c99928d0d8431d159 + FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 + FirebaseInstanceID: ebd2ea79ee38db0cb5f5167b17a0d387e1cc7b6e + FirebaseMessaging: 089b7a4991425783384acc8bcefcd78c0af913bd + Flipper: 6c1f484f9a88d30ab3e272800d53688439e50f69 + Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41 + Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3 + Flipper-Glog: 1dfd6abf1e922806c52ceb8701a3599a79a200a6 + Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 + Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7 + FlipperKit: 6dc9b8f4ef60d9e5ded7f0264db299c91f18832e + Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51 + glog: 1f3da668190260b06b429bb211bfbee5cd790c28 + GoogleAppMeasurement: 0ae90be1cc4dad40f4a27fc767ef59fa032ec87b + GoogleDataTransport: 9a8a16f79feffc7f42096743de2a7c4815e84020 + GoogleDataTransportCCTSupport: 0f39025e8cf51f168711bd3fb773938d7e62ddb5 + GoogleUtilities: 39530bc0ad980530298e9c4af8549e991fd033b1 + Intercom: 523417d82ed1a8c635cc7f97b266eb8bd0bd9d85 + JKBigInteger2: e91672035c42328c48b7dd015b66812ddf40ca9b + libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3 + nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd + OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355 + PromisesObjC: b48e0338dbbac2207e611750777895f7a5811b75 + Protobuf: 2793fcd0622a00b546c60e7cbbcc493e043e9bb9 + Purchases: b8cac232e403ca51541a425a65992b902cecc044 + PurchasesHybridCommon: deba31751165f0a0f84eba3df68a54641f8a69b1 + RCTAppleHealthKit: b787b2f8bd0dd037a9d58964b38252b37c41b05a + RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035 + RCTTypeSafety: 93006131180074cffa227a1075802c89a49dd4ce + React: 29a8b1a02bd764fb7644ef04019270849b9a7ac3 + React-Core: b12bffb3f567fdf99510acb716ef1abd426e0e05 + React-CoreModules: 4a9b87bbe669d6c3173c0132c3328e3b000783d0 + React-cxxreact: e65f9c2ba0ac5be946f53548c1aaaee5873a8103 + React-jsi: b6dc94a6a12ff98e8877287a0b7620d365201161 + React-jsiexecutor: 1540d1c01bb493ae3124ed83351b1b6a155db7da + React-jsinspector: 512e560d0e985d0e8c479a54a4e5c147a9c83493 + react-native-app-auth: 3a8af9e5f62aa3d7a9391d5aa89ab91aeb5a0062 + react-native-get-random-values: 2b7500cdb68066aba87cdccd97067c29e16ffe95 + react-native-intercom: ddc3a81f883b9089649dbaf4c02e3b3ad271b6d1 + react-native-maps: f4b89da81626ad7f151a8bfcb79733295d31ce5c + react-native-netinfo: cd479ab1b67cdd1cb1403a99ecdb24190a6dd7ef + react-native-safe-area-context: 9d9640a9085014864052e38496fc1dfde0b93974 + react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 + react-native-ultimate-config: 429433ed406d1c0d18b72b169dc99702d7180214 + react-native-webview: cf5527893252b3b036eea024a1da6996f7344c74 + React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c + React-RCTAnimation: 49ab98b1c1ff4445148b72a3d61554138565bad0 + React-RCTBlob: a332773f0ebc413a0ce85942a55b064471587a71 + React-RCTImage: e70be9b9c74fe4e42d0005f42cace7981c994ac3 + React-RCTLinking: c1b9739a88d56ecbec23b7f63650e44672ab2ad2 + React-RCTNetwork: 73138b6f45e5a2768ad93f3d57873c2a18d14b44 + React-RCTSettings: 6e3738a87e21b39a8cb08d627e68c44acf1e325a + React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d + React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 + ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 + RNAWSCognito: 1f377c51384f8e702146a1fab6726a56013d0817 + RNBackgroundFetch: 8dbb63141792f1473e863a0797ffbd5d987af2fc + RNCAsyncStorage: d059c3ee71738c39834a627476322a5a8cd5bf36 + RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f + RNCPushNotificationIOS: 4c97a36dbec42dba411cc35e6dac25e34a805fde + RNDeviceInfo: b6e650fbd234732c759544218657d549b4339038 + RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9 + RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c + RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38 + RNKeychain: db956c02a018f7dd3a0ea8a6cf3087bc1894bf2b + RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796 + RNPurchases: bab40549792361f408b1dafbe31a5ebaf0c03c38 + RNRate: a39ac26dc9daf3f9b639ce274b7f80672ae36db1 + RNReactNativeHapticFeedback: 22c5ecf474428766c6b148f96f2ff6155cd7225e + RNReanimated: b5ccb50650ba06f6e749c7c329a1bc3ae0c88b43 + RNScreens: c526239bbe0e957b988dacc8d75ac94ec9cb19da + RNSentry: edba19169f665609fb092ba5eaf4be3c0776f50a + RNSVG: ce9d996113475209013317e48b05c21ee988d42e + SDWebImage: f923a89d7344af399ba77b87a523ae747408207a + SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8 + Sentry: e5796ec31a481474d2f94553213278470f9e302d + UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6 + UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c + UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d + UMConstantsInterface: 64060cf86587bcd90b1dbd804cceb6d377a308c1 + UMCore: eb200e882eadafcd31ead290770835fd648c0945 + UMFaceDetectorInterface: d6677d6ddc9ab95a0ca857aa7f8ba76656cc770f + UMFileSystemInterface: c70ea7147198b9807080f3597f26236be49b0165 + UMFontInterface: d9d3b27af698c5389ae9e20b99ef56a083f491fb + UMImageLoaderInterface: 14dd2c46c67167491effc9e91250e9510f12709e + UMPermissionsInterface: 5e83a9167c177e4a0f0a3539345983cc749efb3e + UMReactNativeAdapter: 126da3486c1a1f11945b649d557d6c2ebb9407b2 + UMSensorsInterface: 48941f70175e2975af1a9386c6d6cb16d8126805 + UMTaskManagerInterface: cb890c79c63885504ddc0efd7a7d01481760aca2 + Yoga: 3ebccbdd559724312790e7742142d062476b698e + YogaKit: f782866e155069a2cca2517aafea43200b01fd5a + +PODFILE CHECKSUM: 37f15bafc3fade02caa1bf73ab3c8069a9d82730 + +COCOAPODS: 1.9.3 diff --git a/ios/Shared/CommandStatus.swift b/ios/Shared/CommandStatus.swift new file mode 100644 index 0000000..90ff633 --- /dev/null +++ b/ios/Shared/CommandStatus.swift @@ -0,0 +1,87 @@ +// +// CommandStatus.swift +// Nyxo +// +// Created by Perttu Lähteenlahti on 19/07/2019. +// Copyright © 2019 Facebook. All rights reserved. +// + +import Foundation +import UIKit +import WatchConnectivity + + +enum Command: String { + case updateAppContext = "UpdateAppContext" + case sendMessage = "SendMessage" + case sendMessageData = "SendMessageData" + case transferUserInfo = "TransferUserInfo" + case transferFile = "TransferFile" + case transferCurrentComplicationUserInfo = "TransferComplicationUserInfo" +} + +// Constants to identify the phrases of a Watch Connectivity communication. +// +enum Phrase: String { + case updated = "Updated" + case sent = "Sent" + case received = "Received" + case replied = "Replied" + case transferring = "Transferring" + case canceled = "Canceled" + case finished = "Finished" + case failed = "Failed" +} + +// Wrap a timed color payload dictionary with a stronger type. +// +//struct TimedColor { +// var timeStamp: String +// var colorData: Data +// +// var color: UIColor { +// let optional = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [UIColor.self], from: colorData) +// guard let color = optional as? UIColor else { +// fatalError("Failed to unarchive a UIClor object!") +// } +// return color +// } +// var timedColor: [String: Any] { +// return [PayloadKey.timeStamp: timeStamp, PayloadKey.colorData: colorData] +// } +// +// init(_ timedColor: [String: Any]) { +// guard let timeStamp = timedColor[PayloadKey.timeStamp] as? String, +// let colorData = timedColor[PayloadKey.colorData] as? Data else { +// fatalError("Timed color dictionary doesn't have right keys!") +// } +// self.timeStamp = timeStamp +// self.colorData = colorData +// } +// +// init(_ timedColor: Data) { +// let data = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(timedColor) +// guard let dictionary = data as? [String: Any] else { +// fatalError("Failed to unarchive a timedColor dictionary!") +// } +// self.init(dictionary) +// } +//} + +// Wrap the command status to bridge the commands status and UI. +// +struct CommandStatus { + var command: Command + var phrase: Phrase +// var timedColor: TimedColor? + var fileTransfer: WCSessionFileTransfer? + var file: WCSessionFile? + var userInfoTranser: WCSessionUserInfoTransfer? + var errorMessage: String? + + init(command: Command, phrase: Phrase) { + self.command = command + self.phrase = phrase + } +} + diff --git a/ios/Shared/Nyxo Watch-Bridging-Header.h b/ios/Shared/Nyxo Watch-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/ios/Shared/Nyxo Watch-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/Shared/Nyxo-Bridging-Header.h b/ios/Shared/Nyxo-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/ios/Shared/Nyxo-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 0000000..834cd0c --- /dev/null +++ b/jest-setup.js @@ -0,0 +1,46 @@ +import RNCNetInfoMock from '@react-native-community/netinfo/jest/netinfo-mock.js' +import React from 'react' +import * as ReactNative from 'react-native' + +global.React = React +global.ReactNative = ReactNative + +global.React.useCallback = (f) => f + +jest.useFakeTimers() +jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper') +jest.mock('@react-native-community/netinfo', () => RNCNetInfoMock) + +export const alert = jest.fn() +export const Alert = { alert } + +export const dimensionWidth = 100 +export const Dimensions = { + get: jest.fn().mockReturnValue({ width: dimensionWidth, height: 100 }) +} + +export const Image = 'Image' + +export const keyboardDismiss = jest.fn() +export const Keyboard = { + dismiss: keyboardDismiss +} + +export const Platform = { + ...ReactNative.Platform, + OS: 'ios', + Version: 123, + isTesting: true, + select: (objs) => objs.ios +} + +export default Object.setPrototypeOf( + { + Alert, + Dimensions, + Image, + Keyboard, + Platform + }, + ReactNative +) diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..279a6b6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { defaults: tsjPreset } = require('ts-jest/presets') + +module.exports = { + setupFiles: ['/jest-setup.js', './node_modules/'], + preset: 'react-native', + setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'], + verbose: true, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + roots: [''], + testPathIgnorePatterns: ['./node_modules/'], + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|@sentry/react-native|aws-amplify|aws-amplify-react-native)/)' + ], + moduleNameMapper: { + '^@components(.*)$': '/src/components$1', + '^@constants(.*)$': '/src/constants$1' + }, + testEnvironment: 'jsdom', + transform: { + '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest' + }, + automock: false, + coverageThreshold: { + global: { + branches: 90, + lines: 95 + } + }, + globals: { + 'ts-jest': { + babelConfig: true + } + }, + cacheDirectory: '.jest/cache' +} diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..f66f574 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,20 @@ +/** + * Metro configuration for React Native + * https://github.com/facebook/react-native + * + * @format + */ + +module.exports = { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: false, + }, + }), + }, + resolver: { + // resolverMainFields: ['styled-components', 'styled-components/native'], + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..12d0cef --- /dev/null +++ b/package.json @@ -0,0 +1,266 @@ +{ + "name": "Nyxo", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@babel/core": "^7.6.2", + "@babel/plugin-proposal-object-rest-spread": "^7.5.1", + "@babel/preset-env": "^7.5.0", + "@babel/preset-typescript": "^7.3.3", + "@babel/register": "^7.4.4", + "@babel/runtime": "^7.6.2", + "@contentful/rich-text-types": "^14.1.0", + "@react-native-community/cli": "^4.8.0", + "@sentry/wizard": "^1.0.1", + "@types/chroma-js": "^2.0.0", + "@types/d3": "^5.7.2", + "@types/enzyme": "^3.10.5", + "@types/enzyme-adapter-react-16": "^1.0.6", + "@types/escape-string-regexp": "^2.0.1", + "@types/i18n-js": "^3.0.1", + "@types/jest": "^25.2.2", + "@types/lodash": "^4.14.144", + "@types/lodash-es": "^4.17.3", + "@types/react": "^16.9.19", + "@types/react-native": "^0.62.10", + "@types/react-navigation": "^3.0.8", + "@types/react-redux": "^7.1.4", + "@types/react-test-renderer": "^16.9.2", + "@types/redux-mock-store": "^1.0.2", + "@types/styled-components": "^5.1.0", + "@types/uuid": "^7.0.3", + "@types/yup": "^0.28.3", + "@typescript-eslint/eslint-plugin": "^3.5.0", + "@typescript-eslint/parser": "^3.5.0", + "babel-eslint": "^10.0.3", + "babel-jest": "^26.0.1", + "babel-plugin-inline-import": "^3.0.0", + "babel-plugin-module-resolver": "^4.0.0", + "babel-plugin-object-to-json-parse": "0.0.8", + "babel-plugin-styled-components": "^1.10.7", + "babel-plugin-transform-remove-console": "^6.9.4", + "babel-preset-airbnb": "^4.1.0", + "babel-preset-react-native": "^5.0.2", + "babel-preset-react-native-stage-0": "^1.0.1", + "babel-upgrade": "1.0.1", + "contentful-management": "^5.12.0", + "contentful-typescript-codegen": "^3.0.0", + "cross-env": "^7.0.2", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", + "eslint": "^6.8.0", + "eslint-config-airbnb": "^18.1.0", + "eslint-config-prettier": "^6.11.0", + "eslint-import-resolver-babel-module": "^5.1.2", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.5", + "eslint-plugin-react-native": "^3.8.1", + "flipper-plugin-react-native-performance": "^0.4.3", + "husky": "^4.2.5", + "jest": "^26.0.1", + "jest-cli": "^26.0.1", + "jest-environment-enzyme": "^7.1.2", + "jest-enzyme": "^7.1.2", + "jest-styled-components": "^7.0.2", + "metro-react-native-babel-preset": "^0.58.0", + "prettier": "^2.0.5", + "react-native-cli": "^2.0.1", + "react-native-mock-render": "^0.1.5", + "react-native-testing-library": "^1.12.0", + "react-native-typescript-transformer": "^1.2.13", + "react-test-renderer": "16.13.1", + "reactotron-react-native": "^5.0.0", + "reactotron-redux": "^3.1.2", + "redux-devtools": "3.5.0", + "redux-devtools-extension": "^2.13.8", + "redux-mock-store": "^1.5.3", + "remote-redux-devtools": "^0.5.16", + "schedule": "^0.5.0", + "ts-jest": "^26.0.0", + "ts-loader": "^7.0.4", + "tsd": "^0.11.0", + "tslib": "^2.0.0", + "tslint": "^6.1.2", + "typescript": "^3.9.6" + }, + "scripts": { + "bundle-ios": "react-native bundle --entry-file index.js --platform ios --dev=false --bundle-output ios/main.jsbundle --assets-dest ios", + "create-bundle-android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/", + "debug-android": "adb logcat *:S ReactNative:V ReactNativeJS:V", + "contentful-typescript-codegen": "contentful-typescript-codegen --output src/Types/generated/contentful.d.ts", + "lint": "eslint . --ext ts --ext tsx --ext js --ext jsx", + "lint:fix": "npm run lint --fix", + "start": "node node_modules/react-native/local-cli/cli.js start", + "android": "react-native run-android", + "ios": "react-native run-ios", + "test": "jest", + "test:watch": "npm test -- --watch", + "ios:beta": "--prefix ios fastlane ios beta ", + "android-dev": "adb reverse tcp:8081 tcp:8081 && react-native run-android", + "pod-install": "cd ios && pod install && cd -", + "bundle-android": "cd android && ./gradlew assembleRelease && cd -", + "postinstall": "npx jetify && patch-package", + "build:ios": "npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios", + "beta:ios": "cd ios && fastlane beta", + "beta:android": "cd android && fastlane beta" + }, + "jest": { + "setupFilesAfterEnv": [ + "jest-enzyme" + ], + "testEnvironment": "enzyme", + "globals": { + "ts-jest": { + "tsConfigFile": "tsconfig.jest.json" + } + }, + "moduleDirectories": [ + "node_modules" + ], + "preset": "react-native", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "transform": { + "^.+\\.(js)$": "/node_modules/babel-jest", + "\\.(ts|tsx)$": "/node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", + "testPathIgnorePatterns": [ + "\\.snap$", + "/node_modules/", + "/lib/" + ], + "cacheDirectory": ".jest/cache" + }, + "lint-staged": { + "*.{js,json,css,md}": [ + "prettier --write", + "git add" + ] + }, + "dependencies": { + "@aws-amplify/core": "^3.2.1", + "@contentful/rich-text-html-renderer": "^13.4.0", + "@contentful/rich-text-plain-text-renderer": "^14.0.0", + "@contentful/rich-text-react-renderer": "^13.4.0", + "@react-native-community/async-storage": "^1.6.2", + "@react-native-community/masked-view": "^0.1.6", + "@react-native-community/netinfo": "^5.7.0", + "@react-native-community/push-notification-ios": "^1.0.7", + "@react-navigation/bottom-tabs": "^5.0.3", + "@react-navigation/compat": "^5.0.3", + "@react-navigation/core": "^5.1.2", + "@react-navigation/native": "^5.0.3", + "@react-navigation/native-stack": "^5.0.3", + "@react-navigation/stack": "^5.0.3", + "@reduxjs/toolkit": "^1.3.5", + "@sentry/react-native": "^1.5.0", + "amazon-cognito-identity-js": "^4.2.1", + "appcenter": "3.0.2", + "appcenter-analytics": "3.0.2", + "appcenter-crashes": "3.0.2", + "aws-amplify": "^3.0.7", + "aws-amplify-react-native": "^4.0.3", + "chroma-js": "^2.0.6", + "contentful": "^7.13.1", + "countdown": "^2.6.0", + "d3": "^5.12.0", + "d3-interpolate": "^1.3.2", + "debug": "^4.1.1", + "escape-string-regexp": "^4.0.0", + "expo-blur": "^8.1.0", + "formik": "^2.1.4", + "i18n-js": "^3.3.0", + "jetifier": "^1.6.4", + "jsc-android": "^241213.1.0", + "lint-staged": "^10.2.2", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "moment": "^2.24.0", + "moment-countdown": "0.0.3", + "moment-range": "^4.0.2", + "patch-package": "^6.2.2", + "postinstall-postinstall": "^2.1.0", + "prop-types": "^15.7.2", + "react": "16.11.0", + "react-native": "0.62.2", + "react-native-animatable": "^1.3.2", + "react-native-app-auth": "^5.0.0", + "react-native-app-intro-slider": "^4.0.2", + "react-native-autoheight-webview": "^1.4.1", + "react-native-background-fetch": "^3.0.4", + "react-native-calendars": "^1.212.0", + "react-native-chart-kit": "^5.5.0", + "react-native-circular-progress": "^1.3.0", + "react-native-datepicker": "^1.7.2", + "react-native-device-info": "^5.5.1", + "react-native-fast-image": "^8.1.5", + "react-native-firebase": "^5.6.0", + "react-native-gesture-handler": "^1.5.3", + "react-native-get-random-values": "^1.4.0", + "react-native-google-fit": "git+https://github.com/plahteenlahti/react-native-google-fit.git", + "react-native-haptic-feedback": "^1.8.2", + "react-native-healthkit": "git+https://github.com/plahteenlahti/rn-apple-healthkit.git", + "react-native-intercom": "^15.0.0", + "react-native-iphone-x-helper": "^1.2.1", + "react-native-keychain": "^6.1.1", + "react-native-linear-gradient": "^2.5.6", + "react-native-localize": "^1.3.3", + "react-native-logger": "^1.0.3", + "react-native-maps": "0.27.1", + "react-native-modal": "^11.5.3", + "react-native-navigation-bar-color": "^2.0.1", + "react-native-offline": "^5.4.0", + "react-native-picker-select": "^7.0.0", + "react-native-purchases": "^3.2.0", + "react-native-radial-context-menu": "0.0.2", + "react-native-rate": "^1.1.10", + "react-native-ratings": "^7.2.0", + "react-native-reanimated": "^1.7.0", + "react-native-redash": "^14.0.4", + "react-native-render-html": "^4.2.0", + "react-native-safe-area-context": "^1.0.0", + "react-native-screens": "^2.0.0-beta.2", + "react-native-simple-gauge": "^0.2.2", + "react-native-snap-carousel": "^3.8.2", + "react-native-splash-screen": "^3.2.0", + "react-native-status-bar-height": "^2.4.0", + "react-native-svg": "^12.1.0", + "react-native-svg-transformer": "^0.14.3", + "react-native-swiper": "^1.5.14", + "react-native-tab-view": "^2.10.0", + "react-native-ultimate-config": "^3.2.3", + "react-native-unimodules": "^0.9.1", + "react-native-view-overflow": "0.0.5", + "react-native-webview": "^9.4.0", + "react-redux": "^7.1.3", + "reading-time": "^1.2.0", + "reanimated-bottom-sheet": "^1.0.0-alpha.14", + "redux": "^4.0.4", + "redux-batched-actions": "^0.5.0", + "redux-persist": "^6.0.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0", + "serialize-javascript": "^3.0.0", + "styled-components": "^5.1.1", + "svg-path-properties": "^1.0.4", + "uuid": "^8.0.0", + "validate.js": "^0.13.1", + "yarn": "^1.22.4", + "yup": "^0.28.3" + }, + "rnmp": { + "assets": [ + "./assets/fonts/" + ] + } +} diff --git a/patches/react-native+0.62.2.patch b/patches/react-native+0.62.2.patch new file mode 100644 index 0000000..e44b2c3 --- /dev/null +++ b/patches/react-native+0.62.2.patch @@ -0,0 +1,115 @@ +diff --git a/node_modules/react-native/React/Views/RCTModalHostView.h b/node_modules/react-native/React/Views/RCTModalHostView.h +index e16dd22..97f24a6 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostView.h ++++ b/node_modules/react-native/React/Views/RCTModalHostView.h +@@ -17,7 +17,7 @@ + + @protocol RCTModalHostViewInteractor; + +-@interface RCTModalHostView : UIView ++@interface RCTModalHostView : UIView + + @property (nonatomic, copy) NSString *animationType; + @property (nonatomic, assign) UIModalPresentationStyle presentationStyle; +@@ -31,9 +31,9 @@ + + @property (nonatomic, copy) NSArray *supportedOrientations; + @property (nonatomic, copy) RCTDirectEventBlock onOrientationChange; ++@property (nonatomic, copy) RCTDirectEventBlock onRequestClose; + + #if TARGET_OS_TV +-@property (nonatomic, copy) RCTDirectEventBlock onRequestClose; + @property (nonatomic, strong) RCTTVRemoteHandler *tvRemoteHandler; + #endif + +diff --git a/node_modules/react-native/React/Views/RCTModalHostView.m b/node_modules/react-native/React/Views/RCTModalHostView.m +index 95d572b..7e4f4c4 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostView.m ++++ b/node_modules/react-native/React/Views/RCTModalHostView.m +@@ -43,6 +43,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge + if ((self = [super initWithFrame:CGRectZero])) { + _bridge = bridge; + _modalViewController = [RCTModalHostViewController new]; ++ // Transparency breaks for overFullScreen in iOS < 13 ++ if (@available(iOS 13.0, *)) { ++ _modalViewController.presentationController.delegate = self; ++ } + UIView *containerView = [UIView new]; + containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + _modalViewController.view = containerView; +@@ -63,6 +67,24 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge + return self; + } + ++// Method must be implemented, otherwise iOS defaults to 'automatic' (pageSheet on >= iOS 13.0) ++- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection ++{ ++ if (self.presentationStyle == UIModalPresentationFullScreen && self.isTransparent) { ++ return UIModalPresentationOverFullScreen; ++ } ++ return self.presentationStyle; ++} ++ ++// Method must be implemented, otherwise iOS defaults to 'automatic' (pageSheet on >= iOS 13.0) ++- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller ++{ ++ if (self.presentationStyle == UIModalPresentationFullScreen && self.isTransparent) { ++ return UIModalPresentationOverFullScreen; ++ } ++ return self.presentationStyle; ++} ++ + #if TARGET_OS_TV + - (void)menuButtonPressed:(__unused UIGestureRecognizer *)gestureRecognizer + { +@@ -70,10 +92,12 @@ - (void)menuButtonPressed:(__unused UIGestureRecognizer *)gestureRecognizer + _onRequestClose(nil); + } + } ++#endif + + - (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose + { + _onRequestClose = onRequestClose; ++ #if TARGET_OS_TV + if (_reactSubview) { + if (_onRequestClose && _menuButtonGestureRecognizer) { + [_reactSubview addGestureRecognizer:_menuButtonGestureRecognizer]; +@@ -81,8 +105,8 @@ - (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose + [_reactSubview removeGestureRecognizer:_menuButtonGestureRecognizer]; + } + } ++ #endif + } +-#endif + + - (void)notifyForBoundsChange:(CGRect)newBounds + { +@@ -156,6 +180,13 @@ - (void)didUpdateReactSubviews + // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` + } + ++- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController ++{ ++ if (_onRequestClose) { ++ _onRequestClose(nil); ++ } ++} ++ + - (void)dismissModalViewController + { + if (_isPresented) { +diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +index fa6f645..da7ca01 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m ++++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +@@ -114,9 +114,6 @@ - (void)invalidate + RCT_EXPORT_VIEW_PROPERTY(identifier, NSNumber) + RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray) + RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock) +- +-#if TARGET_OS_TV + RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock) +-#endif + + @end diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..da8c3e4 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,9 @@ +// react-native.config.js +module.exports = { + dependencies: { + 'react-native-google-fit': { + platforms: { android: null } + } + }, + assets: ['./assets/fonts'] +} diff --git a/rn-cli.config.js b/rn-cli.config.js new file mode 100644 index 0000000..dad3fa8 --- /dev/null +++ b/rn-cli.config.js @@ -0,0 +1,8 @@ +module.exports = { + resolver: { + sourceExts: ['tsx', 'ts', 'js'], + }, + transformer: { + babelTransformerPath: require.resolve('react-native-typescript-transformer'), + }, +}; diff --git a/src/API.ts b/src/API.ts new file mode 100644 index 0000000..c38da66 --- /dev/null +++ b/src/API.ts @@ -0,0 +1,1513 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +export type UpdateUserInput = { + connectionId?: string | null, + id: string, + email?: string | null, + nickname?: string | null, + darkMode?: boolean | null, + intercomId?: string | null, +}; + +export type ModelSleepDataFilterInput = { + id?: ModelIDFilterInput | null, + userId?: ModelIDFilterInput | null, + date?: ModelStringFilterInput | null, + rating?: ModelIntFilterInput | null, + and?: Array< ModelSleepDataFilterInput | null > | null, + or?: Array< ModelSleepDataFilterInput | null > | null, + not?: ModelSleepDataFilterInput | null, +}; + +export type ModelIDFilterInput = { + ne?: string | null, + eq?: string | null, + le?: string | null, + lt?: string | null, + ge?: string | null, + gt?: string | null, + contains?: string | null, + notContains?: string | null, + between?: Array< string | null > | null, + beginsWith?: string | null, +}; + +export type ModelStringFilterInput = { + ne?: string | null, + eq?: string | null, + le?: string | null, + lt?: string | null, + ge?: string | null, + gt?: string | null, + contains?: string | null, + notContains?: string | null, + between?: Array< string | null > | null, + beginsWith?: string | null, +}; + +export type ModelIntFilterInput = { + ne?: number | null, + eq?: number | null, + le?: number | null, + lt?: number | null, + ge?: number | null, + gt?: number | null, + between?: Array< number | null > | null, +}; + +export type ModelUserFilterInput = { + connectionId?: ModelStringFilterInput | null, + id?: ModelIDFilterInput | null, + email?: ModelStringFilterInput | null, + nickname?: ModelStringFilterInput | null, + darkMode?: ModelBooleanFilterInput | null, + intercomId?: ModelStringFilterInput | null, + and?: Array< ModelUserFilterInput | null > | null, + or?: Array< ModelUserFilterInput | null > | null, + not?: ModelUserFilterInput | null, +}; + +export type ModelBooleanFilterInput = { + ne?: boolean | null, + eq?: boolean | null, +}; + +export type ModelCoachingDataFilterInput = { + id?: ModelIDFilterInput | null, + lessons?: ModelStringFilterInput | null, + userId?: ModelIDFilterInput | null, + stage?: ModelStageFilterInput | null, + activeWeek?: ModelStringFilterInput | null, + started?: ModelStringFilterInput | null, + ended?: ModelStringFilterInput | null, + and?: Array< ModelCoachingDataFilterInput | null > | null, + or?: Array< ModelCoachingDataFilterInput | null > | null, + not?: ModelCoachingDataFilterInput | null, +}; + +export type ModelStageFilterInput = { + eq?: Stage | null, + ne?: Stage | null, +}; + +export enum Stage { + ONGOING = "ONGOING", + PAUSED = "PAUSED", + COMPLETED = "COMPLETED", +} + + +export type CreateSleepDataInput = { + id?: string | null, + userId: string, + date: string, + rating?: number | null, + night?: Array< NightSegmentInput | null > | null, +}; + +export type NightSegmentInput = { + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, +}; + +export type UpdateSleepDataInput = { + id: string, + userId?: string | null, + date?: string | null, + rating?: number | null, + night?: Array< NightSegmentInput | null > | null, +}; + +export type DeleteSleepDataInput = { + id?: string | null, +}; + +export type CreateRequestInput = { + id?: string | null, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, +}; + +export type UpdateRequestInput = { + id: string, + requesterName?: string | null, + requesterId?: string | null, + userName?: string | null, + userId?: string | null, + accepted?: boolean | null, +}; + +export type DeleteRequestInput = { + id?: string | null, +}; + +export type CreateUserInput = { + connectionId?: string | null, + id?: string | null, + email: string, + nickname?: string | null, + darkMode?: boolean | null, + intercomId?: string | null, +}; + +export type DeleteUserInput = { + id?: string | null, +}; + +export type CreateCoachingDataInput = { + id?: string | null, + weeks?: Array< WeekInput | null > | null, + lessons?: Array< string | null > | null, + userId: string, + stage?: Stage | null, + activeWeek?: string | null, + started?: string | null, + ended?: string | null, +}; + +export type WeekInput = { + started?: string | null, + ended?: string | null, + locked?: boolean | null, + slug?: string | null, +}; + +export type UpdateCoachingDataInput = { + id: string, + weeks?: Array< WeekInput | null > | null, + lessons?: Array< string | null > | null, + userId?: string | null, + stage?: Stage | null, + activeWeek?: string | null, + started?: string | null, + ended?: string | null, +}; + +export type DeleteCoachingDataInput = { + id?: string | null, +}; + +export type CreateHabitInput = { + id?: string | null, + userId: string, + dayStreak?: number | null, + longestDayStreak?: number | null, + latestCompletedDate?: string | null, + title: string, + description?: string | null, + date: string, + days: Array< DayCompletionRecordInput | null >, + archived?: boolean | null, + period: Period, +}; + +export type DayCompletionRecordInput = { + key?: string | null, + value?: number | null, +}; + +export enum Period { + morning = "morning", + afternoon = "afternoon", + evening = "evening", +} + + +export type UpdateHabitInput = { + id: string, + userId?: string | null, + dayStreak?: number | null, + longestDayStreak?: number | null, + latestCompletedDate?: string | null, + title?: string | null, + description?: string | null, + date?: string | null, + days?: Array< DayCompletionRecordInput | null > | null, + archived?: boolean | null, + period?: Period | null, +}; + +export type DeleteHabitInput = { + id?: string | null, +}; + +export type ModelRequestFilterInput = { + id?: ModelIDFilterInput | null, + requesterName?: ModelStringFilterInput | null, + requesterId?: ModelStringFilterInput | null, + userName?: ModelStringFilterInput | null, + userId?: ModelStringFilterInput | null, + accepted?: ModelBooleanFilterInput | null, + and?: Array< ModelRequestFilterInput | null > | null, + or?: Array< ModelRequestFilterInput | null > | null, + not?: ModelRequestFilterInput | null, +}; + +export type ModelHabitFilterInput = { + id?: ModelIDFilterInput | null, + userId?: ModelIDFilterInput | null, + dayStreak?: ModelIntFilterInput | null, + longestDayStreak?: ModelIntFilterInput | null, + latestCompletedDate?: ModelStringFilterInput | null, + title?: ModelStringFilterInput | null, + description?: ModelStringFilterInput | null, + date?: ModelStringFilterInput | null, + archived?: ModelBooleanFilterInput | null, + period?: ModelPeriodFilterInput | null, + and?: Array< ModelHabitFilterInput | null > | null, + or?: Array< ModelHabitFilterInput | null > | null, + not?: ModelHabitFilterInput | null, +}; + +export type ModelPeriodFilterInput = { + eq?: Period | null, + ne?: Period | null, +}; + +export enum ModelSortDirection { + ASC = "ASC", + DESC = "DESC", +} + + +export type UpdateConnectionIDMutationVariables = { + input: UpdateUserInput, +}; + +export type UpdateConnectionIDMutation = { + updateUser: { + __typename: "User", + email: string, + connectionId: string | null, + } | null, +}; + +export type listSleepDatasAnonymisedQueryVariables = { + filter?: ModelSleepDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type listSleepDatasAnonymisedQuery = { + listSleepDatas: { + __typename: "ModelSleepDataConnection", + items: Array< { + __typename: "SleepData", + id: string, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type getSleepDataAnonymisedQueryVariables = { + id: string, +}; + +export type getSleepDataAnonymisedQuery = { + getSleepData: { + __typename: "SleepData", + id: string, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + } | null, +}; + +export type getUserAnonymisedQueryVariables = { + id: string, +}; + +export type getUserAnonymisedQuery = { + getUser: { + __typename: "User", + id: string, + nickname: string | null, + } | null, +}; + +export type listUsersAnonymisedQueryVariables = { + filter?: ModelUserFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type listUsersAnonymisedQuery = { + listUsers: { + __typename: "ModelUserConnection", + items: Array< { + __typename: "User", + id: string, + nickname: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type GetSleepDataSimpleQueryVariables = { + filter?: ModelSleepDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type GetSleepDataSimpleQuery = { + listSleepDatas: { + __typename: "ModelSleepDataConnection", + items: Array< { + __typename: "SleepData", + id: string, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type QueryCoachingDataQueryVariables = { + filter?: ModelCoachingDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type QueryCoachingDataQuery = { + listCoachingDatas: { + __typename: "ModelCoachingDataConnection", + items: Array< { + __typename: "CoachingData", + id: string, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type CreateSleepDataMutationVariables = { + input: CreateSleepDataInput, +}; + +export type CreateSleepDataMutation = { + createSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type UpdateSleepDataMutationVariables = { + input: UpdateSleepDataInput, +}; + +export type UpdateSleepDataMutation = { + updateSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type DeleteSleepDataMutationVariables = { + input: DeleteSleepDataInput, +}; + +export type DeleteSleepDataMutation = { + deleteSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type CreateRequestMutationVariables = { + input: CreateRequestInput, +}; + +export type CreateRequestMutation = { + createRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type UpdateRequestMutationVariables = { + input: UpdateRequestInput, +}; + +export type UpdateRequestMutation = { + updateRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type DeleteRequestMutationVariables = { + input: DeleteRequestInput, +}; + +export type DeleteRequestMutation = { + deleteRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type CreateUserMutationVariables = { + input: CreateUserInput, +}; + +export type CreateUserMutation = { + createUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type UpdateUserMutationVariables = { + input: UpdateUserInput, +}; + +export type UpdateUserMutation = { + updateUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type DeleteUserMutationVariables = { + input: DeleteUserInput, +}; + +export type DeleteUserMutation = { + deleteUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type CreateCoachingDataMutationVariables = { + input: CreateCoachingDataInput, +}; + +export type CreateCoachingDataMutation = { + createCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type UpdateCoachingDataMutationVariables = { + input: UpdateCoachingDataInput, +}; + +export type UpdateCoachingDataMutation = { + updateCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type DeleteCoachingDataMutationVariables = { + input: DeleteCoachingDataInput, +}; + +export type DeleteCoachingDataMutation = { + deleteCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type CreateHabitMutationVariables = { + input: CreateHabitInput, +}; + +export type CreateHabitMutation = { + createHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type UpdateHabitMutationVariables = { + input: UpdateHabitInput, +}; + +export type UpdateHabitMutation = { + updateHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type DeleteHabitMutationVariables = { + input: DeleteHabitInput, +}; + +export type DeleteHabitMutation = { + deleteHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type GetSleepDataQueryVariables = { + id: string, +}; + +export type GetSleepDataQuery = { + getSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type ListSleepDatasQueryVariables = { + filter?: ModelSleepDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type ListSleepDatasQuery = { + listSleepDatas: { + __typename: "ModelSleepDataConnection", + items: Array< { + __typename: "SleepData", + id: string, + userId: string, + date: string, + rating: number | null, + owner: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type GetRequestQueryVariables = { + id: string, +}; + +export type GetRequestQuery = { + getRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type ListRequestsQueryVariables = { + filter?: ModelRequestFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type ListRequestsQuery = { + listRequests: { + __typename: "ModelRequestConnection", + items: Array< { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type GetUserQueryVariables = { + id: string, +}; + +export type GetUserQuery = { + getUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type ListUsersQueryVariables = { + filter?: ModelUserFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type ListUsersQuery = { + listUsers: { + __typename: "ModelUserConnection", + items: Array< { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type GetCoachingDataQueryVariables = { + id: string, +}; + +export type GetCoachingDataQuery = { + getCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type ListCoachingDatasQueryVariables = { + filter?: ModelCoachingDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type ListCoachingDatasQuery = { + listCoachingDatas: { + __typename: "ModelCoachingDataConnection", + items: Array< { + __typename: "CoachingData", + id: string, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type GetHabitQueryVariables = { + id: string, +}; + +export type GetHabitQuery = { + getHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type ListHabitsQueryVariables = { + filter?: ModelHabitFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type ListHabitsQuery = { + listHabits: { + __typename: "ModelHabitConnection", + items: Array< { + __typename: "Habit", + id: string, + userId: string, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + archived: boolean | null, + period: Period, + owner: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type UserByConnectionIdQueryVariables = { + connectionId?: string | null, + sortDirection?: ModelSortDirection | null, + filter?: ModelUserFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type UserByConnectionIdQuery = { + userByConnectionId: { + __typename: "ModelUserConnection", + items: Array< { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type CoachingByUserQueryVariables = { + userId?: string | null, + sortDirection?: ModelSortDirection | null, + filter?: ModelCoachingDataFilterInput | null, + limit?: number | null, + nextToken?: string | null, +}; + +export type CoachingByUserQuery = { + coachingByUser: { + __typename: "ModelCoachingDataConnection", + items: Array< { + __typename: "CoachingData", + id: string, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null > | null, + nextToken: string | null, + } | null, +}; + +export type OnCreateSleepDataSubscriptionVariables = { + owner: string, +}; + +export type OnCreateSleepDataSubscription = { + onCreateSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type OnUpdateSleepDataSubscriptionVariables = { + owner: string, +}; + +export type OnUpdateSleepDataSubscription = { + onUpdateSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type OnDeleteSleepDataSubscriptionVariables = { + owner?: string | null, +}; + +export type OnDeleteSleepDataSubscription = { + onDeleteSleepData: { + __typename: "SleepData", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + date: string, + rating: number | null, + night: Array< { + __typename: "NightSegment", + value: string, + sourceName: string, + sourceId: string, + startDate: string, + endDate: string, + } | null > | null, + owner: string | null, + } | null, +}; + +export type OnCreateRequestSubscriptionVariables = { + owner: string, +}; + +export type OnCreateRequestSubscription = { + onCreateRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type OnUpdateRequestSubscriptionVariables = { + userId: string, +}; + +export type OnUpdateRequestSubscription = { + onUpdateRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type OnDeleteRequestSubscriptionVariables = { + userId?: string | null, +}; + +export type OnDeleteRequestSubscription = { + onDeleteRequest: { + __typename: "Request", + id: string, + requesterName: string, + requesterId: string, + userName: string, + userId: string, + accepted: boolean, + } | null, +}; + +export type OnCreateUserSubscription = { + onCreateUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type OnUpdateUserSubscriptionVariables = { + owner: string, +}; + +export type OnUpdateUserSubscription = { + onUpdateUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type OnDeleteUserSubscriptionVariables = { + owner?: string | null, +}; + +export type OnDeleteUserSubscription = { + onDeleteUser: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + } | null, +}; + +export type OnCreateCoachingDataSubscriptionVariables = { + owner: string, +}; + +export type OnCreateCoachingDataSubscription = { + onCreateCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type OnUpdateCoachingDataSubscriptionVariables = { + owner: string, +}; + +export type OnUpdateCoachingDataSubscription = { + onUpdateCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type OnDeleteCoachingDataSubscriptionVariables = { + owner?: string | null, +}; + +export type OnDeleteCoachingDataSubscription = { + onDeleteCoachingData: { + __typename: "CoachingData", + id: string, + weeks: Array< { + __typename: "Week", + started: string | null, + ended: string | null, + locked: boolean | null, + slug: string | null, + } | null > | null, + lessons: Array< string | null > | null, + userId: string, + stage: Stage | null, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + activeWeek: string | null, + started: string | null, + ended: string | null, + owner: string | null, + } | null, +}; + +export type OnCreateHabitSubscriptionVariables = { + owner: string, +}; + +export type OnCreateHabitSubscription = { + onCreateHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type OnUpdateHabitSubscriptionVariables = { + owner: string, +}; + +export type OnUpdateHabitSubscription = { + onUpdateHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; + +export type OnDeleteHabitSubscriptionVariables = { + owner?: string | null, +}; + +export type OnDeleteHabitSubscription = { + onDeleteHabit: { + __typename: "Habit", + id: string, + userId: string, + user: { + __typename: "User", + connectionId: string | null, + id: string, + email: string, + nickname: string | null, + darkMode: boolean | null, + intercomId: string | null, + }, + dayStreak: number | null, + longestDayStreak: number | null, + latestCompletedDate: string | null, + title: string, + description: string | null, + date: string, + days: Array< { + __typename: "DayCompletionRecord", + key: string | null, + value: number | null, + } | null >, + archived: boolean | null, + period: Period, + owner: string | null, + } | null, +}; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..70196b3 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,78 @@ +import Amplify from '@aws-amplify/core' +import * as Sentry from '@sentry/react-native' +import * as Analytics from 'appcenter-analytics' +import React from 'react' +import { addEventListener, removeEventListener } from 'react-native-localize' +import Purchases from 'react-native-purchases' +import { enableScreens } from 'react-native-screens' +import SplashScreen from 'react-native-splash-screen' +import { connect } from 'react-redux' +import { ThemeProvider } from 'styled-components/native' +import { sendError } from './actions/NotificationActions' +import amplify from './config/Amplify' +import AppWithNavigationState from './config/AppNavigation' +import CONFIG from './config/Config' +import { setI18nConfig } from './config/i18n' +import { getTheme } from './store/Selectors/UserSelectors' +import { darkTheme, lightTheme, ThemeProps } from './styles/themes' +import { State } from './Types/State' + +if (!__DEV__) { + Sentry.init({ + dsn: CONFIG.SENTRY_DSN + }) +} + +enableScreens() +Amplify.configure(amplify) + +interface AppProps { + sendError: (error: any) => void + theme: ThemeProps +} + +class App extends React.Component { + constructor(props: AppProps) { + super(props) + setI18nConfig() + } + + async componentDidMount() { + SplashScreen.hide() + this.enableAnalytics() + addEventListener('change', this.handleLocalizationChange) + Purchases.setDebugLogsEnabled(true) + Purchases.setup(CONFIG.REVENUE_CAT) + } + + componentWillUnmount() { + removeEventListener('change', this.handleLocalizationChange) + } + + handleLocalizationChange = () => { + setI18nConfig() + this.forceUpdate() + } + + enableAnalytics = async () => { + await Analytics.setEnabled(true) + Analytics.trackEvent('Opened app') + } + + render() { + const { theme } = this.props + const appTheme = theme && theme.mode === 'dark' ? darkTheme : lightTheme + + return ( + + + + ) + } +} + +const mapStateToProps = (state: State) => ({ + theme: getTheme(state) +}) + +export default connect(mapStateToProps, { sendError })(App) diff --git a/src/Hooks/UseAppState.ts b/src/Hooks/UseAppState.ts new file mode 100644 index 0000000..01e4eba --- /dev/null +++ b/src/Hooks/UseAppState.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react' +import { AppState, AppStateStatus } from 'react-native' + +function useAppState() { + const [appState, setAppState] = useState(AppState.currentState) + + useEffect(() => { + const handleAppStateChange = (nextAppState: AppStateStatus) => { + setAppState(nextAppState) + } + + AppState.addEventListener('change', handleAppStateChange) + + return () => { + AppState.removeEventListener('change', handleAppStateChange) + } + }, []) + + return appState +} + +export default useAppState diff --git a/src/Hooks/UseBackgroundFetch.ts b/src/Hooks/UseBackgroundFetch.ts new file mode 100644 index 0000000..2750657 --- /dev/null +++ b/src/Hooks/UseBackgroundFetch.ts @@ -0,0 +1,33 @@ +import { useEffect, useRef } from 'react' +import BackgroundFetch from 'react-native-background-fetch' +import { setI18nConfig } from 'config/i18n' + +const useBackgroundFetch = ( + minimumFetchInterval: number, + callback: () => void +) => { + const savedCallback = useRef(callback) + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + BackgroundFetch.configure( + { + minimumFetchInterval, + startOnBoot: false, // Prevent background events when Android device is rebooted + stopOnTerminate: false // Prevent background events when the app is terminated in Android + }, + async (taskId) => { + await setI18nConfig() + await savedCallback.current() + BackgroundFetch.finish(taskId) + }, + (error: any) => { + console.warn('[js] RNBackgroundFetch failed to start') + } + ) + }, []) +} + +export default useBackgroundFetch diff --git a/src/Hooks/UseInterval.ts b/src/Hooks/UseInterval.ts new file mode 100644 index 0000000..743a048 --- /dev/null +++ b/src/Hooks/UseInterval.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef } from 'react' + +function useInterval(callback, interval = 1000) { + const callbackRef = useRef || null + + useEffect(() => { + callbackRef.current = callback + }) + + useEffect(() => { + const tick = () => { + callbackRef.current && callbackRef.current() + } + + const id = setInterval(tick, interval) + return () => clearInterval(id) + }, [interval]) +} + +export default useInterval diff --git a/src/Hooks/UseNotificationEventHandlers.ts b/src/Hooks/UseNotificationEventHandlers.ts new file mode 100644 index 0000000..ef270e2 --- /dev/null +++ b/src/Hooks/UseNotificationEventHandlers.ts @@ -0,0 +1,51 @@ +import PushNotificationIOS from '@react-native-community/push-notification-ios' +import { useNavigation } from '@react-navigation/native' +import { useEffect } from 'react' +import { Platform } from 'react-native' +import firebase from 'react-native-firebase' +import { NotificationOpen } from 'react-native-firebase/notifications' +import { + COACHING_INCOMPLETE_LESSON, + COACHING_REMIND_LESSONS_IN_WEEK +} from '../config/PushNotifications' + +function useNotificationEventHandlers() { + const navigation = useNavigation() + + const handler = (notificationResponse: any) => { + let notificationId = '' + if (Platform.OS === 'android') { + notificationId = (notificationResponse).notification + .notificationId + } else if (Platform.OS === 'ios') { + notificationId = notificationResponse.notification.data.id + } + + handleNavigation(notificationId) + } + + // Navigate to the notified lesson or week when opening the notification + const handleNavigation = (notificationId: string) => { + if (notificationId === COACHING_REMIND_LESSONS_IN_WEEK.id) { + navigation.navigate('Coaching') + } else if (notificationId === COACHING_INCOMPLETE_LESSON.id) { + navigation.navigate('Coaching') + // navigation.navigate("LessonView", {}); TODO DISABLING FOR NOW BECAUSE THIS WOULD BREAK THE NAVIGATION + } + } + + useEffect(() => { + const notificationListener = firebase + .notifications() + .onNotificationOpened(handler) + + PushNotificationIOS.addEventListener('localNotification', handler) + + return () => { + notificationListener() + PushNotificationIOS.removeEventListener('localNotification', handler) + } + }, []) +} + +export default useNotificationEventHandlers diff --git a/src/Hooks/UseOnMount.ts b/src/Hooks/UseOnMount.ts new file mode 100644 index 0000000..ade7e69 --- /dev/null +++ b/src/Hooks/UseOnMount.ts @@ -0,0 +1,7 @@ +import { useEffect } from 'react' + +function useOnMount(onMount) { + useEffect(onMount, []) +} + +export default useOnMount diff --git a/src/Hooks/UseOnUpdate.ts b/src/Hooks/UseOnUpdate.ts new file mode 100644 index 0000000..761675e --- /dev/null +++ b/src/Hooks/UseOnUpdate.ts @@ -0,0 +1,23 @@ +import { useEffect, useRef } from 'react' + +function useOnUpdate(onUpdate: any, value: any) { + // Flag that inditcates whether we are in a mount or update phase + const isMounted = useRef(false) + + // Create a ref object to store the value + const valueRef = useRef(undefined) + + useEffect(() => { + const prevValue = valueRef.current + // If we are in an update effect invoke the callback with the prev value + if (!isMounted.current) { + isMounted.current = true + } else { + onUpdate(prevValue) + } + // Update the ref object each time the value is updated + valueRef.current = value + }, [value]) // Run only when the value updates +} + +export default useOnUpdate diff --git a/src/Hooks/useLinking.tsx b/src/Hooks/useLinking.tsx new file mode 100644 index 0000000..2cb9947 --- /dev/null +++ b/src/Hooks/useLinking.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { Linking } from 'react-native' +import { + getActionFromState, + getStateFromPath as getStateFromPathDefault, + NavigationContainerRef +} from '@react-navigation/core' +import { LinkingOptions } from '../../typings/react-navigation' + +let isUsingLinking = false + +export default function useLinking( + ref: React.RefObject, + { + prefixes, + config, + getStateFromPath = getStateFromPathDefault + }: LinkingOptions +) { + React.useEffect(() => { + if (isUsingLinking) { + throw new Error( + "Looks like you are using 'useLinking' in multiple components. This is likely an error since deep links should only be handled in one place to avoid conflicts." + ) + } else { + isUsingLinking = true + } + + return () => { + isUsingLinking = false + } + }) + + // We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners + // This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo` + // Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect + const prefixesRef = React.useRef(prefixes) + const configRef = React.useRef(config) + const getStateFromPathRef = React.useRef(getStateFromPath) + + React.useEffect(() => { + prefixesRef.current = prefixes + configRef.current = config + getStateFromPathRef.current = getStateFromPath + }, [config, getStateFromPath, prefixes]) + + const extractPathFromURL = React.useCallback((url: string) => { + for (const prefix of prefixesRef.current) { + if (url.startsWith(prefix)) { + return url.replace(prefix, '') + } + } + + return undefined + }, []) + + const getInitialState = React.useCallback(async () => { + const url = await Linking.getInitialURL() + const path = url ? extractPathFromURL(url) : null + + if (path) { + return getStateFromPathRef.current(path, configRef.current) + } + return undefined + }, [extractPathFromURL]) + + React.useEffect(() => { + const listener = ({ url }: { url: string }) => { + const path = extractPathFromURL(url) + const navigation = ref.current + + if (navigation && path) { + const state = getStateFromPathRef.current(path, configRef.current) + + if (state) { + const action = getActionFromState(state) + + if (action !== undefined) { + navigation.dispatch(action) + } else { + navigation.resetRoot(state) + } + } + } + } + + Linking.addEventListener('url', listener) + + return () => Linking.removeEventListener('url', listener) + }, [extractPathFromURL, ref]) + + return { + getInitialState + } +} diff --git a/src/Types/ChallengeState.ts b/src/Types/ChallengeState.ts new file mode 100644 index 0000000..c192777 --- /dev/null +++ b/src/Types/ChallengeState.ts @@ -0,0 +1,20 @@ +export enum ChallengeStates { + STATE_UNLOCKED = 'STATE_UNLOCKED', + STATE_REVEALED = 'STATE_REVEALED', + STATE_HIDDEN = 'STATE_HIDDEN', + STATE_COMPLETED = 'STATE_COMPLETED', + STATE_ONGOING = 'STATE_ONGOING' +} + +export interface ChallengeState { + challenges: Challenge[] | [] +} + +export interface Challenge { + id: string + state: ChallengeStates + titleFI: string + titleEN: string + descFI: string + descEN: string +} diff --git a/src/Types/CoachingContentState.ts b/src/Types/CoachingContentState.ts new file mode 100644 index 0000000..d07a656 --- /dev/null +++ b/src/Types/CoachingContentState.ts @@ -0,0 +1,67 @@ +import { Document } from '@contentful/rich-text-types' + +export type ContentUpdate = { + weeks: ContentWeek[] + lessons: ContentLesson[] + habits?: ExampleHabit[] + sections?: Section[] +} + +export interface CoachingContentState { + loading?: boolean + weeks: ContentWeek[] + lessons: ContentLesson[] + habits?: ExampleHabit[] + sections?: Section[] +} + +export interface ContentWeek { + order: number + contentId: string + weekName: string + intro: string + weekDescription: string + taskCount: number + lessons: string[] + coverPhoto: string + defaultLocked: boolean + duration: number + + slug: string +} + +export interface ContentLesson { + cover?: string + contentId: string + weekId?: string + lessonName?: string + lessonContent?: Document + additionalInformation?: Document + author?: string | null + authorCards?: AuthorCard[] + section?: Section + customComplete?: string + exampleHabit?: ExampleHabit[] + chronotype?: string + + slug: string + tags?: string[] +} + +export interface AuthorCard { + name: string + credentials: string + avatar: string +} + +export interface Section { + order?: number + title?: string + description?: Document +} + +export interface ExampleHabit { + title?: string + period?: string + description?: Document | string +} diff --git a/src/Types/CoachingNotificationState.ts b/src/Types/CoachingNotificationState.ts new file mode 100644 index 0000000..a06771b --- /dev/null +++ b/src/Types/CoachingNotificationState.ts @@ -0,0 +1,10 @@ +export interface CoachingNotificationState { + incompleteLessons: InteractedLesson[] +} + +export interface InteractedLesson { + latestInteractTimestamp: number + lessonId: string + weekId: string + slug: string +} diff --git a/src/Types/CoachingProfile.ts b/src/Types/CoachingProfile.ts new file mode 100644 index 0000000..270beb9 --- /dev/null +++ b/src/Types/CoachingProfile.ts @@ -0,0 +1,9 @@ +export default interface CoachingProfile { + chronotype: Chronotype | null +} + +export type Chronotype = { + EVENING: 'EVENING' + DAY: 'DAY' + MORNING: 'MORNING' +} diff --git a/src/Types/CoachingState.ts b/src/Types/CoachingState.ts new file mode 100644 index 0000000..b0fc725 --- /dev/null +++ b/src/Types/CoachingState.ts @@ -0,0 +1,43 @@ +export interface CoachingState { + selectedWeek: string | null + selectedLesson: string | null + + weeks: StateWeek[] + + ongoingWeek: string | null + currentWeekStarted: string | null + coachingStarted: string | null + stage: STAGE + + cloudId?: string + cloudUpdated?: string +} + +export interface StateWeek { + contentId: string // id for matching with contentful + completed?: boolean + completionDate?: string + lessons?: StateLesson[] + // completedLessons?: string[]; + locked?: boolean +} + +export interface StateLesson { + contentId: string + completed?: boolean +} + +export type StageType = { + NOT_ALLOWED: 'NOT_ALLOWED' + NOT_STARTED: 'NOT_STARTED' + ONGOING: 'ONGOING' + ENDED: 'ENDED' +} + +export enum STAGE { + NOT_ALLOWED, + NOT_STARTED, + ONGOING, + ENDED, + CURRENT_WEEK_COMPLETED +} diff --git a/src/Types/GetState.ts b/src/Types/GetState.ts new file mode 100644 index 0000000..6cd7a95 --- /dev/null +++ b/src/Types/GetState.ts @@ -0,0 +1,3 @@ +import { State } from './State' + +export type GetState = () => State diff --git a/src/Types/Microtask.ts b/src/Types/Microtask.ts new file mode 100644 index 0000000..1120701 --- /dev/null +++ b/src/Types/Microtask.ts @@ -0,0 +1,15 @@ +import { Period } from './State/Periods' + +export interface MicroTaskState { + tasks: MicroTask[] +} +export interface MicroTask { + id: string + userId: string + microTaskUserId: string + title: string + date: string + days: number[] + archived?: boolean + period: Period +} diff --git a/src/Types/ModalState.ts b/src/Types/ModalState.ts new file mode 100644 index 0000000..43e0b0f --- /dev/null +++ b/src/Types/ModalState.ts @@ -0,0 +1,7 @@ +export interface ModalState { + ratingModal: boolean + buySubscriptionModal: boolean + newHabitModal: boolean + editHabitModal: boolean + explanationsModal: boolean +} diff --git a/src/Types/NetworkState.ts b/src/Types/NetworkState.ts new file mode 100644 index 0000000..a5d1856 --- /dev/null +++ b/src/Types/NetworkState.ts @@ -0,0 +1,4 @@ +export interface NetworkState { + isConnected: boolean + actionQueue: Array +} diff --git a/src/Types/NotificationState.ts b/src/Types/NotificationState.ts new file mode 100644 index 0000000..13e405a --- /dev/null +++ b/src/Types/NotificationState.ts @@ -0,0 +1,48 @@ +export interface NotificationState { + intercomNotificationCount: number + message: string | null + type: NotificationType | null + + shouldAskForNotificationPermission?: boolean + + enabled: boolean + lastActivated?: number // timestamp to track the last activated date if needed + + scheduledNotifications?: ScheduledNotification[] + + // Control each child notification + customerSupportNotification?: NotificationChildState + bedtimeApproachNotification?: NotificationChildState + coachingNewNotification?: NotificationChildState + habitReminderNotification: NotificationChildState +} + +export type ScheduledNotification = { + id: string + title: string + fireDate: string +} + +export enum NotificationType { + ERROR = 'ERROR', + LOADING = 'LOADING', + WARNING = 'WARNING', + INFO = 'INFO' +} + +interface NotificationChildState { + enabled: boolean + lastActivated?: number // timestamp to track the last activated date if needed +} + +export enum UpdateNotificationPermissionType { + BEDTIME_APPROACH_NOTIFICATION = 'UPDATE_BEDTIME_APPROACH_NOTIFICATION_PERMISSION', + CUSTOMER_SUPPORT_NOTIFICATION = 'UPDATE_CUSTOMER_SUPPORT_NOTIFICATION_PERMISSION', + COACHING_NEW_NOTIFICATION = 'UPDATE_COACHING_NEW_NOTIFICATION_PERMISSION' +} + +export enum NotificationPermissionType { + BEDTIME_APPROACH_NOTIFICATION = 'ALLOW_BED_TIME_APPROACH', + CUSTOMER_SUPPORT_NOTIFICATION = 'ALLOW_CUSTOMER_SUPPORT', + COACHING_NEW_NOTIFICATION = 'ALLOW_COACHING_NEWS' +} diff --git a/src/Types/OnboardingState.ts b/src/Types/OnboardingState.ts new file mode 100644 index 0000000..85cbcf7 --- /dev/null +++ b/src/Types/OnboardingState.ts @@ -0,0 +1,4 @@ +export interface OnboardingState { + intercomNeedHelpRead: boolean + dataOnboardingCompleted: boolean +} diff --git a/src/Types/ReduxActions.ts b/src/Types/ReduxActions.ts new file mode 100644 index 0000000..753563e --- /dev/null +++ b/src/Types/ReduxActions.ts @@ -0,0 +1,17 @@ +import { ThunkAction, ThunkDispatch } from 'redux-thunk' +import { Action } from 'redux' +import { State } from './State' + +interface ReduxAction { + type: string + payload?: any + error?: any +} + +export default ReduxAction + +export type ThunkResult = ThunkAction + +export type Thunk = ThunkResult> + +export type Dispatch = ThunkDispatch diff --git a/src/Types/Sleep/Fitbit.ts b/src/Types/Sleep/Fitbit.ts new file mode 100644 index 0000000..f59d9dc --- /dev/null +++ b/src/Types/Sleep/Fitbit.ts @@ -0,0 +1,48 @@ +export type SummaryObject = { + count: number + minutes: number + thirtyDayAvgMinutes: number +} + +export type FitbitSleepObject = { + dateOfSleep: string + duration: number + efficiency: number + endTime: string + infoCode: number + isMainSleep: boolean + levels: { + data: { dateTime: string; level: string; seconds: number }[] + shortData: { dateTime: string; level: string; seconds: number }[] + summary: { + deep: SummaryObject + light: SummaryObject + rem: SummaryObject + wake: SummaryObject + } + } + + logId: number + minutesAfterWakeup: number + minutesAsleep: number + minutesAwake: number + minutesToFallAsleep: number + startTime: string + timeInBed: number + type: string +} + +export interface fitbitResponse { + sleep: FitbitSleepObject[] + summary: { + stages: { + deep: number + light: number + rem: number + wake: number + } + totalMinutesAsleep: number + totalSleepRecords: number + totalTimeInBed: number + } +} diff --git a/src/Types/Sleep/Oura.ts b/src/Types/Sleep/Oura.ts new file mode 100644 index 0000000..a2490df --- /dev/null +++ b/src/Types/Sleep/Oura.ts @@ -0,0 +1,34 @@ +export interface OuraSleepObject { + summary_date: string + period_id: number + is_longest: number + timezone: number + bedtime_start: string + bedtime_end: string + score: number + score_total: number + score_disturbances: number + score_efficiency: number + score_latency: number + score_rem: number + score_deep: number + score_alignment: number + total: number + duration: number + awake: number + light: number + rem: number + deep: number + onset_latency: number + restless: number + efficiency: number + midpoint_time: number + hr_lowest: number + hr_average: number + rmssd: number + breath_average: number + temperature_delta: number + hypnogram_5min: string + hr_5min: number[] + rmssd_5min: number[] +} diff --git a/src/Types/Sleep/Withings.ts b/src/Types/Sleep/Withings.ts new file mode 100644 index 0000000..bbd1bc5 --- /dev/null +++ b/src/Types/Sleep/Withings.ts @@ -0,0 +1,28 @@ +export interface WithingsSleepObject { + id: number | string + timezone: string + model: number + startdate: number + enddate: number + date: string + modified: number | string + data: { + breathing_disturbances_intensity?: number + deepsleepduration?: number + durationtosleep?: number + durationtowakeup?: number + hr_average?: number + hr_max?: number + hr_min?: number + lightsleepduration?: number + remsleepduration?: number + rr_average?: number + rr_max?: number + rr_min?: number + sleep_score?: number + snoring?: number + snoringepisodecount?: number + wakeupcount?: number + wakeupduration?: number + } +} diff --git a/src/Types/SleepClockState.ts b/src/Types/SleepClockState.ts new file mode 100644 index 0000000..21fa6f5 --- /dev/null +++ b/src/Types/SleepClockState.ts @@ -0,0 +1,34 @@ +import { Day, Night } from './Sleepdata' + +export interface SleepClockState { + primarySleepTrackingSource: { + sourceName: string + sourceId: string + } + bedTimeGoal: null + sleepTrackingSources: SleepDataSource[] | undefined + insights: { + goToSleepWindow: string + goToSleepWindowStart: string + goToSleepWindowCenter: string + goToSleepWindowEnd: string + } + healthKitEnabled: boolean + sleepDataUpdated: string + today: string + current_day: Day + selectedDay: Day + activeIndex: number | null + ratings: [] + days: Day[] | [] + nights: Night[] + + startDate: string + selectedItem: Day | null +} + +export type SleepDataSource = { + sourceName: string + sourceId: string + sampleCount?: number +} diff --git a/src/Types/Sleepdata.ts b/src/Types/Sleepdata.ts new file mode 100644 index 0000000..5bacbc0 --- /dev/null +++ b/src/Types/Sleepdata.ts @@ -0,0 +1,71 @@ +export interface iOSSample { + startDate: string + endDate: string + value: Value + sourceId?: string + sourceName?: string + id: string +} + +export interface AndroidSample { + start: string + end: number + value: Value + sourceId: string + name: string +} + +export interface Days { + days: Day[] +} + +export interface Day { + date: string + night: Night[] + unfilteredNight?: Night[] + + bedStart?: string | null + bedEnd?: string | null + sleepStart?: string | null + sleepEnd?: string | null + asleepDuration?: number + inBedDuration?: number + + rating?: number + + mutated?: boolean + id?: string +} + +export interface HealthKitSleepResponse { + sourceId: string + sourceName: string + id: string + value: string + startDate: string + endDate: string +} + +export type Night = { + source?: string + sourceId: string + sourceName: string + + value: Value + startDate: string + endDate: string + totalDuration: number +} + +export enum Value { + InBed = 'INBED', + Asleep = 'ASLEEP', + Awake = 'AWAKE' +} + +export interface InsightTypes { + goToSleepWindow?: string + goToSleepWindowStart: string + goToSleepWindowCenter: string + goToSleepWindowEnd: string +} diff --git a/src/Types/State.ts b/src/Types/State.ts new file mode 100644 index 0000000..a4b417a --- /dev/null +++ b/src/Types/State.ts @@ -0,0 +1,68 @@ +import { CoachingState, CoachingState } from 'typings/state/coaching-state' +import HealthKitState from 'typings/state/health-kit-state' +import { + SleepSourceState, + SleepSourceState +} from 'typings/state/sleep-source-state' +import { ChallengeState, ChallengeState } from './ChallengeState' +import { + CoachingContentState, + CoachingContentState +} from './CoachingContentState' +import { + CoachingNotificationState, + CoachingNotificationState +} from './CoachingNotificationState' +import { MicroTaskState, MicroTaskState } from './Microtask' +import { ModalState, ModalState } from './ModalState' +import { NetworkState, NetworkState } from './NetworkState' +import { NotificationState, NotificationState } from './NotificationState' +import { OnboardingState, OnboardingState } from './OnboardingState' +import { SleepClockState, SleepClockState } from './SleepClockState' +import { ApiState, ApiState } from './State/api-state' +import { AuthState, AuthState } from './State/AuthState' +import { CalendarState, CalendarState } from './State/CalendarState' +import { HabitState } from './State/habit-state' +import { LinkingState, LinkingState } from './State/linking-state' +import { ManualDataState, ManualDataState } from './State/ManualDataState' +import SleepDataSourceState from './State/SleepDataSourceState' +import { SubscriptionState, SubscriptionState } from './SubscriptionState' +import { TrackingState, TrackingState } from './TrackingState' +import { UserState, UserState } from './UserState' + +import { InsightState } from './State/insight-state' + +export interface State { + // User + user: UserState + + // Application + subscriptions: SubscriptionState + onboarding: OnboardingState + modals: ModalState + notifications: NotificationState + + // Coaching + coachingState: CoachingState + coachingContent: CoachingContentState + coachingNotification: CoachingNotificationState + microtask: MicroTaskState + challenge: ChallengeState + habitState: HabitState + // Sleep data + calendar: CalendarState + sleepclock: SleepClockState + sleepscore: any + // heartRate: any; + tracking: TrackingState + manualData: ManualDataState + sources: SleepDataSourceState + apis: ApiState + // network + network: NetworkState + auth: AuthState + linking: LinkingState + sleepSources: SleepSourceState + healthKit: HealthKitState + insights: InsightState +} diff --git a/src/Types/State/AuthState.ts b/src/Types/State/AuthState.ts new file mode 100644 index 0000000..aea56b5 --- /dev/null +++ b/src/Types/State/AuthState.ts @@ -0,0 +1,8 @@ +export interface AuthState { + loading: boolean + registerError?: string + authenticated: boolean + + password?: string + email?: string +} diff --git a/src/Types/State/ManualDataState.ts b/src/Types/State/ManualDataState.ts new file mode 100644 index 0000000..0309a89 --- /dev/null +++ b/src/Types/State/ManualDataState.ts @@ -0,0 +1,5 @@ +export interface ManualDataState { + editMode: boolean + startTime: { h: number; m: number } + endTime: { h: number; m: number } +} diff --git a/src/Types/State/Periods.ts b/src/Types/State/Periods.ts new file mode 100644 index 0000000..1c92e00 --- /dev/null +++ b/src/Types/State/Periods.ts @@ -0,0 +1,23 @@ +export const timePeriodsAll = { + morning: 'morning', + afternoon: 'afternoon', + evening: 'evening', + night: 'night' +} + +export const timePeriodsMicrotask = { + morning: 'morning', + afternoon: 'afternoon', + evening: 'evening' +} + +export enum Period { + morning = 'morning', + afternoon = 'afternoon', + evening = 'evening', + night = 'night' +} + +export interface Periods { + period: Period +} diff --git a/src/Types/State/SleepDataSourceState.ts b/src/Types/State/SleepDataSourceState.ts new file mode 100644 index 0000000..7ea0750 --- /dev/null +++ b/src/Types/State/SleepDataSourceState.ts @@ -0,0 +1,14 @@ +export default interface SleepDataSourceState { + linkedSource?: Source + accessTokenExpirationDate?: string + accessToken?: string + refreshToken?: string + tokenType?: string + idToken?: string +} + +export enum Source { + oura = 'oura', + withings = 'withings', + fitbit = 'fitbit' +} diff --git a/src/Types/State/api-state.ts b/src/Types/State/api-state.ts new file mode 100644 index 0000000..8aa60f6 --- /dev/null +++ b/src/Types/State/api-state.ts @@ -0,0 +1,58 @@ +import { RefreshResult, AuthorizeResult } from 'react-native-app-auth' + +export interface ApiState { + fitbit?: FitbitAuthResponse + googleFit?: GoogleFitResponse + garmin?: GarminResponse + polar?: PolarResponse + oura?: OuraResponse + suunto?: SuuntoResponse + withings?: WithingsResponse + + loadingFitbit: boolean + loadingGoogleFit: boolean + loadingOura: boolean +} + +export interface ResponseBase { + enabled: boolean + accessTokenExpirationDate: string + refreshToken: string + accessToken: string +} + +export interface FitbitAuthResponse extends ResponseBase { + user_id?: string +} + +export type GoogleFitResponse = ResponseBase + +export type SuuntoResponse = ResponseBase + +export interface OuraResponse extends ResponseBase { + user_id?: string +} + +export type GarminResponse = ResponseBase + +export type PolarResponse = ResponseBase + +export interface WithingsResponse extends ResponseBase { + user_id?: string +} + +export interface FitbitRefreshResult extends RefreshResult { + refreshToken: string + additionalParameters: { + user_id: string + } +} + +export interface FitbitAuthorizeResult extends AuthorizeResult { + refreshToken: string + tokenAdditionalParameters: { + user_id: string + } +} + +export type OuraAuthorizeResult = AuthorizeResult diff --git a/src/Types/State/days-state.ts b/src/Types/State/days-state.ts new file mode 100644 index 0000000..24ee608 --- /dev/null +++ b/src/Types/State/days-state.ts @@ -0,0 +1,7 @@ +import { Day, Night } from '../Sleepdata' + +export interface DaysState { + days: Day[] + nights?: Night[] + loading: boolean +} diff --git a/src/Types/State/habit-state.ts b/src/Types/State/habit-state.ts new file mode 100644 index 0000000..c207582 --- /dev/null +++ b/src/Types/State/habit-state.ts @@ -0,0 +1,42 @@ +import { Period } from './Periods' +import { Period as AmplifyPeriod } from '../../API' + +export interface HabitState { + habits: Map + subHabits: Map + draftEditHabit?: Habit + unsyncedHabits: Array + mergingDialogDisplayed?: boolean + loading?: boolean +} + +export interface Habit { + id: string + userId: string | null + dayStreak?: number + longestDayStreak?: number + latestCompletedDate?: string + title: string + description: string + date: string + days: Map + archived?: boolean + period: Period | AmplifyPeriod +} + +export interface UnsyncedHabit { + actionDate: string + mutationType: MutationType + habit: Habit +} + +export enum MutationType { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DELETE = 'DELETE' +} + +export interface DayCompletionRecordInput { + key: string + value: number +} diff --git a/src/Types/State/insight-state.ts b/src/Types/State/insight-state.ts new file mode 100644 index 0000000..b583b33 --- /dev/null +++ b/src/Types/State/insight-state.ts @@ -0,0 +1,8 @@ +export type InsightState = { + loading?: boolean + bedTimeWindow: { + start: string | undefined + center: string | undefined + end: string | undefined + } +} diff --git a/src/Types/State/linking-state.ts b/src/Types/State/linking-state.ts new file mode 100644 index 0000000..10a1003 --- /dev/null +++ b/src/Types/State/linking-state.ts @@ -0,0 +1,4 @@ +export interface LinkingState { + loading: boolean + linkCode?: string | null +} diff --git a/src/Types/SubscriptionState.ts b/src/Types/SubscriptionState.ts new file mode 100644 index 0000000..5b63cf5 --- /dev/null +++ b/src/Types/SubscriptionState.ts @@ -0,0 +1,21 @@ +export interface SubscriptionState { + loading: boolean + isActive: boolean + isSandbox?: boolean + expirationDate?: string +} + +export interface SubscriptionResponseiOS { + originalTransactionDateIOS?: number + originalTransactionIdentifierIOS?: string + productId?: string + transactionDate?: number + transactionId?: string + transactionReceipt?: string +} + +export enum SubscriptionSource { + PARTNER = 'PARTNER', + APP_STORE = 'APP_STORE', + PLAY_STORE = 'PLAY_STORE' +} diff --git a/src/Types/TrackingState.ts b/src/Types/TrackingState.ts new file mode 100644 index 0000000..1cd76ec --- /dev/null +++ b/src/Types/TrackingState.ts @@ -0,0 +1,9 @@ +export interface TrackingState { + useChargerTracking: boolean + isTrackingAutomatically: boolean + automaticTrackingStarted: string | null + automaticTrackingEnded: string | null + isTracking: boolean | null + + showManualButtons: boolean | null +} diff --git a/src/Types/UserState.ts b/src/Types/UserState.ts new file mode 100644 index 0000000..4316380 --- /dev/null +++ b/src/Types/UserState.ts @@ -0,0 +1,15 @@ +import { ThemeProps } from '../styles/themes' + +export interface UserState { + syncEnabled: boolean | null + introduction_completed: boolean | null + quickIntroCompleted: boolean | null + healthkit_enabled: boolean | null + appTheme: ThemeProps + connectionId?: string + username: string | null + email: string | null + loggedIn: boolean | null + intercomId: string | null + authenticated?: boolean +} diff --git a/src/Types/generated/contentful.d.ts b/src/Types/generated/contentful.d.ts new file mode 100644 index 0000000..526af3e --- /dev/null +++ b/src/Types/generated/contentful.d.ts @@ -0,0 +1,488 @@ +// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + +import { Asset, Entry } from 'contentful' +import { Document } from '@contentful/rich-text-types' + +export interface IAnswerFields { + /** Title */ + title: string + + /** Score */ + score: number +} + +/** Answer option to a multiple choice question */ + +export interface IAnswer extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'answer' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IAnswerTimePickerFields { + /** Title */ + title?: string | undefined + + /** Is a time picker */ + isTimePicker: boolean +} + +/** Time picker UI element for questionnaires. */ + +export interface IAnswerTimePicker extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'answerTimePicker' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IAuthorFields { + /** avatar */ + avatar?: Asset | undefined + + /** slug */ + slug?: string | undefined + + /** name */ + name?: string | undefined + + /** Credentials */ + credentials?: string | undefined +} + +/** Author card for coaching */ + +export interface IAuthor extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'author' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IChallengeFields { + /** Title */ + title: string + + /** state */ + state: 'HIDDEN' | 'VISIBLE' + + /** Description */ + description?: Document | undefined +} + +/** Challenges related to coaching */ + +export interface IChallenge extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'challenge' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface ICoachingWeekFields { + /** Cover photo */ + coverPhoto: Asset + + /** order */ + order: number + + /** Week Name */ + weekName?: string | undefined + + /** slug */ + slug: string + + /** Intro */ + intro?: string | undefined + + /** Description */ + weekDescription: Document + + /** Task count */ + taskCount?: number | undefined + + /** Lessons */ + lessons?: ILesson[] | undefined + + /** Locked */ + locked: boolean + + /** Week duration */ + duration: number +} + +export interface ICoachingWeek extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'coachingWeek' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IHabitFields { + /** title */ + title: string + + /** slug */ + slug: string + + /** period */ + period: 'MORNING' | 'EVENING' | 'AFTERNOON' + + /** description */ + description: Document +} + +/** Example habits for user */ + +export interface IHabit extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'habit' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface ILessonFields { + /** Lesson name */ + lessonName: string + + /** slug */ + slug: string + + /** Keywords */ + keywords?: string[] | undefined + + /** Author */ + author?: + | 'Pietari Nurmi' + | 'Eeva Siika-aho' + | 'Anu-Katriina Pesonen' + | 'Perttu Lähteenlahti' + | 'Liisa Kuula-Paavola' + | undefined + + /** Author card */ + authorCard?: IAuthor[] | undefined + + /** Lesson content */ + lessonContent?: Document | undefined + + /** Additional Information */ + additionalInformation?: Document | undefined + + /** Custom complete */ + customComplete?: string | undefined + + /** Stage */ + stage?: number | undefined + + /** Chronotype */ + chronotype?: 'MORNING_LARK' | 'NIGHT_OWL' | undefined + + /** cover */ + cover?: Asset | undefined + + /** section */ + section: ISection + + /** habit */ + habit?: IHabit[] | undefined + + /** weights */ + weights?: Record | undefined +} + +export interface ILesson extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'lesson' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IMetalessonFields { + /** Title */ + title: string + + /** Slug */ + slug: string + + /** Keywords */ + keywords?: string[] | undefined + + /** Lead Paragraph */ + leadParagraph?: Document | undefined + + /** Lessons */ + lessons?: ILesson[] | undefined + + /** Conclusion */ + conclusion?: Document | undefined + + /** Related Content */ + relatedContent?: + | (IHabit | ILesson | IMetalesson | IQuestionnaire)[] + | undefined +} + +/** Generate longer web-optimized articles by combining lessons */ + +export interface IMetalesson extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'metalesson' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IQuestionFields { + /** Title */ + title: string + + /** Type */ + type?: 'Select' | 'Slider' | 'Time picker' | undefined + + /** Question */ + question: string + + /** Answers */ + answers?: (IAnswer | ISliderAnswer | IAnswerTimePicker)[] | undefined +} + +/** Questionnaire question */ + +export interface IQuestion extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'question' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IQuestionnaireFields { + /** Title */ + title: string + + /** Slug */ + slug: string + + /** Description */ + description?: Document | undefined + + /** Questions */ + questions?: IQuestion[] | undefined + + /** Results */ + results?: IResult[] | undefined +} + +export interface IQuestionnaire extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'questionnaire' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface IResultFields { + /** Title */ + title?: string | undefined + + /** Score range */ + scoreRange: Record + + /** Description */ + description?: Document | undefined + + /** Details */ + details?: Document | undefined +} + +/** Questionnaire result text */ + +export interface IResult extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'result' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface ISectionFields { + /** order */ + order?: number | undefined + + /** title */ + title?: string | undefined + + /** description */ + description?: Document | undefined +} + +export interface ISection extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'section' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export interface ISliderAnswerFields { + /** Title */ + title?: string | undefined + + /** Is a slider */ + isSlider: boolean +} + +/** Slider element for questionnaires */ + +export interface ISliderAnswer extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'sliderAnswer' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + +export type CONTENT_TYPE = + | 'answer' + | 'answerTimePicker' + | 'author' + | 'challenge' + | 'coachingWeek' + | 'habit' + | 'lesson' + | 'metalesson' + | 'question' + | 'questionnaire' + | 'result' + | 'section' + | 'sliderAnswer' + +export type LOCALE_CODE = 'en-US' | 'fi-FI' + +export type CONTENTFUL_DEFAULT_LOCALE_CODE = 'en-US' diff --git a/src/Types/navigation/navigation.ts b/src/Types/navigation/navigation.ts new file mode 100644 index 0000000..1b2e020 --- /dev/null +++ b/src/Types/navigation/navigation.ts @@ -0,0 +1,24 @@ +import ROUTE from 'config/routes/Routes' + +export type RootStackParamList = { + readonly [ROUTE.APP]: { + readonly [ROUTE.JOURNAL]: { + readonly [ROUTE.TERVEYSTALO]: { connectionId: string } + readonly [ROUTE.SLEEP]: undefined + readonly [ROUTE.HABITS]: undefined + readonly [ROUTE.DETAIL]: undefined + } + readonly [ROUTE.COACHING]: { + readonly [ROUTE.WEEK]: undefined + readonly [ROUTE.LESSON]: undefined + } + readonly [ROUTE.PROFILE]: {} + } +} + +export type JournalStackParamList = { + readonly Terveystalo: { connectionId: string } + readonly [ROUTE.SLEEP]: undefined + readonly [ROUTE.HABITS]: undefined + readonly [ROUTE.DETAIL]: undefined +} diff --git a/src/actions/Cloud/Sleep.ts b/src/actions/Cloud/Sleep.ts new file mode 100644 index 0000000..1d70943 --- /dev/null +++ b/src/actions/Cloud/Sleep.ts @@ -0,0 +1,92 @@ +import { API, Auth, graphqlOperation } from 'aws-amplify' +import 'react-native-get-random-values' +import { v4 } from 'uuid' +import { + CreateSleepDataInput, + CreateSleepDataMutation, + NightSegmentInput, + UpdateSleepDataInput, + UpdateSleepDataMutation +} from '../../API' +import { createSleepData, updateSleepData } from '../../graphql/mutations' +import { GetState } from '../../Types/GetState' +import { Day, Night } from '../../Types/Sleepdata' +import { updateDay } from '../sleep/sleep-data-actions' + +export const updateSleepDataInCloud = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + sleepclock: { days } + } = getState() + + const createPromises: Promise[] = [] + const updatePromises: Promise[] = [] + const updateStatePromises: Promise[] = [] + + const { username } = await Auth.currentUserInfo() + if (username) { + try { + days.forEach((day: Day) => { + if (day.mutated && day.id) { + const input: UpdateSleepDataInput = { + id: day.id, + date: day.date, + sleepDataUserId: username, + rating: day.rating, + night: formatNightToNightSegmentInput(day.night) + } + + updatePromises.push( + API.graphql( + graphqlOperation(updateSleepData, { + input + }) + ) + ) + } else { + const id = v4() + const input: CreateSleepDataInput = { + id, + date: day.date, + sleepDataUserId: username, + rating: day.rating, + night: formatNightToNightSegmentInput(day.night) + } + + const updatedDay = { ...day, id, updated: false } + + createPromises.push( + API.graphql( + graphqlOperation(createSleepData, { + input + }) + ) + ) + updateStatePromises.push(dispatch(updateDay(updatedDay))) + } + }) + + await Promise.all(createPromises) + await Promise.all(updatePromises) + await Promise.all(updateStatePromises) + } catch (error) { + console.warn(error) + } + } +} + +const formatNightToNightSegmentInput = ( + nights: Night[] +): NightSegmentInput[] => { + const formated: NightSegmentInput[] = nights.map((night) => ({ + value: night.value, + sourceName: night.sourceName, + sourceId: night.sourceId, + startDate: night.startDate, + endDate: night.endDate + })) + + return formated +} diff --git a/src/actions/Cloud/User.ts b/src/actions/Cloud/User.ts new file mode 100644 index 0000000..70f4c72 --- /dev/null +++ b/src/actions/Cloud/User.ts @@ -0,0 +1,25 @@ +import { API, graphqlOperation, Auth } from 'aws-amplify' +import { getUser } from '../../graphql/queries' +import { updateUser } from '../../graphql/mutations' +import { sendError } from '../NotificationActions' +import { GetState } from '../../Types/GetState' +import { setAuthStatus, updateUserFromCloud } from '../user/user-actions' + +export const checkAuthStatus = () => async (dispatch: Function) => { + const user = await Auth.currentUserInfo() + if (user) { + await dispatch(setAuthStatus(true)) + } else { + dispatch(setAuthStatus(false)) + } +} + +export const getUserData = () => async (dispatch: Function) => { + try { + const { username } = await Auth.currentUserInfo() + const res: any = await API.graphql( + graphqlOperation(getUser, { id: username }) + ) + await dispatch(updateUserFromCloud(res.data.getUser)) + } catch (error) {} +} diff --git a/src/actions/CoachingNotification/CoachingNotificationActions.ts b/src/actions/CoachingNotification/CoachingNotificationActions.ts new file mode 100644 index 0000000..fca11a1 --- /dev/null +++ b/src/actions/CoachingNotification/CoachingNotificationActions.ts @@ -0,0 +1,34 @@ +import { getStateWeeks } from 'store/Selectors/coaching-selectors' +import { InteractedLesson } from '../../Types/CoachingNotificationState' +import { GetState } from '../../Types/GetState' + +export const PUSH_TO_INCOMPLETE_LESSONS = 'PUSH_TO_INCOMPLETE_LESSONS' +export const POP_FROM_INCOMPLETE_LESSONS = 'POP_FROM_INCOMPLETE_LESSONS' + +export const pushToIncompleteLessons = ( + interactedLesson: InteractedLesson +) => ({ + type: PUSH_TO_INCOMPLETE_LESSONS, + payload: interactedLesson +}) + +export const popFromIncompleteLessons = ( + interactedLesson: InteractedLesson +) => ({ + type: POP_FROM_INCOMPLETE_LESSONS, + payload: interactedLesson +}) + +export const pushInteractedLesson = (lesson: InteractedLesson) => ( + dispatch: Function, + getState: GetState +) => { + const weeks = getStateWeeks(getState()) + const weekDetail = weeks?.find((w) => w.slug === lesson.slug) + const lessonDetail = weekDetail?.lessons?.find((l) => l === lesson.slug) + const completed = lessonDetail?.completed + + if (!completed) { + dispatch(pushToIncompleteLessons(lesson)) + } +} diff --git a/src/actions/IntercomActions.ts b/src/actions/IntercomActions.ts new file mode 100644 index 0000000..3f0e56a --- /dev/null +++ b/src/actions/IntercomActions.ts @@ -0,0 +1,45 @@ +import 'react-native-get-random-values' +import { v4 } from 'uuid' +import Intercom from 'react-native-intercom' +import { GetState } from '../Types/GetState' +import { setIntercomId } from './user/user-actions' + +export const registerIntercomUser = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + user: { intercomId } + } = getState() + const id = intercomId || v4() + + await Intercom.registerIdentifiedUser({ + userId: id + }) + + await dispatch(setIntercomId(id)) +} + +export const updateUnreadCount = (count: number) => async ( + dispatch: Function +) => {} + +interface IntercomSubscriptionStatus { + subscription: 'active' | 'not active' + latestPurchaseDate?: string + expirationDate?: string | null +} + +export const updateIntercomInformation = async ({ + subscription, + latestPurchaseDate, + expirationDate +}: IntercomSubscriptionStatus) => { + await Intercom.updateUser({ + custom_attributes: { + subscription, + purchase_date: latestPurchaseDate || '', + expiration_date: expirationDate || 'lifetime' + } + }) +} diff --git a/src/actions/MicrotaskActions.ts b/src/actions/MicrotaskActions.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/actions/NotificationActions.ts b/src/actions/NotificationActions.ts new file mode 100644 index 0000000..88ccf43 --- /dev/null +++ b/src/actions/NotificationActions.ts @@ -0,0 +1,380 @@ +import PushNotificationIOS, { + PushNotificationPermissions +} from '@react-native-community/push-notification-ios' +import moment from 'moment' +import { Platform } from 'react-native' +import Firebase from 'react-native-firebase' +import Intercom from 'react-native-intercom' +import translate from 'config/i18n' +import { actionCreators } from '../store/Reducers/NotificationReducer' +import { GetState } from '../Types/GetState' +import { + NotificationType, + UpdateNotificationPermissionType, + ScheduledNotification +} from '../Types/NotificationState' +import { Night } from '../Types/Sleepdata' +import { + androidChannels, + BEDTIME_APPROACH, + COACHING_REMIND_LESSONS_IN_WEEK, + COACHING_INCOMPLETE_LESSON, + NotificationObject +} from '../config/PushNotifications' + +const { notifications: firebaseNotifications } = Firebase +const { + setShouldAskNotificationPermission, + newNotification, + updateNotificationPermission, + addScheduledNotification, + removeScheduledNotification +} = actionCreators + +export const askForPush = () => async (dispatch: Function) => { + if (Platform.OS === 'ios') { + const permissions: PushNotificationPermissions = await PushNotificationIOS.requestPermissions( + { + badge: true, + alert: true, + sound: true + } + ) + if (permissions.alert) { + await dispatch(setShouldAskNotificationPermission(false)) + } + } else { + const FCMToken = await Firebase.messaging().getToken() + await Intercom.sendTokenToIntercom(FCMToken) + await dispatch(setShouldAskNotificationPermission(false)) + } +} + +export const setNotification = ( + type: UpdateNotificationPermissionType, + enabled: boolean +) => (dispatch: Function) => { + dispatch(updateNotificationPermission(type, enabled)) +} + +export const createAndroidChannels = async () => { + const bedtimeChannel = new firebaseNotifications.Android.Channel( + androidChannels.BEDTIME.id, + androidChannels.BEDTIME.name, + androidChannels.BEDTIME.importance + ).setDescription('Channel for bedtime-related notifications') + + const coachingChannel = new firebaseNotifications.Android.Channel( + androidChannels.COACHING.id, + androidChannels.COACHING.name, + androidChannels.COACHING.importance + ).setDescription('Channel for coaching-related notifications') + + const channels = [bedtimeChannel, coachingChannel] + + await firebaseNotifications().android.createChannels(channels) +} + +/* START - HANDLE BEDTIME APPROACH NOTIFICATIONS */ +export const handleBedtimeApproachNotifications = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + sleepclock: { nights, insights }, + notifications: { + bedtimeApproachNotification: { enabled: bedtimeNotificationEnabled } = { + enabled: false + } + } + } = getState() + + const dateISOString = moment() // Get today moment + .startOf('day') + .subtract(1, 'day') + .toISOString() + + const notification = { + ...BEDTIME_APPROACH, + title: translate('BEDTIME_NOTIFICATION.TITLE'), + body: translate('BEDTIME_NOTIFICATION.BODY') + } + dispatch(cancelLocalNotifications(notification)) + + if (bedtimeNotificationEnabled) { + const tonightIndex = ((nights: Night[]) => { + const index = nights.findIndex((night) => { + const nightDate = moment(night.startDate).toDate() + const tonightDate = moment(dateISOString).toDate() + return ( + nightDate.getDate() === tonightDate.getDate() && + nightDate.getMonth() === tonightDate.getMonth() && + nightDate.getFullYear() === tonightDate.getFullYear() + ) + }) + return index + })(nights) + + const MSBeforeNotify = 60 * 60 * 1000 // 1 hour + let tonightStartDate = '' + + if (tonightIndex > -1) { + tonightStartDate = nights[tonightIndex].startDate + } else { + tonightStartDate = insights.goToSleepWindowStart + } + + if (tonightStartDate.length > 0) { + const startDateMS = moment(tonightStartDate).toDate().getTime() + const notifyDateMS = startDateMS - MSBeforeNotify + 24 * 60 * 60 * 1000 // add 1 day miliseconds as sleepData's date is subtracted by 1 day + const scheduledNotifyTime = new Date(notifyDateMS).toISOString() + + // Only send a new notification when scheduled time is ahead current time + if (notifyDateMS > Date.now()) { + if (Platform.OS === 'ios') { + dispatch(scheduleIosNotification(notification, scheduledNotifyTime)) + } else if (Platform.OS === 'android') { + dispatch(scheduleAndroidNotification(notification, notifyDateMS)) + } + } + } + } +} +/* END - HANDLE BEDTIME APPROACH NOTIFICATIONS */ + +/* START- HANDLE COACHING NOTIFICATIONS */ + +export const handleCoachingUncompletedLessonNotifications = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + subscriptions: { isActive: coachingActivated }, + notifications: { + coachingNewNotification: { enabled: coachingEnabled } = { + enabled: false + }, + scheduledNotifications + }, + coachingContent: { lessons }, + coachingNotification: { incompleteLessons } + } = getState() + const notification = { + ...COACHING_INCOMPLETE_LESSON, + title: translate('INCOMPLETE_LESSON_NOTIFICATION.TITLE'), + body: translate('INCOMPLETE_LESSON_NOTIFICATION.BODY') + } + + if (coachingActivated && incompleteLessons.length > 0 && coachingEnabled) { + const latestUncompletedLesson = incompleteLessons[0] + const { lessonId } = latestUncompletedLesson + const lessonDetail = lessons.find((lesson) => lesson.contentId === lessonId) + const lessonName = lessonDetail?.lessonName // TODO GET BACK TO THIS + + if (isOldFireDateBehindToday(scheduledNotifications, notification.id)) { + dispatch(cancelLocalNotifications(notification)) + + // Scheduled hour is 12pm everyday + const fireDate = moment() + .set({ + hour: 12, + minute: 0, + second: 0, + millisecond: 0 + }) + .add(1, 'day') + + if (Platform.OS === 'ios') { + dispatch(cancelLocalNotifications(notification)) + + dispatch(scheduleIosNotification(notification, fireDate.toISOString())) + } else if (Platform.OS === 'android') { + dispatch(scheduleAndroidNotification(notification, fireDate.valueOf())) + } + } + + /* If the saved date is ahead of today, we do not need to implement a new scheduled one meaning + the saved one will not be replaced until current time passes the defined fire date. */ + } else { + dispatch(cancelLocalNotifications(notification)) + } +} + +export const handleCoachingLessonsInWeekNotifications = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + notifications: { + coachingNewNotification: { enabled: coachingEnabled } = { + enabled: false + }, + scheduledNotifications + }, + coachingContent: { weeks }, + coachingState: { ongoingWeek, weeks: stateWeeks, currentWeekStarted }, + subscriptions: { isActive: coachingActivated } + } = getState() + + const notification = { + ...COACHING_REMIND_LESSONS_IN_WEEK, + title: translate('LESSONS_IN_WEEK_NOTIFICATION.TITLE'), + body: translate('LESSONS_IN_WEEK_NOTIFICATION.BODY') + } + + if (coachingEnabled && coachingActivated) { + if (ongoingWeek && currentWeekStarted) { + const stateWeek = stateWeeks.find( + (week) => week.contentId === ongoingWeek + ) + const contentWeek = weeks.find((week) => week.contentId === ongoingWeek) + + if (stateWeek && contentWeek) { + const enoughLessons = stateWeek.lessons + ? stateWeek.lessons.filter((lesson) => lesson.completed).length === + contentWeek.lessons.length + : false + + if (!enoughLessons) { + if ( + isOldFireDateBehindToday(scheduledNotifications, notification.id) + ) { + dispatch(cancelLocalNotifications(notification)) + + // Every 10am + const fireDate = moment() + .set({ + hour: 10, + minute: 0, + second: 0, + millisecond: 0 + }) + .add(1, 'day') + + if (Platform.OS === 'ios') { + dispatch( + scheduleIosNotification(notification, fireDate.toISOString()) + ) + } else if (Platform.OS === 'android') { + dispatch( + scheduleAndroidNotification(notification, fireDate.valueOf()) + ) + } + } + + /* If the saved date is ahead of today, we do not need to implement a new scheduled one meaning + the saved one will not be replaced until current time passes the defined fire date. */ + } + } + } else { + dispatch(cancelLocalNotifications(notification)) + } + } else { + dispatch(cancelLocalNotifications(notification)) + } +} +/* END- HANDLE COACHING NOTIFICATIONS */ + +const isOldFireDateBehindToday = ( + scheduledNotifications: ScheduledNotification[] | undefined, + notificationId: string +) => { + // index of the notification in scheduledNotifications. -1 means none + const index = scheduledNotifications + ? scheduledNotifications.findIndex( + (scheduledNotification) => scheduledNotification.id === notificationId + ) + : -1 + + // The fire date of the found scheduled noti. undefined means no noti found + const oldFireDate = + scheduledNotifications && index > -1 + ? scheduledNotifications[index].fireDate + : undefined + + // To check if the saved fire date is behind today. undefined value means no noti => set to true + // to add a new scheduled noti when true + return oldFireDate + ? moment(oldFireDate).valueOf() - moment().valueOf() < 0 + : true +} + +export const cancelLocalNotifications = ( + notification: NotificationObject +) => async (dispatch: Function) => { + const { userInfo, id } = notification + + if (Platform.OS === 'ios') { + PushNotificationIOS.cancelLocalNotifications(userInfo) + } else if (Platform.OS === 'android') { + await firebaseNotifications().cancelNotification(id) + } + + dispatch(removeScheduledNotification(id)) +} + +const scheduleAndroidNotification = ( + notification: NotificationObject, + fireDate: number +) => async (dispatch: Function) => { + const { id, title, body, channelID, smallIcon, largeIcon } = notification + + const notificationObject = new firebaseNotifications.Notification() + .setNotificationId(id) + .setTitle(title) + .setBody(body) + .android.setChannelId(channelID) + .android.setSmallIcon(smallIcon) + .android.setLargeIcon(largeIcon) + + await firebaseNotifications().scheduleNotification(notificationObject, { + fireDate + }) + + dispatch( + addScheduledNotification({ + id, + title, + fireDate: moment(fireDate).toISOString() + }) + ) +} + +const scheduleIosNotification = ( + notification: NotificationObject, + fireDate: string +) => async (dispatch: Function) => { + const { title, body, userInfo, id } = notification + PushNotificationIOS.scheduleLocalNotification({ + userInfo, + alertTitle: title, + alertBody: body, + fireDate + }) + + dispatch(addScheduledNotification({ id, title, fireDate })) +} + +/* ERROR HANDLING */ + +type errorObject = { + message: string +} + +export const sendError = (error: errorObject) => async (dispatch: Function) => { + if (error.message) { + await dispatch( + newNotification({ + message: error.message, + type: NotificationType.ERROR + }) + ) + } else { + await dispatch( + newNotification({ + message: JSON.stringify(error), + type: NotificationType.ERROR + }) + ) + } +} diff --git a/src/actions/RefreshActions.ts b/src/actions/RefreshActions.ts new file mode 100644 index 0000000..46ba4ee --- /dev/null +++ b/src/actions/RefreshActions.ts @@ -0,0 +1,24 @@ +import { getSleepDataUpdated } from '../store/Selectors/SleepDataSelectors' +import { GetState } from '../Types/GetState' +import { fetchSleepData } from './sleep/sleep-data-actions' +import { prepareSleepDataFetching } from './sleep/health-kit-actions' + +export const refreshSleep = () => async ( + dispatch: Function, + getState: GetState +) => { + const sleepDataUpdate = getSleepDataUpdated(getState()) + await dispatch(prepareSleepDataFetching()) + await dispatch(fetchSleepData()) + // await dispatch(getAllWeeks()); +} + +export const refreshCoaching = async ( + dispatch: Function, + getState: GetState +) => {} + +export const refreshSubscription = async ( + dispatch: Function, + getState: GetState +) => {} diff --git a/src/actions/StartupActions.ts b/src/actions/StartupActions.ts new file mode 100644 index 0000000..9e0beb5 --- /dev/null +++ b/src/actions/StartupActions.ts @@ -0,0 +1,73 @@ +import { Platform } from 'react-native' +import { getIsHealthKitMainSource } from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { getWeek } from 'store/Selectors/SleepDataSelectors' +import { Dispatch, Thunk } from 'Types/ReduxActions' +import { getAuthState } from '../store/Selectors/auth-selectors/auth-selectors' +import { GetState } from '../Types/GetState' +import { refreshAuthStatus } from './auth/auth-actions' +import { + updateCoachingInCloud, + validateWeeklyProgress +} from './coaching/coaching-actions' +import { + handleUnsyncedHabitsThenRetrieveHabitsFromCloud, + updateDayStreaks +} from './habit/habit-actions' +import { calculateInsights } from './insight-actions/insight-actions' +import { + createAndroidChannels, + handleBedtimeApproachNotifications, + handleCoachingLessonsInWeekNotifications, + handleCoachingUncompletedLessonNotifications +} from './NotificationActions' +import { prepareSleepDataFetching } from './sleep/health-kit-actions' +import { fetchSleepData, updateCalendar } from './sleep/sleep-data-actions' +import { updateSubscriptionStatus } from './subscription/subscription-actions' + +export const startup = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const isAuthenticated = getAuthState(getState()) + const isUsingHealthKit = getIsHealthKitMainSource(getState()) + // Create necessary Android channels + if (Platform.OS === 'android') { + await createAndroidChannels() + } + + await dispatch(handleUnsyncedHabitsThenRetrieveHabitsFromCloud()) + + // await dispatch(pullSleepFromCloud()); + await dispatch(updateCalendar()) + + // Actions related to sleep data + if (isUsingHealthKit) { + await dispatch(prepareSleepDataFetching()) + } + + await dispatch(fetchSleepData()) + + await dispatch(updateSubscriptionStatus()) + await dispatch(refreshAuthStatus()) + // // Action related to coaching + await dispatch(validateWeeklyProgress()) + + await dispatch(updateDayStreaks()) + + if (isAuthenticated) { + await dispatch(updateCoachingInCloud()) + } + await dispatch(calculateInsights()) + + await dispatch(handleBedtimeApproachNotifications()) + await dispatch(handleCoachingUncompletedLessonNotifications()) + await dispatch(handleCoachingLessonsInWeekNotifications()) +} + +export const backgroundAction = (): Thunk => async (dispatch: Dispatch) => { + await dispatch(updateCalendar()) + await dispatch(handleBedtimeApproachNotifications()) + await dispatch(handleCoachingUncompletedLessonNotifications()) + await dispatch(handleCoachingLessonsInWeekNotifications()) + await dispatch(handleUnsyncedHabitsThenRetrieveHabitsFromCloud()) +} diff --git a/src/actions/api-actions/fitbit-actions.ts b/src/actions/api-actions/fitbit-actions.ts new file mode 100644 index 0000000..e54c780 --- /dev/null +++ b/src/actions/api-actions/fitbit-actions.ts @@ -0,0 +1,213 @@ +import { revokePreviousSource } from '@actions/sleep-source-actions/revoke-previous-source' +import { setMainSource } from '@actions/sleep-source-actions/sleep-source-actions' +import { formatSleepData } from '@actions/sleep/sleep-data-actions' +import CONFIG from 'config/Config' +import { formatFitbitSamples } from 'helpers/sleep/fitbit-helper' +import moment from 'moment' +import { authorize, refresh, revoke } from 'react-native-app-auth' +import ReduxAction, { Dispatch, Thunk } from 'Types/ReduxActions' +import { SOURCE } from 'typings/state/sleep-source-state' +import { + getFitbitEnabled, + getFitbitToken +} from '../../store/Selectors/api-selectors/api-selectors' +import { GetState } from '../../Types/GetState' +import { + FitbitAuthorizeResult, + FitbitAuthResponse, + FitbitRefreshResult +} from '../../Types/State/api-state' + +export const FITBIT_AUTHORIZE_SUCCESS = 'FITBIT_AUTHORIZE_SUCCESS' +export const FITBIT_REVOKE_SUCCESS = 'FITBIT_REVOKE_SUCCESS' +export const FITBIT_UPDATE_TOKEN = 'FITBIT_UPDATE_TOKEN' + +export const FETCH_SLEEP_FITBIT_START = 'FETCH_SLEEP_FITBIT_START' +export const FETCH_SLEEP_FITBIT_SUCCESS = 'FETCH_SLEEP_FITBIT_SUCCESS' +export const FETCH_SLEEP_FITBIT_FAILURE = 'FETCH_SLEEP_FITBIT_FAILURE' + +/* ACTIONS */ + +export const fitbitAuthorizeSuccess = ( + payload: FitbitAuthResponse +): ReduxAction => ({ + type: FITBIT_AUTHORIZE_SUCCESS, + payload +}) + +export const fitbitRevokeSuccess = (): ReduxAction => ({ + type: FITBIT_REVOKE_SUCCESS +}) + +export const fitbitUpdateToken = ( + payload: FitbitAuthResponse +): ReduxAction => ({ + type: FITBIT_UPDATE_TOKEN, + payload +}) + +export const fetchSleepFitbitStart = (): ReduxAction => ({ + type: FETCH_SLEEP_FITBIT_START +}) + +export const fetchSleepFitbitSuccess = (): ReduxAction => ({ + type: FETCH_SLEEP_FITBIT_SUCCESS +}) + +export const fetchSleepFitbitFailure = (): ReduxAction => ({ + type: FETCH_SLEEP_FITBIT_FAILURE +}) + +/* ASYNC ACTIONS */ + +export const toggleFitbit = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const enabled = getFitbitEnabled(getState()) + + try { + if (enabled) { + dispatch(revokeFitbitAccess()) + } else { + await dispatch(revokePreviousSource()) + await dispatch(authorizeFitbit()) + } + } catch (err) { + console.warn(err) + } +} + +export const authorizeFitbit = (): Thunk => async (dispatch: Dispatch) => { + try { + const response = (await authorize( + CONFIG.FITBIT_CONFIG + )) as FitbitAuthorizeResult + + const { + accessTokenExpirationDate, + refreshToken, + accessToken, + tokenAdditionalParameters: { user_id } + } = response + + await dispatch( + fitbitAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken, + accessToken, + user_id, + enabled: true + }) + ) + await dispatch(setMainSource(SOURCE.FITBIT)) + } catch (error) { + console.warn('authorizeFitbit', error) + } +} + +export const refreshFitbitToken = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { refreshToken: oldToken } = getFitbitToken(getState()) + + if (oldToken) { + try { + const response = (await refresh(CONFIG.FITBIT_CONFIG, { + refreshToken: oldToken + })) as FitbitRefreshResult + + const { + accessTokenExpirationDate, + refreshToken, + accessToken, + additionalParameters: { user_id } + } = response + + dispatch( + fitbitAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken, + accessToken, + user_id, + enabled: true + }) + ) + } catch (error) { + console.warn(error) + } + } +} + +export const revokeFitbitAccess = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { accessToken } = getFitbitToken(getState()) + + try { + if (accessToken) { + await revoke(CONFIG.FITBIT_CONFIG, { + tokenToRevoke: accessToken, + includeBasicAuth: true + }) + + dispatch(fitbitRevokeSuccess()) + dispatch(setMainSource(SOURCE.NO_SOURCE)) + } + } catch (error) { + console.warn(error) + } +} + +export const getFitbitSleep = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { user_id, accessToken, accessTokenExpirationDate } = getFitbitToken( + getState() + ) + dispatch(fetchSleepFitbitStart()) + + const startDate = moment().subtract(1, 'week').format('YYYY-MM-DD') + const endDate = moment().format('YYYY-MM-DD') + if (accessToken) { + try { + if (moment(accessTokenExpirationDate).isAfter(moment())) { + const fitbitApiCall = await fetch( + `https://api.fitbit.com/1.2/user/${user_id}/sleep/date/${startDate}/${endDate}.json`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await fitbitApiCall.json() + const formattedResponse = formatFitbitSamples(response.sleep) + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepFitbitSuccess()) + } else { + await dispatch(refreshFitbitToken()) + const fitbitApiCall = await fetch( + `https://api.fitbit.com/1.2/user/${user_id}/sleep/date/${startDate}/${endDate}.json`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await fitbitApiCall.json() + const formattedResponse = formatFitbitSamples(response.sleep) + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepFitbitSuccess()) + } + } catch (error) { + dispatch(fetchSleepFitbitFailure()) + } + } +} diff --git a/src/actions/api-actions/garmin-actions.ts b/src/actions/api-actions/garmin-actions.ts new file mode 100644 index 0000000..f25fcca --- /dev/null +++ b/src/actions/api-actions/garmin-actions.ts @@ -0,0 +1,58 @@ +import { authorize, refresh, revoke } from 'react-native-app-auth' +import { GetState } from '../../Types/GetState' + +const config = { + clientId: '', + clientSecret: '', + redirectUrl: 'nyxo://callback', + scopes: ['personal', 'daily'], + useNonce: true, + serviceConfiguration: { + authorizationEndpoint: + 'https://connectapi.garmin.com/oauth-service/oauth/request_token', + tokenEndpoint: + 'https://connectapi.garmin.com/oauth-service/oauth/request_token' + } +} + +export const authorizeGarmin = () => async (dispatch: Function) => { + try { + const response = await authorize(config) + } catch (error) { + console.warn(error) + } +} + +export const refreshGarminToken = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + apis: { googleFit } + } = getState() + if (googleFit) { + const { refreshToken: oldToken } = googleFit + try { + const response = await refresh(config, { refreshToken: oldToken }) + } catch (error) { + console.warn(error) + } + } +} + +export const revokeGarminAccess = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + apis: { googleFit } + } = getState() + + if (googleFit) { + const { refreshToken: oldToken } = googleFit + const response = await revoke(config, { + tokenToRevoke: oldToken + }) + console.log(response) + } +} diff --git a/src/actions/api-actions/google-fit-actions.ts b/src/actions/api-actions/google-fit-actions.ts new file mode 100644 index 0000000..64293c1 --- /dev/null +++ b/src/actions/api-actions/google-fit-actions.ts @@ -0,0 +1,279 @@ +import { revokePreviousSource } from '@actions/sleep-source-actions/revoke-previous-source' +import { + changeGoogleFitSource, + setMainSource, + updateGoogleFitSources +} from '@actions/sleep-source-actions/sleep-source-actions' +import { + fetchSleepData, + formatSleepData +} from '@actions/sleep/sleep-data-actions' +import CONFIG from 'config/Config' +import { formatGoogleFitData } from 'helpers/sleep/google-fit-helper' +import moment from 'moment' +import { Platform } from 'react-native' +import { authorize, refresh, revoke } from 'react-native-app-auth' +import { + getGoogleFitEnabled, + getGoogleFitToken +} from 'store/Selectors/api-selectors/api-selectors' +import { getGoogleFitSource } from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { Dispatch, Thunk } from 'Types/ReduxActions' +import { SleepDataSource } from 'Types/SleepClockState' +import { Night } from 'Types/Sleepdata' +import { SOURCE, SUB_SOURCE } from 'typings/state/sleep-source-state' +import { GetState } from '../../Types/GetState' +import { GoogleFitResponse } from '../../Types/State/api-state' +/* ACTION TYPES */ + +export const GOOGLE_FIT_AUTHORIZE_SUCCESS = 'GOOGLE_FIT_AUTHORIZE_SUCCESS' +export const GOOGLE_FIT_REVOKE_SUCCESS = 'GOOGLE_FIT_REVOKE_SUCCESS' +export const GOOGLE_FIT_UPDATE_TOKEN = 'GOOGLE_FIT_UPDATE_TOKEN' + +export const FETCH_GOOGLE_FIT_START = 'FETCH_GOOGLE_FIT_START' +export const FETCH_GOOGLE_FIT_SUCCESS = 'FETCH_GOOGLE_FIT_SUCCESS' +export const FETCH_GOOGLE_FIT_FAILURE = 'FETCH_GOOGLE_FIT_FAILURE' + +/* ACTIONS */ + +export const googleFitAuthorizeSuccess = (payload: GoogleFitResponse) => ({ + type: GOOGLE_FIT_AUTHORIZE_SUCCESS, + payload +}) + +export const googleFitRevokeSuccess = () => ({ + type: GOOGLE_FIT_REVOKE_SUCCESS +}) + +export const googleFitUpdateToken = (payload: GoogleFitResponse) => ({ + type: GOOGLE_FIT_UPDATE_TOKEN, + payload +}) + +/* ASYNC ACTIONS */ + +export const toggleGoogleFit = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + try { + const enabled = getGoogleFitEnabled(getState()) + + if (enabled) { + await dispatch(revokeGoogleFitAccess()) + await dispatch(setMainSource(SOURCE.NO_SOURCE)) + } else { + await dispatch(revokePreviousSource()) + await dispatch(authorizeGoogleFit()) + } + } catch (err) { + console.warn(err) + } +} + +export const authorizeGoogleFit = () => async (dispatch: Function) => { + const config = + Platform.OS === 'android' + ? CONFIG.GOOOGLE_FIT_GONFIG_ANDROID + : CONFIG.GOOOGLE_FIT_GONFIG_IOS + + try { + const response = await authorize(config) + const { accessTokenExpirationDate, refreshToken, accessToken } = response + dispatch( + googleFitAuthorizeSuccess({ + enabled: true, + accessTokenExpirationDate, + refreshToken, + accessToken + }) + ) + + dispatch(setMainSource(SOURCE.GOOGLE_FIT)) + dispatch(readGoogleFitSleep()) + } catch (error) { + console.warn(error) + } +} + +export const refreshGoogleFitToken = () => async ( + dispatch: Function, + getState: GetState +) => { + const { refreshToken: oldToken } = getGoogleFitToken(getState()) + const config = + Platform.OS === 'android' + ? CONFIG.GOOOGLE_FIT_GONFIG_ANDROID + : CONFIG.GOOOGLE_FIT_GONFIG_IOS + + if (oldToken) { + try { + const response = await refresh(config, { + refreshToken: oldToken as string + }) + + const updateData = { + ...response, + refreshToken: + response.refreshToken && response.refreshToken.length > 0 + ? response.refreshToken + : oldToken + } + + dispatch(googleFitUpdateToken({ ...updateData, enabled: true })) + + return response.accessToken + } catch (error) { + // If the refresh token is not working, handle it by revoking the current user. + // The saved refresh token in the state tree will be guaranteed to always be the latest. + // The refresh token is not valid in 2 major cases: + // - The user revokes the Google Fit access + // - The refresh token has not been used for 6 months + await dispatch(revokeGoogleFitAccess()) + console.warn(error) + } + } + + return null +} + +export const revokeGoogleFitAccess = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { refreshToken: oldToken } = getGoogleFitToken(getState()) + const config = + Platform.OS === 'android' + ? CONFIG.GOOOGLE_FIT_GONFIG_ANDROID + : CONFIG.GOOOGLE_FIT_GONFIG_IOS + if (oldToken) { + try { + const response = await revoke(config, { + tokenToRevoke: oldToken + }) + dispatch(googleFitRevokeSuccess()) + } catch (error) { + console.warn(error) + } + } + dispatch(googleFitRevokeSuccess()) +} + +export const readGoogleFitSleep = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { accessToken, accessTokenExpirationDate } = getGoogleFitToken( + getState() + ) + const startDate = moment().subtract(1, 'week').toISOString() + const endDate = moment().toISOString() + + if (accessToken) { + try { + if (moment(accessTokenExpirationDate).isAfter(moment())) { + const googleApiCall = await fetch( + `https://www.googleapis.com/fitness/v1/users/me/sessions?startTime=${startDate}&endTime=${endDate}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await googleApiCall.json() + + const formatted = await formatGoogleFitData(response.session) + dispatch(createGoogleFitSources(formatted)) + dispatch(formatSleepData(formatted)) + } else { + const newAccessToken = await dispatch(refreshGoogleFitToken()) + + if (newAccessToken) { + const googleApiCall = await fetch( + `https://www.googleapis.com/fitness/v1/users/me/sessions?startTime=${startDate}&endTime=${endDate}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${newAccessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await googleApiCall.json() + const formatted = await formatGoogleFitData(response.session) + dispatch(createGoogleFitSources(formatted)) + dispatch(formatSleepData(formatted)) + } + } + } catch (error) { + console.warn('ERROR', error) + } + } +} + +export const writeGoogleFitSleep = (date?: string) => async ( + dispatch: Function, + getState: GetState +) => { + const { + apis: { googleFit } + } = getState() + if (googleFit) { + const { accessToken } = googleFit + try { + const formattedDate = moment(date).toISOString() + const googleApiCall = await fetch( + `https://www.googleapis.com/fitness/v1/users/me/sessions?startTime=${formattedDate}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + + const response = await googleApiCall.json() + } catch (error) { + console.warn(error) + } + } +} + +export const switchGoogleFitSource = (googleFitSource: SUB_SOURCE) => async ( + dispatch: Function +) => { + dispatch(changeGoogleFitSource(googleFitSource)) + dispatch(fetchSleepData()) +} + +export const createGoogleFitSources = (nights: Night[]) => async ( + dispatch: Function, + getState: Function +) => { + const googleFitSource = getGoogleFitSource(getState()) + const sourceList: SUB_SOURCE[] = [] + + nights.forEach((item: Night) => { + const existingSource = sourceList.find( + (source: SleepDataSource) => source.sourceId === item.sourceId + ) + + if (!existingSource) { + sourceList.push({ + sourceName: item.sourceName, + sourceId: item.sourceId + }) + } + }) + + dispatch(updateGoogleFitSources(sourceList)) + const noSleepTrackersInState = !googleFitSource + + if (noSleepTrackersInState) { + const tracker = sourceList[1] ? sourceList[1] : sourceList[0] + dispatch(changeGoogleFitSource(tracker)) + } +} diff --git a/src/actions/api-actions/oura-actions.ts b/src/actions/api-actions/oura-actions.ts new file mode 100644 index 0000000..a6b89b1 --- /dev/null +++ b/src/actions/api-actions/oura-actions.ts @@ -0,0 +1,180 @@ +import { OuraResponse, OuraAuthorizeResult } from 'Types/State/api-state' +import { GetState } from 'Types/GetState' +import { authorize, refresh, revoke } from 'react-native-app-auth' +import CONFIG from 'config/Config' +import { setMainSource } from '@actions/sleep-source-actions/sleep-source-actions' +import { SOURCE } from 'typings/state/sleep-source-state' +import { + getOuraToken, + getOuraEnabled +} from 'store/Selectors/api-selectors/api-selectors' +import moment from 'moment' +import { formatOuraSamples } from 'helpers/sleep/oura-helper' +import { formatSleepData } from '@actions/sleep/sleep-data-actions' +import { revokePreviousSource } from '@actions/sleep-source-actions/revoke-previous-source' + +export const OURA_AUTHORIZE_SUCCESS = 'OURA_AUTHORIZE_SUCCESS' +export const OURA_REVOKE_SUCCESS = 'OURA_REVOKE_SUCCESS' +export const OURA_UPDATE_TOKEN = 'OURA_UPDATE_TOKEN' + +export const FETCH_SLEEP_OURA_START = 'FETCH_SLEEP_OURA_START' +export const FETCH_SLEEP_OURA_SUCCESS = 'FETCH_SLEEP_OURA_SUCCESS' +export const FETCH_SLEEP_OURA_FAILURE = 'FETCH_SLEEP_OURA_FAILURE' + +/* ACTIONS */ + +export const ouraAuthorizeSuccess = (payload: OuraResponse) => ({ + type: OURA_AUTHORIZE_SUCCESS, + payload +}) + +export const ouraRevokeSuccess = () => ({ + type: OURA_REVOKE_SUCCESS +}) + +export const ouraUpdateToken = (payload: OuraResponse) => ({ + type: OURA_UPDATE_TOKEN, + payload +}) + +export const fetchSleepOuraStart = () => ({ + type: FETCH_SLEEP_OURA_START +}) + +export const fetchSleepOuraSuccess = () => ({ + type: FETCH_SLEEP_OURA_SUCCESS +}) + +export const fetchSleepOuraFailure = () => ({ + type: FETCH_SLEEP_OURA_FAILURE +}) + +/* ASYNC ACTIONS */ + +export const toggleOura = () => async ( + dispatch: Function, + getState: GetState +) => { + try { + const enabled = getOuraEnabled(getState()) + if (enabled) { + dispatch(revokeOuraAccess()) + } else { + await dispatch(revokePreviousSource()) + await dispatch(authorizeOura()) + } + } catch (err) { + console.warn(err) + } +} + +export const authorizeOura = () => async (dispatch: Function) => { + try { + const response = (await authorize( + CONFIG.OURA_CONFIG + )) as OuraAuthorizeResult + + const { accessTokenExpirationDate, refreshToken, accessToken } = response + + dispatch( + ouraAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken, + accessToken, + enabled: true + }) + ) + + dispatch(setMainSource(SOURCE.OURA)) + } catch (error) { + console.log('authorizeOura', error) + } +} + +export const refreshOuraToken = () => async ( + dispatch: Function, + getState: GetState +) => { + const { refreshToken: oldToken } = getOuraToken(getState()) + + if (oldToken) { + try { + const response = (await refresh(CONFIG.OURA_CONFIG, { + refreshToken: oldToken + })) as OuraAuthorizeResult + + const { accessTokenExpirationDate, refreshToken, accessToken } = response + dispatch( + ouraAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken: + refreshToken && refreshToken.length > 0 ? refreshToken : oldToken, + accessToken, + enabled: true + }) + ) + } catch (error) { + console.log(error) + } + } +} + +export const revokeOuraAccess = () => async ( + dispatch: Function, + getState: GetState +) => { + dispatch(ouraRevokeSuccess()) + dispatch(setMainSource(SOURCE.NO_SOURCE)) +} + +export const getOuraSleep = () => async ( + dispatch: Function, + getState: GetState +) => { + const { accessToken, accessTokenExpirationDate } = getOuraToken(getState()) + dispatch(fetchSleepOuraStart()) + + const startDate = moment().subtract(1, 'week').format('YYYY-MM-DD') + const endDate = moment().format('YYYY-MM-DD') + + if (accessToken) { + try { + if (moment(accessTokenExpirationDate).isAfter(moment())) { + const ouraAPICall = await fetch( + `https://api.ouraring.com/v1/sleep?start=${startDate}&end=${endDate}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await ouraAPICall.json() + + const formattedResponse = formatOuraSamples(response.sleep) + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepOuraSuccess()) + } else { + await dispatch(refreshOuraToken()) + const ouraAPICall = await fetch( + `https://api.ouraring.com/v1/sleep?start=${startDate}&end=${endDate}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ) + const response = await ouraAPICall.json() + const formattedResponse = formatOuraSamples(response.sleep) + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepOuraSuccess()) + } + } catch (error) { + console.warn('error', error) + dispatch(fetchSleepOuraFailure()) + } + } +} diff --git a/src/actions/api-actions/suunto-actions.ts b/src/actions/api-actions/suunto-actions.ts new file mode 100644 index 0000000..eb2ed7f --- /dev/null +++ b/src/actions/api-actions/suunto-actions.ts @@ -0,0 +1,54 @@ +import { authorize, refresh, revoke } from 'react-native-app-auth' +import { GetState } from '../../Types/GetState' + +const config = {} + +export const authorizeSuunto = () => async (dispatch: Function) => { + try { + const response = await authorize(config) + const { accessTokenExpirationDate, refreshToken, accessToken } = response + // dispatch( + // googleFitAuthorizeSuccess({ + // accessTokenExpirationDate, + // refreshToken, + // accessToken, + // }) + // ); + console.log(response) + } catch (error) { + console.log(error) + } +} + +export const refreshSuuntoToken = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + apis: { googleFit } + } = getState() + if (googleFit) { + const { refreshToken: oldToken } = googleFit + try { + const response = await refresh(config, { refreshToken: oldToken }) + console.log('refreshGoogleFitToken', response) + } catch (error) {} + } +} + +export const revokeSuuntoAccess = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + apis: { googleFit } + } = getState() + + if (googleFit) { + const { refreshToken: oldToken } = googleFit + const response = await revoke(config, { + tokenToRevoke: oldToken + }) + console.log(response) + } +} diff --git a/src/actions/api-actions/withings-actions.ts b/src/actions/api-actions/withings-actions.ts new file mode 100644 index 0000000..8bd4c92 --- /dev/null +++ b/src/actions/api-actions/withings-actions.ts @@ -0,0 +1,222 @@ +import { captureException } from '@sentry/react-native' +import { revokePreviousSource } from '@actions/sleep-source-actions/revoke-previous-source' +import { setMainSource } from '@actions/sleep-source-actions/sleep-source-actions' +import { formatSleepData } from '@actions/sleep/sleep-data-actions' +import CONFIG from 'config/Config' +import { formatWithingsSamples } from 'helpers/sleep/withings-helper' +import moment from 'moment' +import { + authorize, + AuthorizeResult, + refresh, + RefreshResult +} from 'react-native-app-auth' +import ReduxAction, { Dispatch, Thunk } from 'Types/ReduxActions' +import { SOURCE } from 'typings/state/sleep-source-state' +import { + getWithingsEnabled, + getWithingsToken +} from '../../store/Selectors/api-selectors/api-selectors' +import { GetState } from '../../Types/GetState' + +export const WITHINGS_AUTHORIZE_SUCCESS = 'WITHINGS_AUTHORIZE_SUCCESS' +export const WITHINGS_REVOKE_SUCCESS = 'WITHINGS_REVOKE_SUCCESS' +export const WITHINGS_UPDATE_TOKEN = 'WITHINGS_UPDATE_TOKEN' + +export const FETCH_SLEEP_WITHINGS_START = 'FETCH_SLEEP_WITHINGS_START' +export const FETCH_SLEEP_WITHINGS_SUCCESS = 'FETCH_SLEEP_WITHINGS_SUCCESS' +export const FETCH_SLEEP_WITHINGS_FAILURE = 'FETCH_SLEEP_WITHINGS_FAILURE' + +export const withingsAuthorizeSuccess = ( + payload: WithingAuthResponse +): ReduxAction => ({ + type: WITHINGS_AUTHORIZE_SUCCESS, + payload +}) + +export const withingsRevokeSuccess = (): ReduxAction => ({ + type: WITHINGS_REVOKE_SUCCESS +}) + +export const withingsUpdateToken = ( + payload: WithingAuthResponse +): ReduxAction => ({ + type: WITHINGS_UPDATE_TOKEN, + payload +}) + +export const fetchSleepWithingsStart = (): ReduxAction => ({ + type: FETCH_SLEEP_WITHINGS_START +}) + +export const fetchSleepWithingsSuccess = (): ReduxAction => ({ + type: FETCH_SLEEP_WITHINGS_SUCCESS +}) + +export const fetchSleepWithingsFailure = (): ReduxAction => ({ + type: FETCH_SLEEP_WITHINGS_FAILURE +}) + +export const toggleWithings = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const enabled = getWithingsEnabled(getState()) + if (enabled) { + dispatch(revokeWithingsAccess()) + } else { + await dispatch(revokePreviousSource()) + + await dispatch(authorizeWithings()) + } +} + +export const authorizeWithings = (): Thunk => async (dispatch: Dispatch) => { + try { + const response = (await authorize( + CONFIG.WITHINGS_CONFIG + )) as WithingsAuthorizeResult + + const { + accessTokenExpirationDate, + refreshToken, + accessToken, + tokenAdditionalParameters: { userid: user_id } + } = response + + dispatch( + withingsAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken, + accessToken, + user_id, + enabled: true + }) + ) + dispatch(setMainSource(SOURCE.WITHINGS)) + } catch (error) { + captureException(error) + } +} + +export const refreshWithingsToken = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { refreshToken: oldToken } = getWithingsToken(getState()) + + if (oldToken) { + try { + const response = (await refresh(CONFIG.WITHINGS_CONFIG, { + refreshToken: oldToken + })) as WithingsRefreshResult + + const { + accessTokenExpirationDate, + refreshToken, + accessToken, + additionalParameters: { userid: user_id } + } = response + + dispatch( + withingsAuthorizeSuccess({ + accessTokenExpirationDate, + refreshToken: + refreshToken && refreshToken.length > 0 ? refreshToken : oldToken, + accessToken, + user_id, + enabled: true + }) + ) + } catch (error) { + captureException(error) + } + } +} + +export const revokeWithingsAccess = (): Thunk => async (dispatch: Dispatch) => { + dispatch(withingsRevokeSuccess()) + dispatch(setMainSource(SOURCE.NO_SOURCE)) +} + +export const getWithingsSleep = ( + startDate?: string, + endDate?: string +): Thunk => async (dispatch: Dispatch, getState: GetState) => { + const { accessToken, accessTokenExpirationDate } = getWithingsToken( + getState() + ) + dispatch(fetchSleepWithingsStart()) + + const start = startDate || moment().subtract(1, 'week').format('YYYY-MM-DD') + const end = endDate || moment().format('YYYY-MM-DD') + + const dataFields = + 'deepsleepduration,durationtosleep,durationtowakeup,sleep_score,snoring, snoringepisodecount' + + if (accessToken) { + try { + if (moment(accessTokenExpirationDate).isAfter(moment())) { + const withingsApiCall = await fetch( + `https://wbsapi.withings.net/v2/sleep?action=getsummary&startdateymd=${start}&enddateymd=${end}&data_fields=${dataFields}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ) + + const response = await withingsApiCall.json() + const formattedResponse = formatWithingsSamples(response.body.series) + console.log(response) + + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepWithingsSuccess()) + } else { + await dispatch(refreshWithingsToken()) + + const withingsApiCall = await fetch( + `https://wbsapi.withings.net/v2/sleep?action=getsummary&startdateymd=${start}&enddateymd=${end}&data_fields=${dataFields}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ) + + const response = await withingsApiCall.json() + const formattedResponse = formatWithingsSamples(response.body.series) + console.log(response) + dispatch(formatSleepData(formattedResponse)) + dispatch(fetchSleepWithingsSuccess()) + } + } catch (error) { + dispatch(fetchSleepWithingsFailure()) + captureException(error) + } + } +} + +interface WithingsRefreshResult extends RefreshResult { + refreshToken: string + additionalParameters: { + userid: string + } +} + +interface WithingsAuthorizeResult extends AuthorizeResult { + refreshToken: string + tokenAdditionalParameters: { + userid: string + } +} + +export type WithingAuthResponse = { + accessTokenExpirationDate: string + refreshToken: string + accessToken: string + user_id: string + enabled: boolean +} diff --git a/src/actions/auth/auth-actions.ts b/src/actions/auth/auth-actions.ts new file mode 100644 index 0000000..e5ca527 --- /dev/null +++ b/src/actions/auth/auth-actions.ts @@ -0,0 +1,279 @@ +import Auth from '@aws-amplify/auth' +import { + handleHabitsFromCloudWhenLoggingIn, + handleHabitsWhenloggingOut, + toggleMergingDialog +} from '@actions/habit/habit-actions' +import translate from 'config/i18n' +import { areThereChangesInLocal } from 'helpers/habits' +import Intercom from 'react-native-intercom' +import Purchases from 'react-native-purchases' +import { GetState } from 'Types/GetState' +import * as NavigationService from '../../config/NavigationHelper' +import ROUTE from '../../config/routes/Routes' +import { actionCreators as notificationActions } from '../../store/Reducers/NotificationReducer' +import { NotificationType } from '../../Types/NotificationState' +import { updateEmail } from '../user/user-actions' + +/* ACTION TYPES */ + +export const REGISTER_START = 'REGISTER_START' +export const REGISTER_SUCCESS = 'REGISTER_SUCCESS' +export const REGISTER_FAILURE = 'REGISTER_FAILURE' + +export const LOGIN_START = 'LOGIN_START' +export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' +export const LOGIN_FAILURE = 'LOGIN_FAILURE' + +export const LOGOUT_START = 'LOGOUT_START' +export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS' +export const LOGOUT_FAILURE = 'LOGOUT_FAILURE' + +/* ACTIONS */ + +export const registerStart = () => ({ + type: REGISTER_START +}) + +export const registerSuccess = (email: string) => ({ + type: REGISTER_SUCCESS, + payload: { email } +}) + +export const registerFailure = () => ({ + type: REGISTER_FAILURE +}) + +export const loginStart = () => ({ + type: LOGIN_START +}) + +export const loginSuccess = ( + authenticated: boolean, + email: string, + username: string +) => ({ + type: LOGIN_SUCCESS, + payload: { authenticated, email, username } +}) + +export const loginFailure = () => ({ + type: LOGIN_FAILURE +}) + +export const logoutStart = () => ({ + type: LOGOUT_START +}) + +export const logoutSuccess = () => ({ + type: LOGOUT_SUCCESS +}) + +export const logoutFailure = () => ({ + type: LOGOUT_FAILURE +}) + +/* ASYNC ACTIONS */ + +export const register = (email: string, password: string) => async ( + dispatch: any +) => { + dispatch(registerStart()) + try { + const response = await Auth.signUp({ + username: email, + password, + attributes: { email } + }) + + await dispatch(registerSuccess(email)) + await dispatch(login(email, password)) + } catch (error) { + let { message } = error + if (error.code === 'UsernameExistsException') { + message = translate('AUTH_ERROR.EMAIL_EXISTS_EXCEPTION') + } + + await dispatch( + notificationActions.newNotification({ + message, + type: NotificationType.ERROR + }) + ) + + await dispatch(registerFailure()) + } +} + +export const resendEmail = (username: string) => async (dispatch: Function) => { + try { + await Auth.resendSignUp(username) + await dispatch( + notificationActions.newNotification({ + message: 'Confirmation email sent', + type: NotificationType.INFO + }) + ) + } catch (error) { + const message = error.message ? error.message : error + + dispatch( + notificationActions.newNotification({ + message, + type: NotificationType.ERROR + }) + ) + } +} + +export const confirmSignup = (email: string, authCode: string) => async ( + dispatch: Function +) => { + const username = email + try { + const response = await Auth.confirmSignUp(username, authCode) + } catch (error) {} +} + +export const login = (loginEmail: string, loginPassword: string) => async ( + dispatch: Function, + getState: GetState +) => { + dispatch(loginStart()) + + try { + const { + attributes: { email }, + username + } = await Auth.signIn(loginEmail, loginPassword) + + const { + habitState: { habits, subHabits } + } = getState() + + await Intercom.updateUser({ email, user_id: username }) + await Purchases.identify(username) + await Purchases.setEmail(email) + await NavigationService.navigate(ROUTE.SLEEP, {}) + + if (areThereChangesInLocal(habits, subHabits)) { + await dispatch(toggleMergingDialog(true)) + } else { + await dispatch(handleHabitsFromCloudWhenLoggingIn(username, false)) + await NavigationService.navigate(ROUTE.SLEEP, {}) + } + + await dispatch(loginSuccess(true, email, username)) + } catch (error) { + console.warn(error) + const { code } = error + let message = 'Unknown error' + + switch (code) { + case 'UserNotConfirmedException': + await dispatch(updateEmail(loginEmail)) + NavigationService.navigate(ROUTE.CONFIRM, {}) + break + case 'UserNotFoundException': + message = translate('AUTH_ERROR.USER_NOT_EXIST_EXCEPTION') + break + case 'NotAuthorizedException': + message = translate('AUTH_ERROR.INVALID_PASSWORD_EXCEPTION') + break + default: + break + } + + await dispatch( + notificationActions.newNotification({ + message, + type: NotificationType.ERROR + }) + ) + await dispatch(loginFailure()) + } +} + +export const logout = () => async (dispatch: Function) => { + dispatch(logoutStart()) + try { + await dispatch(handleHabitsWhenloggingOut()) + await Auth.signOut() + await Purchases.reset() + await dispatch(logoutSuccess()) + } catch (error) { + await dispatch( + notificationActions.newNotification({ + message: JSON.stringify(error), + type: NotificationType.ERROR + }) + ) + } +} + +export const requestNewPassword = (username: string) => async ( + dispatch: Function +) => { + try { + const forgotRes = await Auth.forgotPassword(username) + } catch (error) { + await dispatch( + notificationActions.newNotification({ + message: JSON.stringify(error), + type: NotificationType.ERROR + }) + ) + } +} + +export const submitNewPassword = ( + username: string, + confirmationCode: string, + password: string +) => async (dispatch: Function) => { + try { + const res = await Auth.forgotPasswordSubmit( + username, + confirmationCode, + password + ) + } catch (error) { + await dispatch( + notificationActions.newNotification({ + message: JSON.stringify(error), + type: NotificationType.ERROR + }) + ) + } +} + +export const updatePassword = ( + oldPassword: string, + newPassword: string +) => async (dispatch: Function) => { + await Auth.currentAuthenticatedUser() + .then((user) => Auth.changePassword(user, oldPassword, newPassword)) + .then((data) => {}) + .catch((err) => {}) +} + +export const updateUserAttributes = async (attributes: {}) => { + await Auth.currentAuthenticatedUser() + .then((user) => Auth.updateUserAttributes(user, attributes)) + .then((data) => {}) + .catch((err) => {}) +} + +export const refreshAuthStatus = () => async (dispatch: Function) => { + dispatch(loginStart()) + try { + const user = await Auth.currentUserInfo() + if (user) { + dispatch(loginSuccess(true, user.attributes.email, user.username)) + } else { + dispatch(loginSuccess(false, '', '')) + } + } catch (error) { + dispatch(loginFailure()) + } +} diff --git a/src/actions/calendar-actions/calendar-actions.ts b/src/actions/calendar-actions/calendar-actions.ts new file mode 100644 index 0000000..d6af0ef --- /dev/null +++ b/src/actions/calendar-actions/calendar-actions.ts @@ -0,0 +1,50 @@ +import moment, { Moment } from 'moment' +import { Day } from 'Types/Sleepdata' + +/* ACTION TYPES */ + +export const CREATE_DAYS_START = 'CREATE_DAYS_START' +export const CREATE_DAYS_SUCCESS = 'CREATE_DAYS_SUCCESS' +export const CREATE_DAYS_FAILURE = 'CREATE_DAYS_FAILURE' + +/* ACTIONS */ + +export const createDaysStart = () => ({ + type: CREATE_DAYS_START +}) + +export const createDaysSuccess = (days: Day[]) => ({ + type: CREATE_DAYS_SUCCESS +}) + +/* ASYNC ACTIONS */ + +export const createCalendar = () => async (dispatch: Function) => { + dispatch(createDaysStart) + + const calendarDays = 7 + const startDate = moment().startOf('day').subtract(calendarDays, 'days') + const calendar: Day[] = [] + + for (let i = 0; i < calendarDays; i++) { + const date = startDate.add(1, 'day') + calendar.push({ date: date.toISOString(), night: [] }) + } + + dispatch(createDaysSuccess(calendar)) +} + +const createNewDaysForCalendar = (lastDate: Moment, today: Moment) => async ( + dispatch: Function +) => { + dispatch(createDaysStart) + + const dayArray: Day[] = [] + + while (today.isAfter(lastDate)) { + lastDate.add(1, 'day') + dayArray.push({ date: lastDate.toISOString(), night: [] }) + } + + await dispatch(createDaysSuccess(dayArray)) +} diff --git a/src/actions/challenges/challenge-actions.ts b/src/actions/challenges/challenge-actions.ts new file mode 100644 index 0000000..3c700b3 --- /dev/null +++ b/src/actions/challenges/challenge-actions.ts @@ -0,0 +1,14 @@ +import { Challenge } from '../../Types/ChallengeState' + +export const MAKE_CHALLENGE_VISIBLE = 'MAKE_CHALLENGE_VISIBLE' +export const ADD_CHALLENGES = 'ADD_CHALLENGES' + +export const makeChallengeVisible = (challenge: Challenge) => ({ + type: MAKE_CHALLENGE_VISIBLE, + payload: challenge +}) + +export const addChallenges = (challenges: Challenge[]) => ({ + type: ADD_CHALLENGES, + payload: challenges +}) diff --git a/src/actions/coaching/coaching-actions.ts b/src/actions/coaching/coaching-actions.ts new file mode 100644 index 0000000..8b2b3b3 --- /dev/null +++ b/src/actions/coaching/coaching-actions.ts @@ -0,0 +1,267 @@ +import API, { graphqlOperation } from '@aws-amplify/api' +import { + ListCoachingDatasQuery, + ModelCoachingDataFilterInput, + UpdateCoachingDataInput +} from 'API' +import { Auth } from 'aws-amplify' +import { createCoachingData, updateCoachingData } from 'graphql/mutations' +import { listCoachingDatas } from 'graphql/queries' +import moment from 'moment' +import { getAuthState } from 'store/Selectors/auth-selectors/auth-selectors' +import { + getActiveWeekWithContent, + getCoachingMonth, + getCurrentWeekAll, + WEEK_STAGE +} from 'store/Selectors/coaching-selectors' +import { GetState } from 'Types/GetState' +import { CoachingMonth, STAGE, StateWeek } from 'typings/state/coaching-state' +import { v4 } from 'uuid' +import ReduxAction from 'Types/ReduxActions' + +/* ACTION TYPES */ + +export const RESET_COACHING = 'RESET_COACHING' + +export const SELECT_WEEK = 'SELECT_WEEK' +export const SELECT_LESSON = 'SELECT_LESSON' + +export const START_COACHING = 'START_COACHING' +export const END_COACHING = 'END_COACHING' +export const RESUME_COACHING = 'RESUME_COACHING' + +export const START_WEEK = 'START_WEEK' +export const START_LESSON = 'START_LESSON' + +export const COMPLETE_LESSON = 'COMPLETE_LESSON' +export const COMPLETE_WEEK = 'COMPLETE_WEEK' +export const COMPLETE_COACHING = 'COMPLETE_COACHING' + +export const PREPARE_WEEK_FOR_COMPLETION = 'PREPARE_WEEK_FOR_COMPLETION' + +export const SET_ACTIVE_WEEK = 'SET_ACTIVE_WEEK' +export const SET_ACTIVE_MONTH = 'SET_ACTIVE_MONTH' + +export const SET_STAGE = 'SET_STAGE' + +export const PULL_COACHING_START = 'PULL_COACHING_START' +export const PULL_COACHING_SUCCESS = 'PULL_COACHING_SUCCESS' +export const PULL_COACHING_FAILURE = 'PULL_COACHING_FAILURE' + +export const CREATE_COACHING_START = 'CREATE_COACHING_START' +export const CREATE_COACHING_SUCCESS = 'CREATE_COACHING_SUCCESS' +export const CREATE_COACHING_FAILURE = 'CREATE_COACHING_FAILURE' + +export const UPDATE_COACHING_START = 'UPDATE_COACHING_START' +export const UPDATE_COACHING_SUCCESS = 'UPDATE_COACHING_SUCCESS' +export const UPDATE_COACHING_FAILURE = 'UPDATE_COACHING_FAILURE' + +/* ACTIONS */ + +export const resetCoaching = (): ReduxAction => ({ + type: RESET_COACHING +}) + +export const selectWeek = (slug: string): ReduxAction => ({ + payload: slug, + type: SELECT_WEEK +}) + +export const selectLesson = (slug: string): ReduxAction => ({ + payload: slug, + type: SELECT_LESSON +}) + +export const completeLesson = (slug: string): ReduxAction => ({ + type: COMPLETE_LESSON, + payload: slug +}) + +export const completeWeek = ( + completedWeekSlug: string, + nextWeekSlug?: string +): ReduxAction => ({ + type: COMPLETE_WEEK, + payload: { completedWeekSlug, nextWeekSlug } +}) + +export const prepareWeekForCompletion = (slug: string) => ({ + type: PREPARE_WEEK_FOR_COMPLETION, + payload: slug +}) + +export const completeCoaching = (id: string) => ({ + type: COMPLETE_COACHING, + payload: id +}) + +export const setStage = (stage: STAGE) => ({ + payload: stage, + type: SET_STAGE +}) + +export const startCoaching = ( + coachingMonth: CoachingMonth, + activeWeek: StateWeek +) => ({ + type: START_COACHING, + payload: { activeMonth: coachingMonth, activeWeek } +}) + +export const startCoachingWeek = (weekSlug: string) => ({ + payload: weekSlug, + type: START_WEEK +}) + +/* ASYNC ACTIONS */ + +export const startCoachingMonth = (startingWeek: StateWeek) => ( + dispatch: Function +) => { + const coachingMonth: CoachingMonth = { + stage: STAGE.ONGOING, + id: v4(), + weeks: [{ ...startingWeek, stage: WEEK_STAGE.ONGOING }], + lessons: [], + started: moment().toISOString() + } + + dispatch( + startCoaching(coachingMonth, { ...startingWeek, stage: WEEK_STAGE.ONGOING }) + ) +} + +export const completeLessonAndUpdateProgress = () => ( + dispatch: Function, + getState: GetState +) => { + const content = getCurrentWeekAll(getState()) +} + +export const validateWeeklyProgress = () => async ( + dispatch: Function, + getState: GetState +) => { + const week = getActiveWeekWithContent(getState()) + const state = getCoachingMonth(getState()) + dispatch(openNextWeek()) +} + +export const openNextWeek = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + coachingContent: { weeks } + } = getState() + const week = getActiveWeekWithContent(getState()) + const incremented: number = week?.order ? week?.order + 1 : 0 + const nextWeek = weeks.find((week) => week.order === incremented) + + if (nextWeek) { + // await dispatch(); + // await dispatch(openWeekLock(nextWeek.contentId)); + // await dispatch(actions.setOngoingWeek(nextWeek.contentId)); + } +} + +export const updateCoaching = (coachingMonths: CoachingMonth[]) => async ( + dispatch: Function, + getState: GetState +) => { + const { username } = await Auth.currentUserInfo() + const updatePromises: Promise[] = [] + try { + coachingMonths.forEach((month) => { + const input: UpdateCoachingDataInput = { + id: month.id, + userId: username, + lessons: month.lessons, + weeks: month.weeks.map((week) => ({ + slug: week.slug, + started: week.started, + ended: week.ended + })), + started: month.started + } + + updatePromises.push( + API.graphql(graphqlOperation(updateCoachingData, { input })) as any + ) + }) + await Promise.all(updatePromises) + } catch (error) { + console.warn(error) + } +} + +export const createCoaching = (coachingMonths: CoachingMonth[]) => async ( + dispatch: Function, + getState: GetState +) => { + const updatePromises: Promise[] = [] + const { username } = await Auth.currentUserInfo() + + try { + coachingMonths.forEach((month) => { + const input: UpdateCoachingDataInput = { + id: month.id, + userId: username, + lessons: month.lessons, + weeks: month.weeks.map((week) => ({ + slug: week.slug, + started: week.started, + ended: week.ended + })), + started: month.started + } + + updatePromises.push( + API.graphql(graphqlOperation(createCoachingData, { input })) as any + ) + }) + await Promise.all(updatePromises) + } catch (error) { + console.warn(error) + } +} + +export const updateCoachingInCloud = () => async ( + dispatch: Function, + getState: GetState +) => { + const { + coachingState: { coachingMonths } + } = getState() + const isLoggedIn = getAuthState(getState()) + + if (isLoggedIn) { + const { username } = await Auth.currentUserInfo() + const monthsToCreate: CoachingMonth[] = [] + const monthsToUpdate: CoachingMonth[] = [] + const filter: ModelCoachingDataFilterInput = { + userId: { eq: username } + } + + try { + const response = (await API.graphql( + graphqlOperation(listCoachingDatas, { filter }) + )) as { + data: ListCoachingDatasQuery + } + coachingMonths.forEach((month) => { + const exists = response.data.listCoachingDatas?.items?.find( + (m) => m?.id === month.id + ) + exists ? monthsToUpdate.push(month) : monthsToCreate.push(month) + }) + + dispatch(createCoaching(monthsToCreate)) + dispatch(updateCoaching(monthsToUpdate)) + } catch (error) { + console.warn(error) + // dispatch(pullFailure()); + } + } +} diff --git a/src/actions/coaching/coaching-to-cloud-actions.ts b/src/actions/coaching/coaching-to-cloud-actions.ts new file mode 100644 index 0000000..f777095 --- /dev/null +++ b/src/actions/coaching/coaching-to-cloud-actions.ts @@ -0,0 +1,4 @@ +import { Auth } from 'aws-amplify' +import { v4 } from 'uuid' +import { GetState } from '../../Types/GetState' +import { ListCoachingDatasQuery, CreateCoachingDataInput } from '../../API' diff --git a/src/actions/coaching/content-actions.ts b/src/actions/coaching/content-actions.ts new file mode 100644 index 0000000..2dcfa36 --- /dev/null +++ b/src/actions/coaching/content-actions.ts @@ -0,0 +1,193 @@ +import { documentToPlainTextString } from '@contentful/rich-text-plain-text-renderer' +import { ContentfulClientApi, Entry } from 'contentful' +import I18n from 'i18n-js' +import CONFIG from '../../config/Config' +import { actionCreators as contentActions } from '../../store/Reducers/content-reducer/content-reducer' +import { + AuthorCard, + ContentLesson, + ContentWeek, + ExampleHabit, + Section +} from '../../Types/CoachingContentState' +import { + ICoachingWeekFields, + ILessonFields +} from '../../Types/generated/contentful' +import { sendError } from '../NotificationActions' + +const { createClient } = require('contentful/dist/contentful.browser.min.js') + +const client: ContentfulClientApi = createClient({ + space: CONFIG.CONTENTFUL_SPACE, + accessToken: CONFIG.CONTENTFUL_SPACE_ACCESS_TOKEN +}) + +const getFieldValue = ( + entry: Entry, + fieldToGet: string, + object: any, + callback?: Function, + fieldToSet?: string +) => { + if (entry.fields[fieldToGet]) { + object[fieldToSet || fieldToGet] = callback + ? callback(entry.fields[fieldToGet]) + : entry.fields[fieldToGet] + } +} + +export const getAllWeeks = () => async (dispatch: Function) => { + const locale = I18n.locale === 'en' ? 'en-US' : 'fi-FI' + const weeks: ContentWeek[] = [] + const lessons: any = [] + const sections: Section[] = [] + const exampleHabits: ExampleHabit[] = [] + + await dispatch(contentActions.updateContentStart()) + try { + const coachingWeeks: any = await client.getEntries({ + locale, + content_type: 'coachingWeek', + include: 3 + }) + + coachingWeeks.items.forEach((coachingWeek: Entry) => { + const weekObject: ContentWeek = {} + + weekObject.contentId = coachingWeek.sys.id + + getFieldValue(coachingWeek, 'weekName', weekObject) + getFieldValue(coachingWeek, 'duration', weekObject) + getFieldValue( + coachingWeek, + 'locked', + weekObject, + undefined, + 'defaultLocked' + ) + if (coachingWeek.fields.coverPhoto) { + weekObject.coverPhoto = coachingWeek.fields.coverPhoto.fields.file.url + } + + if (coachingWeek.fields.slug) { + weekObject.slug = coachingWeek.fields.slug + } + getFieldValue(coachingWeek, 'order', weekObject) + getFieldValue(coachingWeek, 'intro', weekObject) + getFieldValue( + coachingWeek, + 'weekDescription', + weekObject, + documentToPlainTextString + ) + + if (coachingWeek.fields.lessons) { + const weekLessons: any = [] + + coachingWeek.fields.lessons.forEach((lesson: Entry) => { + const lessonObject: ContentLesson = { contentId: lesson.sys.id } + + if (lesson.fields.slug) { + lessonObject.slug = lesson.fields.slug + } + getFieldValue(lesson, 'lessonName', lessonObject) + getFieldValue(lesson, 'additionalInformation', lessonObject) + + getFieldValue(lesson, 'author', lessonObject) + getFieldValue(lesson, 'lessonName', lessonObject) + getFieldValue(lesson, 'stage', lessonObject) + getFieldValue(lesson, 'lessonContent', lessonObject) + + lessonObject.authorCards = mapAuthors(lesson) + + if (lesson.fields.cover) { + lessonObject.cover = lesson.fields.cover.fields.file.url + } + + if (lesson.fields.keywords) { + lessonObject.tags = lesson.fields.keywords + } + + if (lesson.fields.section) { + const section: Section = { + title: lesson.fields.section.fields.title, + order: lesson.fields.section.fields.order, + description: lesson.fields!.section!.fields!.description! + } + lessonObject.section = section + sections.push(section) + } + + if (lesson.fields.habit) { + const habits: ExampleHabit[] = [] + + lesson.fields.habit.forEach((habit) => { + const exampleHabit: ExampleHabit = { + title: habit.fields.title, + period: habit.fields.period, + description: habit.fields.description + } + habits.push(exampleHabit) + exampleHabits.push(exampleHabit) + }) + + lessonObject.exampleHabit = habits + } + + lessonObject.weekId = weekObject.contentId + + weekLessons.push(lesson.fields.slug) + lessons.push(lessonObject) + }) + + weekObject.lessons = weekLessons + } + + if (coachingWeek.fields.taskCount) { + weekObject.taskCount = coachingWeek.fields.taskCount + } + + weeks.push(weekObject) + }) + + const sorted = weeks.sort((a, b) => a.order - b.order) + + await dispatch( + contentActions.updateContentSuccess({ + weeks: sorted, + lessons, + sections, + habits: exampleHabits + }) + ) + } catch (error) { + await Promise.all([ + dispatch(contentActions.updateContentError()), + dispatch(sendError(error)) + ]) + } +} + +const mapAuthors = (lesson: Entry): AuthorCard[] => { + const authorArray: AuthorCard[] = [] + + if (lesson.fields.authorCard) { + lesson.fields.authorCard.forEach((card: Entry) => { + const author: any = {} + + if (card.fields.name) { + author.name = card.fields.name + } + + getFieldValue(card, 'name', author) + getFieldValue(card, 'credentials', author) + + if (card.fields.avatar.fields.file.url) { + author.avatar = card.fields.avatar.fields.file.url + } + authorArray.push(author) + }) + } + return authorArray +} diff --git a/src/actions/habit/habit-actions.spec.ts b/src/actions/habit/habit-actions.spec.ts new file mode 100644 index 0000000..0a552d4 --- /dev/null +++ b/src/actions/habit/habit-actions.spec.ts @@ -0,0 +1,281 @@ +import { enableMapSet } from 'immer' +import configureStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { + Habit, + HabitState, + MutationType, + UnsyncedHabit +} from 'Types/State/habit-state' +import { Period } from 'Types/State/Periods' +import { AuthState } from 'Types/State/AuthState' +import { + addHabit, + addUnsyncedHabit, + archiveHabit, + deleteHabitById, + markTodayHabitAsCompleted, + updateEditedHabit, + updateHabitDayStreak, + editHabit, + removeHabit +} from './habit-actions' + +enableMapSet() + +jest.mock('aws-amplify', () => ({ + API: jest.fn() +})) + +jest.mock('react-native', () => ({ + StyleSheet: { + hairlineWidth: 10 + } +})) + +jest.mock('moment', () => () => ({ + startOf: () => ({ + toISOString: () => '2020-05-07T21:00:00.000Z' + }), + toISOString: () => '2020-05-07T21:00:00.000Z' +})) + +const middlewares = [thunk] + +const startOfTodayString = '2020-05-07T21:00:00.000Z' +const mockStore = configureStore(middlewares) +const localHabit1: Habit = { + id: 'local-habit-1', + title: 'Local Habit 1', + description: 'Local Habit 1', + date: startOfTodayString, + days: new Map(), + period: Period.morning, + userId: null +} + +interface State { + user: { + username: string + loggedIn: boolean + } + habitState: HabitState + auth: AuthState +} + +const state: State = { + user: { + username: 'User1', + loggedIn: true + }, + habitState: { + habits: new Map(), + subHabits: new Map(), + unsyncedHabits: [], + draftEditHabit: localHabit1, + mergingDialogDisplayed: false + }, + auth: { + authenticated: true, + loading: false + } +} + +const store = mockStore(state) + +describe('Synchronous habit actions should work', () => { + afterEach(() => { + store.clearActions() + }) + + const habit: Habit = { + id: 'expected-habit-1', + title: 'Expected Habit 1', + description: 'Expected Habit 1', + date: startOfTodayString, + days: new Map(), + period: Period.morning, + userId: 'User1' + } + + it('addHabit should work', async () => { + const expectedHabit: Habit = { + ...localHabit1, + title: 'Expected Habit 1', + description: 'Expected Habit Description 1', + period: Period.morning + } + + await store.dispatch( + addHabit( + expectedHabit.title, + expectedHabit.description, + expectedHabit.period, + expectedHabit.id + ) + ) + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.CREATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('editHabit should work', async () => { + const expectedHabit = { + ...localHabit1, + title: 'Modified Expected Habit 1', + description: 'Modified Expected Habit Description', + period: Period.afternoon + } + + await store.dispatch( + editHabit( + expectedHabit.title, + expectedHabit.description, + expectedHabit.period, + expectedHabit + ) + ) + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.UPDATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('updateHabitDayStreak should work', async () => { + const expectedHabit = { ...localHabit1 } + + await store.dispatch(updateHabitDayStreak(expectedHabit, 1)) + + expectedHabit.dayStreak = 1 + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.UPDATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('markTodayHabitAsCompleted should work when habit is blank', async () => { + await store.dispatch(markTodayHabitAsCompleted(localHabit1)) + + const expectedHabit: Habit = { + ...localHabit1, + days: new Map().set(startOfTodayString, 1), + latestCompletedDate: startOfTodayString, + dayStreak: 1, + longestDayStreak: 1 + } + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.UPDATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('markTodayHabitAsCompleted should work when habit has defined completion records', async () => { + const habit: Habit = { + ...localHabit1, + days: new Map().set(startOfTodayString, 1), + latestCompletedDate: startOfTodayString, + dayStreak: 1, + longestDayStreak: 1 + } + + await store.dispatch(markTodayHabitAsCompleted(habit)) + + const expectedHabit: Habit = { + ...localHabit1, + days: new Map().set(startOfTodayString, 0), + latestCompletedDate: startOfTodayString, + dayStreak: 0, + longestDayStreak: 1 + } + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.UPDATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('archiveHabit should work', async () => { + const expectedHabit = { ...localHabit1 } + + await store.dispatch(archiveHabit(expectedHabit)) + + expectedHabit.archived = true + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.UPDATE + } + + const expectedActions = [ + updateEditedHabit(expectedHabit), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('deleteHabitById should work', async () => { + const expectedHabit = { ...localHabit1 } + + await store.dispatch(deleteHabitById(expectedHabit)) + + const expectedUnsyncedHabit: UnsyncedHabit = { + actionDate: startOfTodayString, + habit: expectedHabit, + mutationType: MutationType.DELETE + } + + const expectedActions = [ + removeHabit(expectedHabit.id), + addUnsyncedHabit(expectedUnsyncedHabit) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) +}) diff --git a/src/actions/habit/habit-actions.ts b/src/actions/habit/habit-actions.ts new file mode 100644 index 0000000..cb0b843 --- /dev/null +++ b/src/actions/habit/habit-actions.ts @@ -0,0 +1,466 @@ +import { API, graphqlOperation } from 'aws-amplify' +import produce from 'immer' +import moment from 'moment' +import 'react-native-get-random-values' +import * as Sentry from '@sentry/react-native' +import { getAuthState } from 'store/Selectors/auth-selectors/auth-selectors' +import { + getHabits, + getUnsyncedHabits, + getHabitsMap +} from 'store/Selectors/habit-selectors/habit-selectors' +import { getUsername } from 'store/Selectors/UserSelectors' +import { GetState } from 'Types/GetState' +import ReduxAction, { Thunk, ThunkResult, Dispatch } from 'Types/ReduxActions' +import { Habit, MutationType, UnsyncedHabit } from 'Types/State/habit-state' +import { Period } from 'Types/State/Periods' +import { v4 } from 'uuid' +import { + CreateHabitInput, + ListHabitsQuery, + ModelHabitFilterInput +} from '../../API' +import { createHabit, deleteHabit, updateHabit } from '../../graphql/mutations' +import { listHabits } from '../../graphql/custom/queries' +import { + convertDaysToFitGraphQL, + convertLineBreaks, + convertPeriodType, + convertRemoteHabitsToLocalHabits, + shouldResetDayStreak +} from '../../helpers/habits' + +/* ACTION TYPES */ + +export const UPDATE_HABIT = 'UPDATE_HABIT' +export const DELETE_HABIT = 'DELETE_HABIT' +export const DRAFT_EDIT_HABIT = 'DRAFT_EDIT_HABIT' +export const PUSH_UNSYNCED_HABIT = 'PUSH_UNSYNCED_HABIT' +export const POP_UNSYNCED_HABIT = 'POP_UNSYNCED_HABIT' +export const TOGGLE_MERGING_DIALOG = 'TOGGLE_MERGING_DIALOG' +export const REPLACE_HABITS = 'REPLACE_HABITS' +export const REPLACE_SUB_HABITS = 'REPLACE_SUB_HABITS' +export const CLEAR_SUB_HABITS = 'CLEAR_SUB_HABITS' + +/* ACTIONS */ + +export const updateEditedHabit = (habit: Habit): ReduxAction => ({ + type: UPDATE_HABIT, + payload: habit +}) + +export const removeHabit = (id: string): ReduxAction => ({ + type: DELETE_HABIT, + payload: id +}) + +export const draftEditHabit = (habit: Habit): ReduxAction => ({ + type: DRAFT_EDIT_HABIT, + payload: habit +}) + +export const addUnsyncedHabit = ( + unsyncedHabit: UnsyncedHabit +): ReduxAction => ({ + type: PUSH_UNSYNCED_HABIT, + payload: unsyncedHabit +}) + +export const removeSyncedHabit = (habitId: string): ReduxAction => ({ + type: POP_UNSYNCED_HABIT, + payload: habitId +}) + +export const toggleMergingDialog = (toggle?: boolean): ReduxAction => ({ + type: TOGGLE_MERGING_DIALOG, + payload: toggle +}) + +export const replaceHabits = (habits: Map): ReduxAction => ({ + type: REPLACE_HABITS, + payload: habits +}) +export const clearSubHabits = (): ReduxAction => ({ + type: CLEAR_SUB_HABITS +}) +export const replaceSubHabits = (habits: Map): ReduxAction => ({ + type: REPLACE_SUB_HABITS, + payload: habits +}) + +/* ASYNC ACTIONS */ + +export const addHabit = ( + title: string, + description = '', + period: Period, + id?: string +): Thunk => async (dispatch: Dispatch) => { + const days = new Map() + + const habit: Habit = { + id: id || v4(), + userId: null, + title: title.trim(), + description: convertLineBreaks(description.trim()), + days, + date: moment().startOf('day').toISOString(), + period + } + + await dispatch(updateEditedHabit(habit)) + await dispatch(stashHabitToSync(habit, MutationType.CREATE)) +} + +export const editHabit = ( + title: string, + description: string, + period: Period, + modifiedHabit: Habit +): ThunkResult> => async (dispatch: Dispatch) => { + const updatedHabit: Habit = { + ...modifiedHabit, + title: title.trim(), + description: convertLineBreaks(description.trim()), + period + } + await dispatch(updateEditedHabit(updatedHabit)) + await dispatch(stashHabitToSync(updatedHabit, MutationType.UPDATE)) +} + +export const updateHabitDayStreak = ( + habit: Habit, + dayStreak: number +): ThunkResult> => async (dispatch: Dispatch) => { + const updatedHabit: Habit = { ...habit, dayStreak } + await dispatch(updateEditedHabit(updatedHabit)) + await dispatch(stashHabitToSync(updatedHabit, MutationType.UPDATE)) +} + +export const markTodayHabitAsCompleted = ( + habit: Habit +): ThunkResult> => async (dispatch: Dispatch) => { + const today = moment().startOf('day').toISOString() + const { days, longestDayStreak = 0 } = habit + let { dayStreak = 0 } = habit + let dayValue = 0 + + if (days.has(today)) { + const completedToday = days.get(today) === 1 + + if (!completedToday) { + dayStreak += 1 + dayValue = 1 + } else { + dayStreak -= 1 + dayValue = 0 + } + } else { + dayStreak += 1 + dayValue = 1 + } + + const updatedDays = produce(days, (draft) => { + draft.set(today, dayValue) + }) + + const updatedHabit: Habit = { + ...habit, + days: updatedDays, + latestCompletedDate: today, + dayStreak, + longestDayStreak: + longestDayStreak > dayStreak ? longestDayStreak : dayStreak + } + await dispatch(updateEditedHabit(updatedHabit)) + await dispatch(stashHabitToSync(updatedHabit, MutationType.UPDATE)) +} + +export const archiveHabit = ( + habit: Habit +): ThunkResult> => async (dispatch: Dispatch) => { + const updatedHabit: Habit = { + ...habit, + archived: habit.archived ? !habit.archived : true + } + await dispatch(updateEditedHabit(updatedHabit)) + await dispatch(stashHabitToSync(updatedHabit, MutationType.UPDATE)) +} + +export const deleteHabitById = (habit: Habit): Thunk => async ( + dispatch: Dispatch +) => { + await dispatch(removeHabit(habit.id)) + await dispatch(stashHabitToSync(habit, MutationType.DELETE)) +} + +const stashHabitToSync = ( + habit: Habit, + mutationType: MutationType +): ThunkResult> => async ( + dispatch: Dispatch, + getState: GetState +) => { + const loggedIn = getAuthState(getState()) + const unsyncedHabits = getUnsyncedHabits(getState()) + + if (loggedIn) { + const inQueue = unsyncedHabits.find( + (unsynced) => unsynced.habit.id === habit.id + ) + + const actionDate = moment().toISOString() + + if (!inQueue) { + await dispatch( + addUnsyncedHabit({ + actionDate, + habit, + mutationType + }) + ) + } else if (mutationType === MutationType.DELETE) { + await dispatch( + addUnsyncedHabit({ + actionDate, + habit, + mutationType + }) + ) + } else { + await dispatch( + addUnsyncedHabit({ + actionDate, + habit, + mutationType: inQueue.mutationType + }) + ) + } + } +} + +const syncHabit = ( + mutationType: MutationType, + habit: Habit +): ThunkResult> => async ( + dispatch: Dispatch, + getState: GetState +) => { + const username = getUsername(getState()) + const loggedIn = getAuthState(getState()) + + if (loggedIn) { + const updatedHabit: CreateHabitInput = { + id: habit.id, + date: habit.date, + title: habit.title, + description: habit?.description ? habit.description : '', + archived: habit.archived, + dayStreak: habit.dayStreak, + longestDayStreak: habit.longestDayStreak, + days: convertDaysToFitGraphQL(habit.days), + latestCompletedDate: habit.latestCompletedDate, + period: convertPeriodType(habit.period), + userId: username as string + } + + try { + switch (mutationType) { + case MutationType.DELETE: + await API.graphql( + graphqlOperation(deleteHabit, { input: { id: updatedHabit.id } }) + ) + break + + case MutationType.UPDATE: + await API.graphql( + graphqlOperation(updateHabit, { input: updatedHabit }) + ) + break + case MutationType.CREATE: + await API.graphql( + graphqlOperation(createHabit, { input: updatedHabit }) + ) + break + default: + break + } + + // Remove successfully synced habit + await dispatch(removeSyncedHabit(habit.id)) + } catch (error) { + Sentry.captureException(error) + } + } +} + +export const handleUnsyncedHabits = (): ThunkResult> => async ( + dispatch: Dispatch, + getState: GetState +) => { + const { + habitState: { unsyncedHabits } + } = getState() + + try { + const promiseArray: Promise[] = [] + unsyncedHabits.forEach((unsyncedHabit: UnsyncedHabit) => { + const { mutationType } = unsyncedHabit + promiseArray.push(dispatch(syncHabit(mutationType, unsyncedHabit.habit))) + }) + await Promise.all(promiseArray) + } catch (error) { + Sentry.captureException(error) + } +} + +// When user signs in, handle the intention of merging or not merging habits +export const handleHabitsFromCloudWhenLoggingIn = ( + userId: string, + merge: boolean +): Thunk => async (dispatch: Dispatch, getState: GetState) => { + const variables: { + filter: ModelHabitFilterInput + limit?: number + nextToken?: string + } = { + filter: { + userId: { + eq: userId + } + } + } + + try { + const response = (await API.graphql( + graphqlOperation(listHabits, variables) + )) as { + data: ListHabitsQuery + } + const cloudHabits = response.data.listHabits?.items + const resultHabits = convertRemoteHabitsToLocalHabits(cloudHabits) + const habits = getHabitsMap(getState()) + + const promiseArray: any[] = [] + + // If user does want to merge, we replace habitState.habits with concatenated on-cloud habits and current habitState.habits. + if (merge) { + habits.forEach((localHabit: Habit) => { + // go through responsed items and merging local habits to see if there is any habits in commons + const commonIndex = cloudHabits?.findIndex( + (item: any) => item.title.trim() === localHabit.title.trim() + ) + + const syncingHabit: Habit = { + ...localHabit, + id: '', + userId + } + + // if the local habit is not stored in the cloud yet, we sync and merge it + if (commonIndex === -1) { + syncingHabit.id = v4() // Create new id for adding to cloud + + // Perform sync here + promiseArray.push( + dispatch(syncHabit(MutationType.CREATE, syncingHabit)) + ) + } + // If the local habit is stored in the cloud, we update the cloud with the latest version of it (on-device/local) + else { + syncingHabit.id = cloudHabits[commonIndex].id // Keep the existing id + + // Perform sync here + promiseArray.push( + dispatch(syncHabit(MutationType.UPDATE, syncingHabit)) + ) + } + + // Perform merge here + resultHabits.set(syncingHabit.id, syncingHabit) + }) + } + + // Replace habitState.subHabits with old habitState.habits + promiseArray.push(dispatch(replaceSubHabits(habits))) + // Replace current habitState.habits with the result map + promiseArray.push(dispatch(replaceHabits(resultHabits))) + await Promise.all(promiseArray) + } catch (error) { + Sentry.captureException(error) + + await dispatch(toggleMergingDialog(false)) + } +} + +// When user logs out, invoke this +export const handleHabitsWhenloggingOut = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + await dispatch(handleUnsyncedHabits()) + + const { + habitState: { subHabits } + } = getState() + + // Replace current habitState.habits with habitState.subHabits + await dispatch(replaceHabits(subHabits)) +} + +export const handleUnsyncedHabitsThenRetrieveHabitsFromCloud = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const loggedIn = getAuthState(getState()) + + if (loggedIn) { + await dispatch(handleUnsyncedHabits()) + await dispatch(getHabitsFromCloud()) + } +} + +// Can be used to get saved-on-cloud habits +export const getHabitsFromCloud = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const username = getUsername(getState()) + + const variables: { + filter: ModelHabitFilterInput + } = { + filter: { + userId: { + eq: username + } + } + } + + try { + const response = (await API.graphql( + graphqlOperation(listHabits, { variables }) + )) as { + data: ListHabitsQuery + } + + const items = response.data.listHabits?.items + + const resultHabits = convertRemoteHabitsToLocalHabits(items) + await dispatch(replaceHabits(resultHabits)) + } catch (error) { + Sentry.captureException(error) + } +} + +export const updateDayStreaks = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const habits = getHabits(getState()) + habits.forEach((habit) => { + if (shouldResetDayStreak(habit)) { + dispatch(updateHabitDayStreak(habit, 0)) + } + }) +} diff --git a/src/actions/iOS/SleepDataActions.ts b/src/actions/iOS/SleepDataActions.ts new file mode 100644 index 0000000..ea27375 --- /dev/null +++ b/src/actions/iOS/SleepDataActions.ts @@ -0,0 +1,42 @@ +import Moment from 'moment' +import AppleHealthKit from 'react-native-healthkit' +import { Value } from '../../Types/Sleepdata' + +export const updateData = () => async (dispatch: Function) => {} + +export const createNight = ( + startTime: string, + endTime: string, + value?: Value +) => async (dispatch: Function) => { + if (!value) { + const newNightBed = { + startDate: startTime, + endDate: endTime, + value: Value.InBed + } + + const newNightSleep = { + startDate: startTime, + endDate: endTime, + value: Value.Asleep + } + + await AppleHealthKit.saveSleep( + newNightBed, + (error: any, response: any) => {} + ) + + await AppleHealthKit.saveSleep( + newNightSleep, + (error: any, response: any) => {} + ) + } else { + const newNight = { + value, + startDate: startTime, + endDate: endTime + } + await AppleHealthKit.saveSleep(newNight, (error: any, response: any) => {}) + } +} diff --git a/src/actions/insight-actions/insight-actions.ts b/src/actions/insight-actions/insight-actions.ts new file mode 100644 index 0000000..913349b --- /dev/null +++ b/src/actions/insight-actions/insight-actions.ts @@ -0,0 +1,96 @@ +import { getWeek } from 'store/Selectors/SleepDataSelectors' +import { Day } from 'Types/Sleepdata' +import { GetState } from 'Types/GetState' +import moment from 'moment' +import { nearestMinutes } from 'helpers/time' +/* ACTION TYPES */ + +export const CALCULATE_INSIGHT_START = 'CALCULATE_INSIGHT_START' +export const CALCULATE_INSIGHT_SUCCESS = 'CALCULATE_INSIGHT_SUCCESS' +export const CALCULATE_INSIGHT_FAILURE = 'CALCULATE_INSIGHT_FAILURE' + +/* ACTIONS */ + +export const calculationStart = () => ({ + type: CALCULATE_INSIGHT_START +}) + +export const calculationSuccess = (insights: Insight) => ({ + type: CALCULATE_INSIGHT_SUCCESS, + payload: insights +}) +export const calculationFailure = () => ({ + type: CALCULATE_INSIGHT_FAILURE +}) +/* ASYNC ACTIONS */ + +type BedTimeWindowInsight = { + start: string | undefined + center: string | undefined + end: string | undefined +} +type Insight = { + bedTimeWindow: BedTimeWindowInsight +} + +export const calculateBedtimeWindow = (days: Day[]): BedTimeWindowInsight => { + let averageBedTime = 0 + let divideBy = 0 + days.forEach((day) => { + const dayStarted = moment(day.date) // Beginning of the day + if (day.bedStart) { + const bedTimeStart = moment(day.bedStart) + + const totalDifference = bedTimeStart.diff(dayStarted, 'minutes') + // Add difference to the average time + averageBedTime += totalDifference + // increment divider + divideBy += 1 + } + }) + + if (divideBy !== 0) { + averageBedTime /= divideBy + } + + // Remove the extra 24 hours + if (averageBedTime > 1440) { + averageBedTime = -1440 + } + + const bedTimeWindowCenter = nearestMinutes( + 15, + moment().startOf('day').minutes(averageBedTime) + ).toISOString() + + const bedTimeWindowStart = moment(bedTimeWindowCenter) + .subtract(45, 'minutes') + .toISOString() + + const bedTimeWindowEnd = moment(bedTimeWindowCenter) + .add(45, 'minutes') + .toISOString() + + const insights = { + start: bedTimeWindowStart, + center: bedTimeWindowCenter, + end: bedTimeWindowEnd + } + + return insights +} + +export const calculateInsights = () => async ( + dispatch: Function, + getState: GetState +) => { + dispatch(calculationStart()) + const week = getWeek(getState()) + const insights = calculateBedtimeWindow(week) + + try { + dispatch(calculationSuccess({ bedTimeWindow: insights })) + } catch (error) { + dispatch(calculationFailure()) + } +} diff --git a/src/actions/linking/linking-actions.ts b/src/actions/linking/linking-actions.ts new file mode 100644 index 0000000..6c6da3b --- /dev/null +++ b/src/actions/linking/linking-actions.ts @@ -0,0 +1,137 @@ +import Auth from '@aws-amplify/auth' +import { API, graphqlOperation, GraphQLResult } from '@aws-amplify/api' +import { UpdateConnectionIDMutation } from 'API' +import CONFIG from '../../config/Config' +import { updateConnectionId } from '../../graphql/custom/mutations' +import { + updateSubscriptionStatus, + restorePurchase +} from '../subscription/subscription-actions' + +/* ACTIONS TYPES */ +export const LINKING_START = 'LINKING_START' +export const LINKING_SUCCESS = 'LINKING_SUCCESS' +export const LINKING_FAILURE = 'LINKING_FAILURE' + +export const REMOVE_LINK_START = 'REMOVE_LINK_START' +export const REMOVE_LINK_SUCCESS = 'REMOVE_LINK_SUCCESS' +export const REMOVE_LINK_FAILURE = 'REMOVE_LINK_FAILURE' + +/* ACTIONS */ + +export const startLinking = () => ({ + type: LINKING_START +}) + +export const linkSuccess = (connectionId?: string | null) => ({ + type: LINKING_SUCCESS, + payload: connectionId +}) + +export const linkingFailure = () => ({ + type: LINKING_FAILURE +}) + +export const removeLinkStart = () => ({ + type: REMOVE_LINK_START +}) + +export const removeLinkSuccess = () => ({ + type: REMOVE_LINK_SUCCESS +}) + +export const removeLinkFailure = () => ({ + type: REMOVE_LINK_FAILURE +}) + +/* ASYNC ACTIONS */ + +export const linkAccount = (connectionId: string) => async ( + dispatch: Function +) => { + dispatch(startLinking()) + + try { + const { username } = await Auth.currentUserInfo() + + const input = { + id: username, + connectionId + } + const codeIsValid = await validateLinkCode(connectionId, username) + if (codeIsValid) { + const { + data: { updateUser } + } = (await API.graphql( + graphqlOperation(updateConnectionId, { input }) + )) as { + data: UpdateConnectionIDMutation + } + await dispatch(restorePurchase()) + dispatch(linkSuccess(updateUser?.connectionId)) + } + } catch (error) { + console.warn(error) + dispatch(linkingFailure()) + } +} + +export const removeLink = () => async (dispatch: Function) => { + const { username } = await Auth.currentUserInfo() + try { + const input = { + id: username, + connectionId: null + } + const response: any = await API.graphql( + graphqlOperation(updateConnectionId, { input }) + ) + dispatch(removeLinkSuccess()) + } catch (error) { + console.warn(error) + dispatch(removeLinkFailure()) + } +} + +export const getConnectionId = () => async (dispatch: Function) => { + const { username } = await Auth.currentUserInfo() + try { + const input = { + id: username, + connectionId: null + } + // const response: any = await API.graphql(graphqlOperation(updateConnectionId, { input })); + // dispatch(removeLinkSuccess()); + } catch (error) { + // dispatch(removeLinkFailure()); + } +} + +/* HELPERS */ +export const validateLinkCode = async (code: string, userId: string) => { + const configuration = { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Origin: '' + }, + body: JSON.stringify({ + hashId: code, + userId + }) + } + + try { + const response = await fetch(CONFIG.LINK_VALIDATION_URL, configuration) + const { body } = await response.json() + const { valid, check, userId } = body + + if (valid && check >= 0 && check <= 9) { + return true + } + return false + } catch (error) { + return false + } +} diff --git a/src/actions/manual-sleep/manual-sleep-actions.spec.ts b/src/actions/manual-sleep/manual-sleep-actions.spec.ts new file mode 100644 index 0000000..4229799 --- /dev/null +++ b/src/actions/manual-sleep/manual-sleep-actions.spec.ts @@ -0,0 +1,29 @@ +import { + setValues, + SET_VALUES, + toggleEditMode, + TOGGLE_EDIT_MODE +} from './manual-sleep-actions' + +const testValues = { + start: { h: 10, m: 30 }, + end: { h: 12, m: 0 } +} + +describe('Manual sleep actions', () => { + it('should create an action to set values', () => { + const expectedAction = { + type: SET_VALUES, + payload: testValues + } + + expect(setValues(testValues.start, testValues.end)).toEqual(expectedAction) + }) + + it('should create an action to toggle edit mode', () => { + const expectedAction = { + type: TOGGLE_EDIT_MODE + } + expect(toggleEditMode()).toEqual(expectedAction) + }) +}) diff --git a/src/actions/manual-sleep/manual-sleep-actions.ts b/src/actions/manual-sleep/manual-sleep-actions.ts new file mode 100644 index 0000000..bb419d9 --- /dev/null +++ b/src/actions/manual-sleep/manual-sleep-actions.ts @@ -0,0 +1,122 @@ +import moment from 'moment' +import { Platform } from 'react-native' +import AppleHealthKit from 'react-native-healthkit' +import { + getAngleAM, + getNightDuration, + sortDays, + sortNights +} from '../../helpers/sleep' +import { GetState } from '../../Types/GetState' +import { Day, Value, Night } from '../../Types/Sleepdata' +import { updateSleepData } from '../sleep/sleep-data-actions' + +export const SET_VALUES = 'SET_VALUES' +export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE' + +export const setValues = ( + start: { h: number; m: number }, + end: { h: number; m: number } +) => ({ + type: SET_VALUES, + payload: { start, end } +}) + +export const toggleEditMode = () => ({ + type: TOGGLE_EDIT_MODE +}) + +export const addManualDataToNight = ( + date: string, + nightStart: { h: number; m: number }, + nightEnd: { h: number; m: number } +) => async (dispatch: Function, getState: GetState) => { + const { + sleepclock: { days, nights } + } = getState() + + const startTime = moment(date) + .startOf('day') + .subtract(1, 'day') + .hours(nightStart.h >= 18 ? nightStart.h : nightStart.h + 24) + .minute(nightStart.m) + .toISOString() + + const endTime = moment(date) + .startOf('day') + .subtract(1, 'day') + .hours(nightEnd.h >= 18 ? nightEnd.h : nightEnd.h + 24) + .minute(nightEnd.m) + .toISOString() + + const dayToUpdate = days.find((day: Day) => day.date === date) + const filteredDays = days.filter((day: Day) => day.date !== date) + + const newNightSample: Night = { + source: 'Nyxo', + sourceId: 'app.sleepcircle.application', + sourceName: 'Nyxo', + value: Value.InBed, + startDate: startTime, + endDate: endTime, + totalDuration: getNightDuration(startTime, endTime) + } + + if (dayToUpdate) { + const updatedDay: Day = { + ...dayToUpdate, + night: [...dayToUpdate.night, newNightSample] + } + + const sortedDays = sortDays([...filteredDays, updatedDay]) + const sortedNights = sortNights([...nights, newNightSample]) + + if (Platform.OS === 'ios') { + await createNight(startTime, endTime) + } + + await dispatch( + updateSleepData({ + days: sortedDays, + nights: sortedNights + }) + ) + } +} + +export const createNight = async ( + startTime: string, + endTime: string, + value?: Value +) => { + if (!value) { + const newNightBed = { + startDate: startTime, + endDate: endTime, + value: Value.InBed + } + + const newNightSleep = { + startDate: startTime, + endDate: endTime, + value: Value.Asleep + } + + await AppleHealthKit.saveSleep( + newNightBed, + (error: any, response: any) => {} + ) + + await AppleHealthKit.saveSleep( + newNightSleep, + (error: any, response: any) => {} + ) + } else { + const newNight = { + value, + startDate: startTime, + endDate: endTime + } + await AppleHealthKit.saveSleep(newNight, (error: any, response: any) => {}) + } +} diff --git a/src/actions/modal/modal-actions.spec.ts b/src/actions/modal/modal-actions.spec.ts new file mode 100644 index 0000000..9cc13b3 --- /dev/null +++ b/src/actions/modal/modal-actions.spec.ts @@ -0,0 +1,33 @@ +import { + TOGGLE_NEW_HABIT_MODAL, + TOGGLE_EDIT_HABIT_MODAL, + TOGGLE_RATING_MODAL, + toggleNewHabitModal, + toggleEditHabitModal, + toggleRatingModal +} from './modal-actions' + +describe('Manuals actions', () => { + it('should create an action to toggle new habit modal', () => { + const expectedAction = { + type: TOGGLE_NEW_HABIT_MODAL + } + + expect(toggleNewHabitModal()).toEqual(expectedAction) + }) + + it('should create an action to toggle edit habit modal', () => { + const expectedAction = { + type: TOGGLE_EDIT_HABIT_MODAL + } + + expect(toggleEditHabitModal()).toEqual(expectedAction) + }) + + it('should create an action to toggle rating modal', () => { + const expectedAction = { + type: TOGGLE_RATING_MODAL + } + expect(toggleRatingModal()).toEqual(expectedAction) + }) +}) diff --git a/src/actions/modal/modal-actions.ts b/src/actions/modal/modal-actions.ts new file mode 100644 index 0000000..40d9107 --- /dev/null +++ b/src/actions/modal/modal-actions.ts @@ -0,0 +1,21 @@ +export const TOGGLE_NEW_HABIT_MODAL = 'TOGGLE_NEW_HABIT_MODAL' +export const TOGGLE_EDIT_HABIT_MODAL = 'TOGGLE_EDIT_HABIT_MODAL' +export const TOGGLE_RATING_MODAL = 'TOGGLE_RATING_MODAL' +export const TOGGLE_EXPLANATIONS_MODAL = 'TOGGLE_EXPLANATIONS_MODAL' + +export const toggleNewHabitModal = (value?: boolean) => { + return { type: TOGGLE_NEW_HABIT_MODAL, payload: value } +} + +export const toggleEditHabitModal = (value?: boolean) => { + return { type: TOGGLE_EDIT_HABIT_MODAL, payload: value } +} + +export const toggleRatingModal = () => { + return { type: TOGGLE_RATING_MODAL } +} + +export const toggleExplanationsModal = (value?: boolean) => ({ + type: TOGGLE_EXPLANATIONS_MODAL, + payload: value +}) diff --git a/src/actions/onboarding/onboarding-actions.ts b/src/actions/onboarding/onboarding-actions.ts new file mode 100644 index 0000000..32fec4c --- /dev/null +++ b/src/actions/onboarding/onboarding-actions.ts @@ -0,0 +1,10 @@ +export const INTERCOM_NEED_HELP_READ = 'INTERCOM_NEED_HELP_READ' +export const DATA_ONBOARDING_COMPLETED = 'DATA_ONBOARDING_COMPLETED' + +export const markIntercomHelpAsRead = () => ({ + type: INTERCOM_NEED_HELP_READ +}) + +export const markDataOnboardingCompleted = () => ({ + type: DATA_ONBOARDING_COMPLETED +}) diff --git a/src/actions/shared.ts b/src/actions/shared.ts new file mode 100644 index 0000000..6f323f0 --- /dev/null +++ b/src/actions/shared.ts @@ -0,0 +1 @@ +export const RESET_APP = 'RESET_APP' diff --git a/src/actions/sleep-source-actions/revoke-previous-source.ts b/src/actions/sleep-source-actions/revoke-previous-source.ts new file mode 100644 index 0000000..a91a872 --- /dev/null +++ b/src/actions/sleep-source-actions/revoke-previous-source.ts @@ -0,0 +1,35 @@ +import { SOURCE } from 'typings/state/sleep-source-state' +import { toggleHealthKit } from '@actions/sleep-source-actions/sleep-source-actions' +import { toggleGoogleFit } from '@actions/api-actions/google-fit-actions' +import { toggleFitbit } from '@actions/api-actions/fitbit-actions' +import { Thunk, Dispatch } from 'Types/ReduxActions' +import { GetState } from 'Types/GetState' +import { getMainSource } from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { toggleOura } from '@actions/api-actions/oura-actions' +import { toggleWithings } from '@actions/api-actions/withings-actions' + +export const revokePreviousSource = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const currentSource = getMainSource(getState()) + + switch (currentSource) { + case SOURCE.HEALTH_KIT: + await dispatch(toggleHealthKit()) + break + case SOURCE.GOOGLE_FIT: + await dispatch(toggleGoogleFit()) + break + case SOURCE.FITBIT: + await dispatch(toggleFitbit()) + break + case SOURCE.OURA: + await dispatch(toggleOura()) + case SOURCE.WITHINGS: + await dispatch(toggleWithings()) + break + default: + break + } +} diff --git a/src/actions/sleep-source-actions/sleep-source-actions.ts b/src/actions/sleep-source-actions/sleep-source-actions.ts new file mode 100644 index 0000000..4d0779d --- /dev/null +++ b/src/actions/sleep-source-actions/sleep-source-actions.ts @@ -0,0 +1,87 @@ +import { revokePreviousSource } from '@actions/sleep-source-actions/revoke-previous-source' +import { setHealthKitStatus } from '@actions/sleep/health-kit-actions' +import { fetchSleepData } from '@actions/sleep/sleep-data-actions' +import AppleHealthKit from 'react-native-healthkit' +import { getIsHealthKitMainSource } from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { GetState } from 'Types/GetState' +import { Dispatch, Thunk } from 'Types/ReduxActions' +import { SOURCE, SUB_SOURCE } from 'typings/state/sleep-source-state' + +const PERMS = AppleHealthKit.Constants.Permissions + +const healthKitOptions = { + permissions: { + read: [PERMS.HeartRate, PERMS.ActiveEnergyBurned, PERMS.SleepAnalysis], + write: [PERMS.SleepAnalysis] + } +} + +/* ACTION TYPES */ + +export const SET_MAIN_SOURCE = 'SET_MAIN_SOURCE' +export const CHANGE_HEALTH_KIT_SOURCE = 'CHANGE_HEALTH_KIT_SOURCE' +export const UPDATE_HEALTH_KIT_SOURCES = 'UPDATE_HEALTH_KIT_SOURCES' + +export const CHANGE_GOOGLE_FIT_SOURCE = 'CHANGE_GOOGLE_FIT_SOURCE' +export const UPDATE_GOOGLE_FIT_SOURCES = 'UPDATE_GOOGLE_FIT_SOURCES' + +/* ACTIONS */ + +export const setMainSource = (source: SOURCE) => ({ + type: SET_MAIN_SOURCE, + payload: { mainSource: source } +}) + +export const changeHealthKitSource = (hkSource: SUB_SOURCE) => ({ + type: CHANGE_HEALTH_KIT_SOURCE, + payload: { healthKitSource: hkSource } +}) + +export const updateHealthKitSources = (sources: SUB_SOURCE[]) => ({ + type: UPDATE_HEALTH_KIT_SOURCES, + payload: { sources } +}) + +export const changeGoogleFitSource = (googleFitSource: SUB_SOURCE) => ({ + type: CHANGE_GOOGLE_FIT_SOURCE, + payload: { googleFitSource } +}) + +export const updateGoogleFitSources = (sources: SUB_SOURCE[]) => ({ + type: UPDATE_GOOGLE_FIT_SOURCES, + payload: { sources } +}) + +/* ASYNC ACTIONS */ + +export const toggleHealthKit = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const isHealthKitMainSource = getIsHealthKitMainSource(getState()) + + try { + if (isHealthKitMainSource) { + dispatch(setMainSource(SOURCE.NO_SOURCE)) + } else { + await dispatch(revokePreviousSource()) + await dispatch(setHealthKitAsSourceAndFetch()) + await dispatch(setMainSource(SOURCE.HEALTH_KIT)) + } + } catch (err) { + console.warn(err) + } +} + +export const setHealthKitAsSourceAndFetch = (): Thunk => async ( + dispatch: Dispatch +) => { + await AppleHealthKit.initHealthKit(healthKitOptions, async (err, res) => { + if (err) { + await dispatch(setHealthKitStatus(false)) + } else { + dispatch(setHealthKitStatus(true)) + await dispatch(fetchSleepData()) + } + }) +} diff --git a/src/actions/sleep/health-kit-actions.ts b/src/actions/sleep/health-kit-actions.ts new file mode 100644 index 0000000..9ee40e3 --- /dev/null +++ b/src/actions/sleep/health-kit-actions.ts @@ -0,0 +1,149 @@ +import { + changeHealthKitSource, + updateHealthKitSources +} from '@actions/sleep-source-actions/sleep-source-actions' +import { formatHealthKitResponse } from 'helpers/sleep/sleep-data-helper' +import moment from 'moment' +import { Platform } from 'react-native' +import { + default as AppleHealthKit, + default as appleHealthKit +} from 'react-native-healthkit' +import { getHealthKitSource } from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { SUB_SOURCE } from 'typings/state/sleep-source-state' +import { Dispatch, Thunk } from 'Types/ReduxActions' +import { GetState } from 'Types/GetState' +import { SleepDataSource } from '../../Types/SleepClockState' +import { HealthKitSleepResponse, Night } from '../../Types/Sleepdata' +import { fetchSleepData, formatSleepData } from './sleep-data-actions' + +/* ACTION TYPES */ + +export const FETCH_SLEEP_HEALTH_KIT_START = 'FETCH_SLEEP_HEALTH_KIT_START' +export const FETCH_SLEEP_HEALTH_KIT_SUCCESS = 'FETCH_SLEEP_HEALTH_KIT_SUCCESS' +export const FETCH_SLEEP_HEALTH_KIT_FAILURE = 'FETCH_SLEEP_HEALTH_KIT_FAILURE' + +export const SWITCH_HEALTH_KIT_SOURCE = 'SWITCH_HEALTH_KIT_SOURCE' + +export const TOGGLE_HEALTH_KIT_AVAILABILITY = 'TOGGLE_HEALTH_KIT_AVAILABILITY' +export const TOGGLE_USE_HEALTH_KIT = 'TOGGLE_USE_HEALTH_KIT' +export const SET_HEALTH_KIT_STATUS = 'SET_HEALTH_KIT_STATUS' + +/* ACTIONS */ + +export const setHealthKitStatus = (enabled: boolean) => ({ + type: SET_HEALTH_KIT_STATUS, + payload: enabled +}) + +export const fetchHKSleepStart = () => ({ + type: FETCH_SLEEP_HEALTH_KIT_START +}) + +export const fetchHKSleepSuccess = () => ({ + type: FETCH_SLEEP_HEALTH_KIT_SUCCESS +}) + +export const fetchHKSleepFailure = () => ({ + type: FETCH_SLEEP_HEALTH_KIT_FAILURE +}) + +/* ASYNC ACTIONS */ + +const PERMS = AppleHealthKit.Constants.Permissions + +const healthKitOptions = { + permissions: { + read: [PERMS.HeartRate, PERMS.ActiveEnergyBurned, PERMS.SleepAnalysis], + write: [PERMS.SleepAnalysis] + } +} + +export const prepareSleepDataFetching = () => async (dispatch: Function) => { + Platform.OS === 'ios' ? await dispatch(initHealthKit()) : null +} + +export const initHealthKit = () => async (dispatch: Function) => { + await AppleHealthKit.initHealthKit(healthKitOptions, (err, res) => { + if (err) { + dispatch(setHealthKitStatus(false)) + } else { + dispatch(setHealthKitStatus(true)) + } + }) +} + +/** + * Switches tracking source and gets all the new data + * @todo fix so that it does not always fetch all the data + * @param {Array} nights Unfiltered night data from Healthkit + * + */ +export const switchHKSourceAndFetch = (hkSource: SUB_SOURCE): Thunk => async ( + dispatch: Dispatch +) => { + dispatch(changeHealthKitSource(hkSource)) + dispatch(fetchSleepData()) +} + +export const createHealthKitSources = ( + rawSleepData: HealthKitSleepResponse[] = [] +): Thunk => async (dispatch: Dispatch, getState: GetState) => { + const hkSource = getHealthKitSource(getState()) + + const sourceList: SUB_SOURCE[] = [ + { sourceName: 'Nyxo', sourceId: 'app.sleepcircle.application' } + ] + + rawSleepData.forEach((item: HealthKitSleepResponse) => { + const existingSource = sourceList.find( + (source: SleepDataSource) => source.sourceId === item.sourceId + ) + + if (!existingSource) { + sourceList.push({ + sourceName: item.sourceName, + sourceId: item.sourceId + }) + } + }) + + dispatch(updateHealthKitSources(sourceList)) + const noSleepTrackersInState = !hkSource + + if (noSleepTrackersInState) { + const tracker = sourceList[1] ? sourceList[1] : sourceList[0] + await dispatch(changeHealthKitSource(tracker)) + } +} + +export const fetchSleepFromHealthKit = () => async (dispatch: Function) => { + dispatch(fetchHKSleepStart()) + const getDataFrom = moment().subtract(2, 'week').startOf('days').toISOString() + + const options = { + startDate: getDataFrom + } + try { + await appleHealthKit.getSleepSamples( + options, + async (error: any, response: any) => { + if (error) { + dispatch(fetchHKSleepFailure()) + } + dispatch(createHealthKitSources(response)) + + const formattedData: Night[] = response.map( + (nightObject: HealthKitSleepResponse) => + formatHealthKitResponse(nightObject) + ) + + dispatch(formatSleepData(formattedData)) + dispatch(fetchHKSleepSuccess()) + } + ) + } catch (error) { + console.warn(error) + dispatch(fetchHKSleepFailure()) + } +} diff --git a/src/actions/sleep/sleep-data-actions.ts b/src/actions/sleep/sleep-data-actions.ts new file mode 100644 index 0000000..fdfb101 --- /dev/null +++ b/src/actions/sleep/sleep-data-actions.ts @@ -0,0 +1,258 @@ +import { getFitbitSleep } from '@actions/api-actions/fitbit-actions' +import { readGoogleFitSleep } from '@actions/api-actions/google-fit-actions' +import { getOuraSleep } from '@actions/api-actions/oura-actions' +import { getWithingsSleep } from '@actions/api-actions/withings-actions' +import { sendError } from '@actions/NotificationActions' +import { + calculateTotalSleep, + findEndTime, + findStartTime, + matchDayAndNight +} from 'helpers/sleep/sleep-data-helper' +import { sameDay } from 'helpers/time' +import moment from 'moment' +import { + getMainSource, + getSharedSource +} from 'store/Selectors/sleep-source-selectors/sleep-source-selectors' +import { getAllDays } from 'store/Selectors/SleepDataSelectors' +import { GetState } from 'Types/GetState' +import { Dispatch, Thunk } from 'Types/ReduxActions' +import { SOURCE } from 'typings/state/sleep-source-state' +import { Day, Night, Value } from '../../Types/Sleepdata' +import { fetchSleepFromHealthKit } from './health-kit-actions' + +/* ACTION TYPES */ + +export const CREATE_CALENDAR_START = 'CREATE_CALENDAR_START' + +export const CREATE_NEW_CALENDAR = 'CREATE_NEW_CALENDAR' +export const PUSH_NEW_DAYS_TO_CALENDAR = 'PUSH_NEW_DAYS_TO_CALENDAR' +export const RATE_NIGHT = 'RATE_NIGHT' +export const UPDATE_DAY = 'UPDATE_DAY' +export const UPDATE_SLEEP_DATA = 'UPDATE_SLEEP_DATA' +export const SET_START_DATE = 'SET_START_DAY' +export const SET_TODAY = 'SET_TODAY' +export const SET_TODAY_AS_SELECTED = 'SET_TODAY_AS_SELECTED' +export const SET_SELECTED_DAY = 'SET_SELECTED_DAY' +export const SET_ACTIVE_INDEX = 'SET_ACTIVE_INDEX' + +/* ACTIONS */ + +export const createNewCalendar = (days: Day[]) => ({ + type: CREATE_NEW_CALENDAR, + payload: days +}) + +export const pushNewDaysToCalendar = (days: Day[]) => ({ + type: PUSH_NEW_DAYS_TO_CALENDAR, + payload: days +}) + +export const rateDay = (rating: number) => ({ + type: RATE_NIGHT, + payload: rating +}) + +export const updateDay = (day: Day) => ({ + type: UPDATE_DAY, + payload: day +}) + +export const updateSleepData = (data: { days: Day[]; nights: Night[] }) => ({ + type: UPDATE_SLEEP_DATA, + payload: data +}) + +export const setSelectedDay = (day: Day) => ({ + type: SET_SELECTED_DAY, + payload: day +}) + +export const setToday = (today: string) => ({ + type: SET_TODAY, + payload: today +}) + +export const setTodayAsSelected = (today: string) => ({ + type: SET_TODAY_AS_SELECTED, + payload: today +}) + +export const setStartDate = (date: string) => ({ + type: SET_START_DATE, + payload: date +}) + +export const setActiveIndex = (index: number) => ({ + type: SET_ACTIVE_INDEX, + payload: index +}) + +/* ASYNC ACTIONS */ + +export const fetchSleepData = (): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + const source = getMainSource(getState()) + + switch (source) { + case SOURCE.HEALTH_KIT: + dispatch(fetchSleepFromHealthKit()) + break + case SOURCE.GOOGLE_FIT: + dispatch(readGoogleFitSleep()) + break + case SOURCE.FITBIT: + dispatch(getFitbitSleep()) + break + case SOURCE.OURA: + dispatch(getOuraSleep()) + break + case SOURCE.WITHINGS: + dispatch(getWithingsSleep()) + break + + default: + break + } +} + +const createCalendarFromScratch = () => async (dispatch: Dispatch) => { + const calendarDays = 7 + const startDate = moment().startOf('day').subtract(calendarDays, 'days') + + const calendar: Day[] = [] + + for (let i = 0; i < calendarDays; i++) { + const date = startDate.add(1, 'day') + calendar.push({ date: date.toISOString(), night: [] }) + } + + await dispatch(createNewCalendar(calendar)) +} + +const createNewDaysForCalendar = ( + lastDate: momentLike, + today: momentLike +) => async (dispatch: Function) => { + const dayArray: Day[] = [] + + while (today.isAfter(lastDate)) { + lastDate.add(1, 'day') + dayArray.push({ date: lastDate.toISOString(), night: [] }) + } + + await dispatch(pushNewDaysToCalendar(dayArray)) +} + +/* +Making a change on 26.1.2020 +Calendar should always have at least seven days in it +*/ + +export const updateCalendar = () => async ( + dispatch: Function, + getState: GetState +) => { + const today = moment() + const todayISO = moment().toISOString() + const { + sleepclock: { days, current_day } + } = getState() + + const currentDayDate: string | undefined = current_day?.date + const calendarIsEmpty = days === undefined || days?.length === 0 + + if (calendarIsEmpty) { + await dispatch(createCalendarFromScratch()) + } else { + const lastDate = days.sort((aDay, bDay) => + moment(aDay.date).diff(moment(bDay.date)) + )[0] + + const lastDateToMoment = moment(lastDate?.date) + + const todayIsAfterLastUpdatedDate = today + .startOf('day') + .isAfter(lastDateToMoment.startOf('day')) + + const lessThanSevenDays = days.length < 7 + + if (lessThanSevenDays) { + await dispatch( + createNewDaysForCalendar(moment(today).subtract(7, 'days'), today) + ) + } + + if (todayIsAfterLastUpdatedDate) { + await dispatch(createNewDaysForCalendar(lastDateToMoment, today)) + } + } + + if (!sameDay(currentDayDate, todayISO)) { + await dispatch(setTodayAsSelected(todayISO)) + } +} + +export const formatSleepData = (nights: Night[]): Thunk => async ( + dispatch: Dispatch, + getState: GetState +) => { + try { + const days = getAllDays(getState()) + const hkSource = getSharedSource(getState()) + // Filter data by the default + const filteredBySource = nights.filter( + (night: Night) => night.sourceId === hkSource?.sourceId + ) + const unfilteredNights = nights.filter( + (night: Night) => night.sourceId !== hkSource?.sourceId + ) + + const updatedDays: Day[] = days.map((day: Day) => { + const night: Night[] = filteredBySource.filter((nightObject: Night) => + matchDayAndNight(nightObject.startDate, day.date) + ) + const unfilteredNight = unfilteredNights.filter((nightObject: Night) => + matchDayAndNight(nightObject.startDate, day.date) + ) + + let sleepStart = day.sleepStart ? day.sleepStart : null + let sleepEnd = day.sleepEnd ? day.sleepEnd : null + let bedStart = day.bedStart ? day.bedStart : null + let bedEnd = day.bedEnd ? day.bedEnd : null + + const inBedDuration = calculateTotalSleep(night, Value.InBed) + const asleepDuration = calculateTotalSleep(night, Value.Asleep) + + // Start times for easier handling + if (inBedDuration !== 0) { + bedStart = findStartTime(night, Value.InBed) + bedEnd = findEndTime(night, Value.InBed) + } + + if (asleepDuration !== 0) { + sleepStart = findStartTime(night, Value.Asleep) + sleepEnd = findEndTime(night, Value.Asleep) + } + + return { + ...day, + night, + unfilteredNight, + asleepDuration, + inBedDuration, + sleepStart, + sleepEnd, + bedStart, + bedEnd + } + }) + + dispatch(updateSleepData({ days: updatedDays, nights })) + } catch (error) { + dispatch(sendError(error)) + } +} diff --git a/src/actions/sleep/sleep-to-cloud-actions.ts b/src/actions/sleep/sleep-to-cloud-actions.ts new file mode 100644 index 0000000..5f3706c --- /dev/null +++ b/src/actions/sleep/sleep-to-cloud-actions.ts @@ -0,0 +1,151 @@ +import API from '@aws-amplify/api' +import { graphqlOperation } from 'aws-amplify' +import { getAllDays } from 'store/Selectors/SleepDataSelectors' +import { getUsername } from 'store/Selectors/UserSelectors' +import { GetState } from 'Types/GetState' +import { v4 } from 'uuid' +import { ListSleepDatasQuery, UpdateSleepDataInput } from '../../API' +import { createSleepData, updateSleepData } from '../../graphql/mutations' +import { listSleepDatas } from '../../graphql/queries' +import { Day } from '../../Types/Sleepdata' + +/* ACTION TYPES */ + +export const PULL_START = 'PULL_START' +export const PULL_SUCCESS = 'PULL_SUCCESS' +export const PULL_FAILURE = 'PULL_FAILURE' + +export const CREATE_START = 'CREATE_START' +export const CREATE_SUCCESS = 'CREATE_SUCCESS' +export const CREATE_FAILURE = 'CREATE_FAILURE' + +export const UPDATE_START = 'UPDATE_START' +export const UPDATE_SUCCESS = 'UPDATE_SUCCESS' +export const UPDATE_FAILURE = 'UPDATE_FAILURE' + +/* ACTIONS */ + +const pullStart = () => ({ + type: PULL_START +}) + +const pullSuccess = () => ({ + type: PULL_SUCCESS +}) + +const pullFailure = () => ({ + type: PULL_FAILURE +}) + +// Create Sleep Data + +const createStart = () => ({ + type: CREATE_START +}) + +const createSuccess = (days: UpdateSleepDataInput[]) => ({ + type: CREATE_SUCCESS, + payload: { days } +}) + +const createFailure = () => ({ + type: CREATE_FAILURE +}) + +// Update Sleep Data + +const updateStart = () => ({ + type: UPDATE_START +}) + +const updateSuccess = () => ({ + type: UPDATE_SUCCESS +}) + +const updateFailure = () => ({ + type: UPDATE_FAILURE +}) + +/* AYSNC ACTIONS */ + +export const pullSleepFromCloud = () => async ( + dispatch: Function, + getState: GetState +) => { + dispatch(pullStart()) + const days = getAllDays(getState()) + const daysToCreate: Day[] = [] + const daysToUpdate: Day[] = [] + try { + const response = (await API.graphql( + graphqlOperation(listSleepDatas, {}) + )) as { + data: ListSleepDatasQuery + } + + days.forEach((day) => { + const exists = response.data.listSleepDatas?.items?.find( + (d) => d?.date === day.date + ) + exists ? daysToUpdate.push(day) : daysToCreate.push(day) + }) + + dispatch(createSleep(daysToCreate)) + dispatch(updateSleep(daysToUpdate)) + } catch (error) { + console.warn(error) + dispatch(pullFailure()) + } +} + +export const createSleep = (days: Day[]) => async ( + dispatch: Function, + getState: GetState +) => { + const createPromises: Promise[] = [] + const username = getUsername(getState()) + try { + const withIds: UpdateSleepDataInput[] = days.map((day) => ({ + id: v4(), + userId: username, + rating: day.rating, + night: day.night, + date: day.date + })) + + withIds.forEach((day) => { + createPromises.push( + API.graphql(graphqlOperation(createSleepData, { input: day })) as any + ) + }) + + await Promise.all(createPromises) + dispatch(createSuccess(withIds)) + } catch (error) { + console.warn(error) + dispatch(createFailure()) + } +} + +export const updateSleep = (days: Day[]) => async (dispatch: Function) => { + const updatePromises: Promise[] = [] + + try { + days.forEach((day) => { + const input: UpdateSleepDataInput = { + id: day.id as string, + rating: day.rating, + night: day.night + } + + updatePromises.push( + API.graphql(graphqlOperation(updateSleepData, { input })) as any + ) + }) + + await Promise.all(updatePromises) + } catch (error) { + console.warn(error) + dispatch(updateFailure()) + } +} diff --git a/src/actions/subscription/subscription-actions.ts b/src/actions/subscription/subscription-actions.ts new file mode 100644 index 0000000..b5ea7af --- /dev/null +++ b/src/actions/subscription/subscription-actions.ts @@ -0,0 +1,155 @@ +import Purchases, { PurchasesPackage } from 'react-native-purchases' +import Intercom from 'react-native-intercom' +import CONFIG from '../../config/Config' +import { GetState } from '../../Types/GetState' +import { updateIntercomInformation } from '../IntercomActions' + +const key = CONFIG.SUBSCRIPTION_ENTITLEMENT_KEY as string + +/* ACTION TYPES */ + +export const PURCHASE_SUBSCRIPTION_START = 'PURCHASE_SUBSCRIPTION_START' +export const PURCHASE_SUBSCRIPTION_SUCCESS = 'PURCHASE_SUBSCRIPTION_SUCCESS' +export const PURCHASE_SUBSCRIPTION_FAILURE = 'PURCHASE_SUBSCRIPTION_FAILURE' + +export const RESTORE_START = 'RESTORE_START' +export const RESTORE_SUCCESS = 'RESTORE_PURCHASE' +export const RESTORE_FAILURE = 'RESTORE_FAILURE' + +export const DISABLE_COACHING = 'DISABLE_COACHING' + +/* ACTIONS */ + +export const purchaseStart = () => ({ + type: PURCHASE_SUBSCRIPTION_START +}) + +export const purchaseSuccess = (payload: { + isActive: boolean + expirationDate?: string | null +}) => ({ + type: PURCHASE_SUBSCRIPTION_SUCCESS, + payload +}) + +export const purchaseFailure = (error: string) => ({ + type: PURCHASE_SUBSCRIPTION_FAILURE, + payload: error +}) + +export const restoreStart = () => ({ + type: RESTORE_START +}) + +export const restoreSuccess = (payload: { + isActive: boolean + expirationDate?: string | null +}) => ({ + type: RESTORE_SUCCESS, + payload +}) + +export const restoreFailure = () => ({ + type: RESTORE_FAILURE +}) + +export const purchaseCoachingForAWeek = () => ({ + type: RESTORE_START +}) + +export const purchaseCoachingForAYear = () => ({ + type: RESTORE_START +}) + +export const disableCoaching = () => ({ + type: DISABLE_COACHING +}) + +/* ASYNC ACTIONS */ + +/** + * @async + * Run on every app start and updates subscription status + */ +export const updateSubscriptionStatus = () => async ( + dispatch: Function, + getState: GetState +) => { + try { + const { + entitlements: { active } + } = await Purchases.getPurchaserInfo() + if (typeof active[key] !== 'undefined') { + const { expirationDate, latestPurchaseDate } = active[key] + await updateIntercomInformation({ + subscription: 'not active', + latestPurchaseDate, + expirationDate + }) + dispatch(purchaseSuccess({ isActive: true, expirationDate })) + } else { + await updateIntercomInformation({ + subscription: 'not active' + }) + dispatch(purchaseSuccess({ isActive: false })) + } + } catch (error) { + console.warn(error) + } +} + +/** + * @async + * Purchases a new subscription for nyxo coaching and updates Intercom's user information to reflect this. + */ +export const purchaseSubscription = (subscription: PurchasesPackage) => async ( + dispatch: Function +) => { + dispatch(purchaseStart()) + try { + const { purchaserInfo } = await Purchases.purchasePackage(subscription) + if (typeof purchaserInfo.entitlements.active[key] !== 'undefined') { + const { + isActive, + expirationDate, + latestPurchaseDate + } = purchaserInfo.entitlements.active[key] + + await Intercom.updateUser({ + custom_attributes: { + subscription: 'active', + purchase_date: latestPurchaseDate, + expiration_date: expirationDate || 'lifetime' + } + }) + + dispatch(purchaseSuccess({ isActive, expirationDate })) + } + } catch (error) { + dispatch(purchaseFailure(error)) + } +} + +/** + * @async + * Restores a user's previous purchases and enables coaching for user + */ +export const restorePurchase = () => async (dispatch: Function) => { + dispatch(restoreStart()) + try { + const purchaserInfo = await Purchases.restoreTransactions() + if (typeof purchaserInfo.entitlements.active[key] !== 'undefined') { + dispatch( + restoreSuccess({ + isActive: true, + expirationDate: purchaserInfo.entitlements.active[key].expirationDate + }) + ) + } else { + dispatch(restoreSuccess({ isActive: false })) + } + } catch (error) { + console.warn(error) + dispatch(restoreFailure()) + } +} diff --git a/src/actions/user/user-actions.ts b/src/actions/user/user-actions.ts new file mode 100644 index 0000000..7dd8071 --- /dev/null +++ b/src/actions/user/user-actions.ts @@ -0,0 +1,81 @@ +import { Auth, graphqlOperation, API } from 'aws-amplify' +import { updateUser } from 'graphql/mutations' +import { GetState } from 'Types/GetState' +import { ThemeProps } from '../../styles/themes' + +/* ACTION TYPES */ +export const CHANGE_USER_NAME = 'CHANGE_USER_NAME' +export const CHANGE_USER_EMAIL = 'CHANGE_USER_EMAIL' +export const COMPLETE_INTRODUCTION = 'COMPLETE_INTRODUCTION' +export const SET_THEME = 'SET_THEME' +export const SET_INTERCOM_ID = 'SET_INTERCOM_ID' +export const UPDATE_EMAIL = 'UPDATE_EMAIL' +export const UPDATE_USER_FROM_CLOUD = 'UPDATE_USER_FROM_CLOUD' + +export const UPDATE_USER_START = 'UPDATE_USER_START' +export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS' +export const UPDATE_USER_FAILURE = 'UPDATE_USER_FAILURE' + +/* ACTIONS */ + +export const updateEmail = (email: string) => ({ + type: UPDATE_EMAIL, + payload: email +}) + +export const markIntroductionCompleted = (completed: boolean) => ({ + type: COMPLETE_INTRODUCTION, + payload: completed +}) + +export const setTheme = (theme: ThemeProps) => ({ + type: SET_THEME, + payload: theme +}) + +export const updateUserFromCloud = (user: any) => ({ + type: UPDATE_USER_FROM_CLOUD, + payload: user +}) + +export const setIntercomId = (intercomId: string) => ({ + payload: intercomId, + type: SET_INTERCOM_ID +}) + +export const updateUserStart = () => ({ + type: UPDATE_USER_START +}) + +export const updateUserSuccess = () => ({ + type: UPDATE_USER_SUCCESS +}) + +export const updateUserFailure = () => ({ + type: UPDATE_USER_FAILURE +}) + +/* ASYNC ACTIONS */ + +export const updateUserDateInCloud = () => async ( + dispatch: Function, + getState: GetState +) => { + dispatch(updateUserStart()) + const { + user: { intercomId } + } = getState() + const { username } = await Auth.currentUserInfo() + try { + const input = { + id: username, + intercomId + } + const response: any = await API.graphql( + graphqlOperation(updateUser, { input }) + ) + dispatch(updateUserSuccess()) + } catch (error) { + dispatch(updateUserFailure()) + } +} diff --git a/src/components/AnimatedFastImage/AnimatedFastImage.tsx b/src/components/AnimatedFastImage/AnimatedFastImage.tsx new file mode 100644 index 0000000..042c8d7 --- /dev/null +++ b/src/components/AnimatedFastImage/AnimatedFastImage.tsx @@ -0,0 +1,6 @@ +import FastImage from 'react-native-fast-image' +import Animated from 'react-native-reanimated' + +const AnimatedFastImage = Animated.createAnimatedComponent(FastImage) + +export default AnimatedFastImage diff --git a/src/components/AnimatedSvgPath.tsx b/src/components/AnimatedSvgPath.tsx new file mode 100644 index 0000000..8bd474c --- /dev/null +++ b/src/components/AnimatedSvgPath.tsx @@ -0,0 +1,136 @@ +import React, { useState, useRef, Component, useEffect, memo } from 'react' +import Animated, { Easing } from 'react-native-reanimated' +import { Path } from 'react-native-svg' +import { describeArc } from '../helpers/geometry' + +const { + Value, + set, + Clock, + onChange, + timing, + concat, + lessThan, + cond, + and, + clockRunning, + block, + debug, + startClock, + stopClock, + add, + or +} = Animated + +class AnimatedPath extends Component { + setNativeProps = (props) => { + this._component && this._component.setNativeProps(props) + } + + render() { + return ( + (this._component = component)} + {...this.props} + /> + ) + } +} +const AnimatedComponentPath = Animated.createAnimatedComponent(AnimatedPath) + +interface AnimatedSvgPathProps { + startAngle: number + endAngle: number + index: number + x: number + y: number + radius: number + color: string + strokeWidth: number +} + +const AnimatedSvgPath = (props: AnimatedSvgPathProps) => { + // this.intermediate = { startAngle: 0, endAngle: 360 }; + + const ref = useRef() + const clock = new Clock() + const animatedStartAngle = new Value(1) + const animatedEndAngle = new Value(1) + + const [startAngle, setStartAngle] = useState(0) + const [endAngle, setEndAngle] = useState(360) + + Animated.useCode( + block([ + // set(animatedStartAngle, props.startAngle), + ]), + [props.startAngle] + ) + + const animatedPath = runTiming(clock, -120, 120) + + const path = () => { + return describeArc( + props.x, + props.y, + props.radius, + startAngle, + endAngle + ).toString() + } + + // ref.current.setNativeProps({ d: path() }); + + return ( + + ) +} + +export default memo(AnimatedSvgPath) + +function runTiming(clock, value, dest) { + const state = { + finished: new Value(0), + position: new Value(0), + time: new Value(0), + frameTime: new Value(0) + } + + const config = { + duration: 5000, + toValue: new Value(0), + easing: Easing.inOut(Easing.ease) + } + + return block([ + cond( + clockRunning(clock), + [ + // if the clock is already running we update the toValue, in case a new dest has been passed in + set(config.toValue, dest) + ], + [ + // if the clock isn't running we reset all the animation params and start the clock + set(state.finished, 0), + set(state.time, 0), + set(state.position, value), + set(state.frameTime, 0), + set(config.toValue, dest), + startClock(clock) + ] + ), + // we run the step here that is going to update position + timing(clock, state, config), + // if the animation is over we stop the clock + cond(state.finished, debug('stop clock', stopClock(clock))), + // we made the block return the updated position + state.position + ]) +} diff --git a/src/components/AnimationComponents/AnimateDisplay.tsx b/src/components/AnimationComponents/AnimateDisplay.tsx new file mode 100644 index 0000000..ad9cb92 --- /dev/null +++ b/src/components/AnimationComponents/AnimateDisplay.tsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect, useRef } from 'react' +import { View, Image, Dimensions } from 'react-native' +import Animated, { Easing } from 'react-native-reanimated' +import { withNavigationFocus } from 'react-navigation' + +import MaskedView from '@react-native-community/masked-view' + +const { height, width } = Dimensions.get('window') + +interface AnimateDisplayProps { + children: any + navigation: { + isFocused: () => boolean + } +} + +const AnimateDisplay = (props: AnimateDisplayProps) => { + const [scaleValue, setScale] = useState(new Animated.Value(1)) + const [scaleChild, setScaleChild] = useState(new Animated.Value(1)) + const [capturedView, captureView] = useState( + require('../../assets/testScreen.png') + ) + + const animateScale = () => { + Animated.timing( + // Animate over time + scaleValue, // The animated value to drive + { + easing: Easing.inOut(Easing.ease), + toValue: 200, // Animate to opacity: 1 (opaque) + duration: 250 // Make it take a while + } + ).start() + } + + const animateChild = () => { + Animated.timing( + // Animate over time + scaleChild, // The animated value to drive + { + easing: Easing.inOut(Easing.ease), + toValue: 1, // Animate to opacity: 1 (opaque) + duration: 250 // Make it take a while + } + ).start() + } + + useEffect(() => { + if (props.navigation.isFocused()) { + animateScale() + animateChild() + } else { + setScale(new Animated.Value(1)) + setScaleChild(new Animated.Value(0)) + } + }, [props.navigation.isFocused()]) + + // const scaleOut = scaleValue.interpolate({ + // inputRange: [0, 1], + // outputRange: [1, 20], + // }); + + // const scaleIn = scaleValue.interpolate({ + // inputRange: [0, 1], + // outputRange: [20, 1], + // }); + + return ( + + + + + + }> + + {props.children} + + + + ) +} + +export default withNavigationFocus(AnimateDisplay) diff --git a/src/components/AuthSpecific/Disclaimer.tsx b/src/components/AuthSpecific/Disclaimer.tsx new file mode 100644 index 0000000..d9b7676 --- /dev/null +++ b/src/components/AuthSpecific/Disclaimer.tsx @@ -0,0 +1,44 @@ +import React, { memo } from 'react' +import { Linking } from 'react-native' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import CONFIG from '../../config/Config' + +const Disclaimer = () => { + const openTerms = () => { + Linking.openURL(CONFIG.TERMS_LINK) + } + + const openPrivacyPolicy = () => { + Linking.openURL(CONFIG.PRIVACY_LINK) + } + + return ( + + + By creating a Nyxo Cloud Account, I agree to Nyxo's{' '} + Terms of Service + and + Privacy Policy + + + ) +} + +export default memo(Disclaimer) + +const DisclaimerContainer = styled.View` + margin: 30px 0px; +` + +const UnderLine = styled.Text` + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + text-decoration: underline; + margin: 0px 5px; +` + +const DisclaimerText = styled.Text` + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.medium}; + line-height: 25px; +` diff --git a/src/components/BottomInfo.tsx b/src/components/BottomInfo.tsx new file mode 100644 index 0000000..21f4dfa --- /dev/null +++ b/src/components/BottomInfo.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { View, Text } from 'react-native' + +const BottomInfo = () => { + return ( + + I'm just a placeholder + + ) +} + +export default BottomInfo diff --git a/src/components/BottomTabSheet/BottomActionSheet.tsx b/src/components/BottomTabSheet/BottomActionSheet.tsx new file mode 100644 index 0000000..4417d3e --- /dev/null +++ b/src/components/BottomTabSheet/BottomActionSheet.tsx @@ -0,0 +1,22 @@ +import React, { memo } from 'react' +import { View, Text, Dimensions } from 'react-native' +import BottomSheet from 'reanimated-bottom-sheet' +import BottomActionSheetHeader from './BottomActionSheetHeader' +import BottomActionSheetContent from './BottomActionSheetContent' + +const { height } = Dimensions.get('window') + +const BottomActionSheet = () => { + const renderInner = () => + const renderHeader = () => + + return ( + + ) +} + +export default memo(BottomActionSheet) diff --git a/src/components/BottomTabSheet/BottomActionSheetContent.tsx b/src/components/BottomTabSheet/BottomActionSheetContent.tsx new file mode 100644 index 0000000..23e59df --- /dev/null +++ b/src/components/BottomTabSheet/BottomActionSheetContent.tsx @@ -0,0 +1,15 @@ +import React, { memo } from 'react' +import { View, Text, Dimensions } from 'react-native' +import { BlurView } from 'expo-blur' + +const BottomSheetContent = () => { + return ( + + + Start tracking sleep + + + ) +} + +export default memo(BottomSheetContent) diff --git a/src/components/BottomTabSheet/BottomActionSheetHeader.tsx b/src/components/BottomTabSheet/BottomActionSheetHeader.tsx new file mode 100644 index 0000000..eadfbb7 --- /dev/null +++ b/src/components/BottomTabSheet/BottomActionSheetHeader.tsx @@ -0,0 +1,13 @@ +import React, { memo } from 'react' +import { View, Text, Dimensions } from 'react-native' +import { BlurView } from 'expo-blur' + +const BottomActionSheetHeader = () => { + return ( + + {/* Header */} + + ) +} + +export default memo(BottomActionSheetHeader) diff --git a/src/components/Buttons/BackToAppButton.tsx b/src/components/Buttons/BackToAppButton.tsx new file mode 100644 index 0000000..6f80f83 --- /dev/null +++ b/src/components/Buttons/BackToAppButton.tsx @@ -0,0 +1,40 @@ +import { useNavigation } from '@react-navigation/native' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +const BackToAppButton = () => { + const navigation = useNavigation() + + const getBackToApp = () => { + navigation.navigate('Sleep') + } + + return ( + + + + ) +} + +export default memo(BackToAppButton) + +const Button = styled.TouchableOpacity` + justify-content: center; + align-items: center; +` + +const Container = styled.View` + margin: 30px; +` + +const ButtonText = styled(TranslatedText)` + font-family: ${fonts.bold}; + font-size: 15px; + padding: 5px; + color: ${colors.radiantBlue}; +` diff --git a/src/components/Buttons/BottomButton.tsx b/src/components/Buttons/BottomButton.tsx new file mode 100644 index 0000000..6e30419 --- /dev/null +++ b/src/components/Buttons/BottomButton.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import { TouchableOpacity } from 'react-native' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +interface BottomButtonProps { + disabled?: boolean + onPress: Function + title: string + loading?: boolean +} + +const BottomButton = (props: BottomButtonProps) => { + const { loading, disabled, title, onPress } = props + + const handlePress = () => { + onPress() + } + + return ( + + + + {!loading && {title}} + {loading && } + + + + ) +} + +export default BottomButton + +interface ButtonProps { + readonly disabled?: boolean +} + +const Container = styled.View` + margin: 10px 20px; +` + +const ButtonContainer = styled.View` + opacity: ${(props) => (props.disabled ? 0.5 : 1)}; + background-color: ${colors.radiantBlue}; + border-radius: 5px; + padding: 15px; +` + +const ButtonText = styled(TranslatedText)` + opacity: ${(props) => (props.disabled ? 0.2 : 1)}; + color: ${colors.white}; + font-family: ${fonts.medium}; + text-align: center; + font-size: 17px; +` + +const Loader = styled.ActivityIndicator`` diff --git a/src/components/Buttons/GoBack.tsx b/src/components/Buttons/GoBack.tsx new file mode 100644 index 0000000..dc08b14 --- /dev/null +++ b/src/components/Buttons/GoBack.tsx @@ -0,0 +1,56 @@ +import { useNavigation } from '@react-navigation/native' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { StyleProps } from '../../styles/themes' +import IconBold from '../iconBold' + +interface Props { + route?: string +} + +const GoBack = ({ route }: Props) => { + const navigation = useNavigation() + + const handlePress = () => { + route ? navigation.navigate(route, {}) : navigation.goBack() + } + + return ( + + + + + + ) +} + +export default memo(GoBack) + +const Container = styled.TouchableOpacity` + padding: 20px 0px; +` + +export const Spacer = styled.View` + flex: 2; +` + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.PRIMARY_TEXT_COLOR +}))`` + +const Background = styled.View` + background-color: ${(props: StyleProps) => + props.theme.PRIMARY_BACKGROUND_COLOR}; + padding: 20px; + justify-content: center; + align-items: center; + border-radius: 20px; + overflow: hidden; + max-width: 40px; + max-height: 40px; + box-shadow: ${(props: StyleProps) => props.theme.SHADOW}; +` + +export const GoBackContainer = styled.View` + padding: 0px 20px; +` diff --git a/src/components/Buttons/IconButton.tsx b/src/components/Buttons/IconButton.tsx new file mode 100644 index 0000000..6aa60a1 --- /dev/null +++ b/src/components/Buttons/IconButton.tsx @@ -0,0 +1,32 @@ +import React, { memo } from 'react' +import { View } from 'react-native' +import { IconBold } from '../iconRegular' +import ScalingButton from './ScalingButton' + +interface Props { + onPress: Function + icon: string + color: string + backgroundColor: string + analyticsEvent: string +} + +const IconButton = (props: Props) => { + const handlePress = () => { + props.onPress() + } + return ( + + + + + + ) +} + +export default memo(IconButton) diff --git a/src/components/Buttons/LinkingButton.tsx b/src/components/Buttons/LinkingButton.tsx new file mode 100644 index 0000000..083d108 --- /dev/null +++ b/src/components/Buttons/LinkingButton.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import styled from 'styled-components/native' +import TranslatedText from 'components/TranslatedText' +import colors from 'styles/colors' +import { fonts } from 'styles/themes' + +interface Props { + navigate: Function + link: Function + code?: string + loading: boolean + disabled?: boolean +} + +const LinkingButton = ({ navigate, link, code, loading, disabled }: Props) => { + const handlePress = () => { + code ? link() : navigate() + } + return ( + + + + ) +} + +export default LinkingButton + +interface ButtonProps { + readonly disabled?: boolean + readonly white?: boolean +} + +const Button = styled.View` + border-radius: 5px; + border-color: ${(props) => + props.white ? colors.radiantBlue : 'transparent'}; + border-width: 1px; + padding: 15px; + min-width: 150px; + margin-bottom: 10px; + width: auto; + align-items: center; + background-color: ${(props) => + props.white ? colors.white : colors.radiantBlue}; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.2 : 1)}; +` + +const ButtonText = styled(TranslatedText)` + font-family: ${fonts.medium}; + color: ${(props) => (props.white ? colors.radiantBlue : colors.white)}; + font-size: 15px; + text-transform: uppercase; + text-align: center; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.5 : 1)}; +` + +const Loading = styled.ActivityIndicator`` + +const Touchable = styled.TouchableOpacity`` diff --git a/src/components/Buttons/LoginButton.tsx b/src/components/Buttons/LoginButton.tsx new file mode 100644 index 0000000..f5b06af --- /dev/null +++ b/src/components/Buttons/LoginButton.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { useNavigation } from '@react-navigation/core' +import { PrimaryButton } from './PrimaryButton' +import ROUTE from '../../config/routes/Routes' + +const Login = () => { + const navigation = useNavigation() + + const navigateToLogin = () => { + navigation.navigate('Auth', { screen: ROUTE.LOGIN }) + } + + return +} + +export default Login diff --git a/src/components/Buttons/PrimaryButton.tsx b/src/components/Buttons/PrimaryButton.tsx new file mode 100644 index 0000000..2277d52 --- /dev/null +++ b/src/components/Buttons/PrimaryButton.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' +import ScalingButton from './ScalingButton' + +interface PrimaryButton { + disabled?: boolean + onPress: () => void + title: string + white?: boolean + loading?: boolean +} + +export const PrimaryButton: React.SFC = (props) => { + const { loading, white, disabled, title, onPress } = props + return ( + + + + ) +} + +interface ButtonProps { + readonly disabled?: boolean + readonly white?: boolean +} + +const Button = styled.View` + border-radius: 5px; + border-color: ${(props) => + props.white ? colors.radiantBlue : 'transparent'}; + border-width: 1px; + padding: 15px; + min-width: 150px; + margin-bottom: 10px; + width: auto; + align-items: center; + background-color: ${(props) => + props.white ? colors.white : colors.radiantBlue}; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.2 : 1)}; +` + +const ButtonText = styled(TranslatedText)` + font-family: ${fonts.medium}; + color: ${(props) => (props.white ? colors.radiantBlue : colors.white)}; + font-size: 15px; + text-align: center; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.5 : 1)}; +` + +const Loading = styled.ActivityIndicator`` diff --git a/src/components/Buttons/RatingButton.tsx b/src/components/Buttons/RatingButton.tsx new file mode 100644 index 0000000..249036a --- /dev/null +++ b/src/components/Buttons/RatingButton.tsx @@ -0,0 +1,67 @@ +import React, { FC } from 'react' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { rateDay } from '../../actions/sleep/sleep-data-actions' +import { fonts } from '../../styles/themes' +import IconBold from '../iconBold' +import TranslatedText from '../TranslatedText' +import ScalingButton from './ScalingButton' + +type Props = { + selected: boolean + value: number + title: string + icon: string + color: string +} + +const RatingButton: FC = ({ selected, value, title, icon, color }) => { + const dispatch = useDispatch() + + const handlePress = () => { + dispatch(rateDay(value)) + } + + return ( + + + + + + + {title} + + + + ) +} + +export default RatingButton + +const Container = styled.View` + flex: 1; +` + +const Text = styled(TranslatedText)` + margin-top: 5px; + text-align: center; + font-size: 15px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; +` + +const ButtonContents = styled.View` + align-items: center; + margin-bottom: 10px; +` + +type IconProps = { + readonly color: string +} + +const IconContainer = styled.View` + background-color: ${({ color }) => color}; + border-radius: 30px; +` diff --git a/src/components/Buttons/ScalingButton.tsx b/src/components/Buttons/ScalingButton.tsx new file mode 100644 index 0000000..f72a54d --- /dev/null +++ b/src/components/Buttons/ScalingButton.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import ReactNativeHapticFeedback from 'react-native-haptic-feedback' +import Analytics from 'appcenter-analytics' +import { + GestureResponderEvent, + Animated, + Easing, + TouchableWithoutFeedback +} from 'react-native' +import styled from 'styled-components/native' +import Intercom from 'react-native-intercom' + +interface ScalingButtonProps { + onPress: (event: GestureResponderEvent) => void + analyticsEvent: string + disabled?: boolean + children: React.ReactNode + noDefaultStyles?: boolean + styles?: any +} + +const ScalingButton = (props: ScalingButtonProps) => { + const scaleValue = new Animated.Value(0) + const [isPressed, setPressed] = React.useState(false) + + const scale = () => { + scaleValue.setValue(0) + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + speed: 60, + bounciness: 25 + }).start() + } + + const onPressIn = () => { + scale() + } + + const onPress = (event: any) => { + Analytics.trackEvent('Button pressed', { + buttonType: props.analyticsEvent + }) + Intercom.logEvent('Button pressed', { + buttonType: props.analyticsEvent + }) + setPressed(true) + ReactNativeHapticFeedback.trigger('impactLight', { + enableVibrateFallback: true + }) + + props.onPress(event) + } + + const onPressOut = () => { + Animated.timing(scaleValue, { + toValue: 0, + duration: 200, + easing: Easing.ease, + useNativeDriver: true + }).start() + + setPressed(false) + } + + const buttonScaleIn = scaleValue.interpolate({ + inputRange: [0, 1], + outputRange: [1, 0.9] + }) + + const buttonScaleOut = scaleValue.interpolate({ + inputRange: [0, 1], + outputRange: [0.9, 1] + }) + + return ( + + + + ) +} + +const Button = styled(Animated.View)`` + +export default React.memo(ScalingButton) diff --git a/src/components/Buttons/SecondaryButton.tsx b/src/components/Buttons/SecondaryButton.tsx new file mode 100644 index 0000000..0fe911c --- /dev/null +++ b/src/components/Buttons/SecondaryButton.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' +import ScalingButton from './ScalingButton' + +interface Props { + disabled?: boolean + onPress: () => void + title: string + white?: boolean +} + +export const SecondaryButton: React.SFC = (props) => { + return ( + + + + ) +} + +interface ButtonProps { + readonly disabled?: boolean + readonly white?: boolean +} + +const Button = styled.View` + border-radius: 5px; + padding: 15px; + min-width: 150px; + margin-bottom: 10px; + width: auto; + align-items: center; + background-color: ${(props) => + props.white ? colors.white : colors.radiantBlue}; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.2 : 1)}; +` + +const ButtonText = styled(TranslatedText)` + font-family: ${fonts.medium}; + color: ${(props) => (props.white ? colors.radiantBlue : colors.white)}; + font-size: 15px; + text-align: center; + opacity: ${(props: ButtonProps) => (props.disabled ? 0.5 : 1)}; +` diff --git a/src/components/Buttons/TerveystaloButton.tsx b/src/components/Buttons/TerveystaloButton.tsx new file mode 100644 index 0000000..bae94a0 --- /dev/null +++ b/src/components/Buttons/TerveystaloButton.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { SvgCss } from 'react-native-svg' +import styled from 'styled-components/native' +import TerveystaloLogo from '../../../assets/terveystalo-logo.svg' + +const TerveystaloButton = () => { + return ( + + + + ) +} + +export default TerveystaloButton + +const Container = styled.TouchableOpacity` + padding: 10px; + border-color: ${({ theme }) => theme.PRIMARY_BUTTON_COLOR}; + border-width: 1px; + border-radius: 10px; + background-color: ${({ theme }) => theme.PRIMARY_BACKGROUND_COLOR}; +` diff --git a/src/components/Buttons/TextButton.tsx b/src/components/Buttons/TextButton.tsx new file mode 100644 index 0000000..0282fe9 --- /dev/null +++ b/src/components/Buttons/TextButton.tsx @@ -0,0 +1,43 @@ +import React, { memo } from 'react' +import { TouchableOpacity } from 'react-native' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import TranslatedText from '../TranslatedText' + +interface TextButtonProps { + center?: boolean + children: any + style?: any + onPress: Function +} + +const TextButton = (props: TextButtonProps) => { + const handlePress = () => { + props.onPress() + } + return ( + + + {props.children} + + + ) +} + +const Container = styled.View` + padding: 5px; + margin-left: 20px; + margin-right: 20px; +` + +interface TextProps { + readonly center?: boolean +} + +const Text = styled(TranslatedText)` + font-size: 17px; + color: ${colors.radiantBlue}; + text-align: ${(props) => (props.center ? 'center' : 'left')}; +` + +export default memo(TextButton) diff --git a/src/components/Buttons/backButton.tsx b/src/components/Buttons/backButton.tsx new file mode 100644 index 0000000..f88d293 --- /dev/null +++ b/src/components/Buttons/backButton.tsx @@ -0,0 +1,55 @@ +import { useNavigation } from '@react-navigation/native' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import ScalingButton from './ScalingButton' +import IconBold from '../iconBold' +import TranslatedText from '../TranslatedText' + +interface BackButtonInterface { + title?: string + dark?: boolean + contentContainerStyle?: any + route?: string +} + +const BackButton = (props: BackButtonInterface) => { + const navigation = useNavigation() + + const handlePress = () => { + props.route ? navigation.navigate(props.route, {}) : navigation.goBack() + } + + return ( + + + + ) +} + +export default memo(BackButton) + +const Container = styled(ScalingButton)` + flex: 1; +` + +const Button = styled.View` + flex-direction: row; +` + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.PRIMARY_TEXT_COLOR +}))`` + +interface TextProps extends StyleProps { + readonly dark?: boolean +} +const Text = styled(TranslatedText)` + margin-left: 5px; + font-size: 15px; + font-family: ${fonts.medium}; + color: ${(props: TextProps) => props.theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 0000000..3dac921 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,23 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { StyleProps } from '../styles/themes' + +interface CardProps { + children: JSX.Element[] | JSX.Element +} + +const Card = (props: CardProps) => ( + {props.children} +) + +export default memo(Card) + +const CardContainer = styled.View` + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + padding: 10px 10px; + box-shadow: 1px 1px 5px rgba(32, 33, 37, 0.1); + z-index: 1; + flex: 1; + border-radius: 5px; +` diff --git a/src/components/Challenge/ChallengeItem.tsx b/src/components/Challenge/ChallengeItem.tsx new file mode 100644 index 0000000..1d84894 --- /dev/null +++ b/src/components/Challenge/ChallengeItem.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { Dimensions, Text, View } from 'react-native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import { Challenge } from '../../Types/ChallengeState' +import ScalingButton from '../Buttons/ScalingButton' +import { IconBold } from '../iconRegular' + +const { width } = Dimensions.get('window') + +const ChallengeItem = ({ challenge }: { challenge: Challenge }) => { + const onPress = () => {} + + return ( + + + + + + + + {challenge.titleFI} + + {/* + {challenge.descFI} + */} + + + + ) +} + +export default React.memo(ChallengeItem) diff --git a/src/components/Charts/Axis.tsx b/src/components/Charts/Axis.tsx new file mode 100644 index 0000000..3dd87e2 --- /dev/null +++ b/src/components/Charts/Axis.tsx @@ -0,0 +1,80 @@ +import React, { Component } from 'react' +import { G, Line, Path, Rect, Text } from 'react-native-svg' +import PropTypes from 'prop-types' +import d3 from 'd3' + +const Axis = (props) => { + // static propTypes = { + // width: PropTypes.number.isRequired, + // ticks: PropTypes.number.isRequired, + // x: PropTypes.number, + // y: PropTypes.number, + // startVal: PropTypes.oneOfType([ + // React.PropTypes.number, + // React.PropTypes.object + // ]), + // endVal: PropTypes.oneOfType([ + // React.PropTypes.number, + // React.PropTypes.object + // ]), + // vertical: PropTypes.bool, + // scale: PropTypes.func // if scale is specified use that scale + // } + + let { width, ticks, x, y, startVal, endVal, vertical } = props + const TICKSIZE = width / 35 + x = x || 0 + y = y || 0 + const endX = vertical ? x : x + width + const endY = vertical ? y - width : y + let { scale } = props + if (!scale) { + scale = typeof startVal === 'number' ? d3.scaleLinear() : d3.scaleTime() + scale.domain(vertical ? [y, endY] : [x, endX]).range([startVal, endVal]) + } + const tickPoints = vertical + ? getTickPoints(vertical, y, endY, ticks) + : getTickPoints(vertical, x, endX, ticks) + + return ( + + + {tickPoints.map((pos) => ( + + ))} + {tickPoints.map((pos) => ( + + aa + + ))} + + ) + + function getTickPoints(vertical, start, end, numTicks) { + const res = [] + const ticksEvery = Math.floor(props.width / (numTicks - 1)) + if (vertical) { + for (let cur = start; cur >= end; cur -= ticksEvery) res.push(cur) + } else { + for (let cur = start; cur <= end; cur += ticksEvery) res.push(cur) + } + return res + } +} + +export default Axis diff --git a/src/components/Charts/HeartRateChart.tsx b/src/components/Charts/HeartRateChart.tsx new file mode 100644 index 0000000..3d9a000 --- /dev/null +++ b/src/components/Charts/HeartRateChart.tsx @@ -0,0 +1,245 @@ +import * as d3 from 'd3' +import Moment from 'moment' +import React, { useEffect, useState } from 'react' +import { Dimensions, StyleSheet, View } from 'react-native' +import appleHealthKit from 'react-native-healthkit' +import Svg, { G, Path, Rect, Text as SvgText } from 'react-native-svg' +import colors from '../../styles/colors' +import EmptyState from '../EmptyState' + +const { width } = Dimensions.get('window') +const PaddingSize = 20 +const TickWidth = PaddingSize * 2 + +interface HeartRateChartProps { + startDate: string + endDate: string + data: { + samples: [HeartRateSample?] + } +} + +interface HeartRateSample { + endDate: string + sourceId: string + sourceName: string + startDate: string + value: number +} + +const HeartRateChart = (props: HeartRateChartProps) => { + const [hrData, setHRdata] = useState() + + const timePaddedStart = Moment(props.startDate) + .subtract(1, 'hour') + .toISOString() + const timePaddedEnd = Moment(props.endDate).add(1, 'hour').toISOString() + + async function getData(startDate: string, endDate: string) { + const options = { + startDate: timePaddedStart, + endDate: timePaddedEnd + } + + let avgHeartRate + let samples + + await appleHealthKit.getHeartRateSamples( + options, + (err: any, response: [HeartRateSample]) => { + if (err) { + } + samples = response + avgHeartRate = d3.mean( + response, + (sample: HeartRateSample) => sample.value + ) + setHRdata(samples) + } + ) + } + + useEffect(() => { + getData(props.startDate, props.endDate) + }, []) + + if (!hrData || hrData.length === 0) { + return + } + + const xAccessor = (d: HeartRateSample) => new Date(d.startDate) + const yAccessor = (d: HeartRateSample) => d.value + + const chartWidth = width + const chartHeight = 200 + const scaleX = d3 + .scaleTime() + .domain([new Date(timePaddedStart), new Date(timePaddedEnd)]) + .range([0, chartWidth]) + + const allYValues = hrData.reduce((all, datum) => { + all.push(yAccessor(datum)) + return all + }, []) + const extentY = d3.extent(allYValues) + + const scaleY = d3 + .scaleLinear() + .domain([25, 125]) + // .nice() + // We invert our range so it outputs using the axis that React uses. + .range([chartHeight, 0]) + + const data = hrData + + const lineShape = d3 + .line() + .curve(d3.curveBasis) + .x((d) => scaleX(xAccessor(d))) + .y((d) => scaleY(yAccessor(d))) + + const yTicks = scaleY.ticks() + const xTicks = scaleX.ticks(d3.timeHour.every(1)) + + console.table(yTicks) + + return ( + + + {yTicks.map((tick, index) => ( + + {tick} + + ))} + + + + + + + {xTicks.map((tick, index) => { + return ( + + {Moment(tick).format('H')} + + ) + })} + + + ) +} + +export default HeartRateChart + +function createScaleX(start: string, end: string, width: number) { + return d3 + .scaleTime() + .domain([new Date(start), new Date(end)]) + .range([0, width]) +} + +function createScaleY(minY: number, maxY: number, height: number) { + return ( + d3 + .scaleLinear() + .domain([minY, maxY]) + .nice() + // We invert our range so it outputs using the axis that React uses. + .range([height, 0]) + ) +} + +interface createLineGraphProps { + data: [HeartRateSample] + xAccessor: Function + yAccessor: Function + width: number + height: number +} + +export function createLineGraph({ + data, + xAccessor, + yAccessor, + width, + height +}: createLineGraphProps) { + const lastDatum: HeartRateSample = data[data.length - 1] + const scaleX = createScaleX(data[0].startDate, lastDatum.startDate, width) + + // Collect all y values. + const allYValues = data.reduce((all, datum) => { + all.push(yAccessor(datum)) + return all + }, []) + + // Get the min and max y value. + const extentY = d3.extent(allYValues) + const scaleY = createScaleY(extentY[0], extentY[1], height) + + const lineShape = d3 + .line() + // .curve(shape.curveBasis) + .x((d) => scaleX(xAccessor(d))) + .y((d) => scaleY(yAccessor(d))) + + return { + data, + scale: { + x: scaleX, + y: scaleY + }, + path: lineShape(data), + ticks: { + x: scaleX.ticks(), + y: scaleY.ticks() + } + } +} + +const styles = StyleSheet.create({ + tickLabelX: { + position: 'absolute', + bottom: 0, + fontSize: 12, + textAlign: 'center' + }, + + ticksYContainer: { + position: 'absolute', + top: 0, + left: 0 + }, + + tickLabelY: { + position: 'absolute', + left: 0, + backgroundColor: 'transparent' + }, + + tickLabelYText: { + fontSize: 12, + textAlign: 'center' + }, + + ticksYDot: { + position: 'absolute', + width: 2, + height: 2, + backgroundColor: 'red', + borderRadius: 100 + } +}) diff --git a/src/components/Charts/ProfileChart.tsx b/src/components/Charts/ProfileChart.tsx new file mode 100644 index 0000000..a47d319 --- /dev/null +++ b/src/components/Charts/ProfileChart.tsx @@ -0,0 +1,53 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { constants, fonts, StyleProps } from '../../styles/themes' + +const data = [ + { + efficiency: 0.7, + duration: 1, + bedtime: 0.9, + jetlag: 0.67, + consistency: 0.8 + }, + { + efficiency: 0.6, + duration: 0.9, + bedtime: 0.8, + jetlag: 0.7, + consistency: 0.6 + } +] + +const ProfileChart = () => { + return ( + + + + {/* */} + + ) +} + +export default memo(ProfileChart) + +const PhotoContainer = styled.View` + margin: 0px 20px 20px; + height: 300px; + justify-content: center; + align-items: center; + padding: 20px; + border-bottom-color: ${(props: StyleProps) => props.theme.HAIRLINE_COLOR}; + border-bottom-width: ${constants.hairlineWidth}px; +` + +const Image = styled.Image` + flex: 1; + z-index: 0; + width: 100%; + height: 100%; +` + +const Explanation = styled.Text` + font-family: ${fonts.medium}; +` diff --git a/src/components/Charts/SleepScoreGraph.tsx b/src/components/Charts/SleepScoreGraph.tsx new file mode 100644 index 0000000..59b52b1 --- /dev/null +++ b/src/components/Charts/SleepScoreGraph.tsx @@ -0,0 +1,145 @@ +import * as d3 from 'd3' +import range from 'lodash/range' +import React from 'react' +import { Dimensions, StyleSheet, View } from 'react-native' +import Svg, { G, Line, Rect } from 'react-native-svg' +import colors from '../../styles/colors' + +const { width, height } = Dimensions.get('window') +const chartWidth = width - 40 +const chartHeight = 200 + +const SleepScoreGraph = (props) => { + const { data } = props + + if (!data) { + return null + } + + // X-Axis + const barWidth = 10 + const xDomain = d3.extent(data, (item) => new Date(item.date)) + + const xRange = [20, chartWidth - 20] + const x = d3.scaleLinear().domain(xDomain).range(xRange) + + // Y-Axis + const yDomain = [0, 100] + const yRange = [0, chartHeight] + const y = d3.scaleLinear().nice().domain(yDomain).range(yRange) + + const reverseY = d3.scaleLinear().domain(yDomain).range([chartHeight, 0]) + + // const movingAvgData = movingAvg(data, 2); + // const dataWithRunningAverage = data.map((item, key) => ({ + // ...item, + // rAvg: movingAvgData[key], + // })); + + // const runningAverage = d3 + // .line() + // .x(d => x(d.key)) + // .y(d => reverseY(d.rAvg ? d.rAvg : 0))(dataWithRunningAverage); + + const yAxisScale = d3.scaleLinear().domain([0, 10]).range(yRange) + + const yAxes = range(11).map((item) => ( + + + + )) + + return ( + + + {yAxes} + {data.map((item) => ( + + ))} + {/* */} + {/* {data.map(item => ( + + {item.key} + + ))} */} + + + ) +} + +const c = StyleSheet.create({ + chartContainer: { + flex: 1, + marginHorizontal: 20, + alignItems: 'center', + justifyContent: 'center' + } +}) + +export default SleepScoreGraph + +/** + * returns an array with moving average of the input array + * @param array - the input array + * @param count - the number of elements to include in the moving average calculation + * @param qualifier - an optional function that will be called on each + * value to determine whether it should be used + */ +function movingAvg(array, count, qualifier) { + // calculate average for subarray + const avg = (array, qualifier) => { + let sum = 0 + let count = 0 + let value + for (const i in array) { + value = array[i].value + if (!qualifier || qualifier(value)) { + sum += value + count++ + } + } + + return sum / count + } + + const result = [] + let val + + // pad beginning of result with null values + + // calculate average for each subarray and add to result + for (let i = 0, len = array.length - count; i <= len; i++) { + val = avg(array.slice(i, i + count), qualifier) + + if (isNaN(val)) { + result.push(null) + } else { + result.push(val) + } + } + + return result +} diff --git a/src/components/Charts/SleepTimeChart/BottomInfo.tsx b/src/components/Charts/SleepTimeChart/BottomInfo.tsx new file mode 100644 index 0000000..db7394d --- /dev/null +++ b/src/components/Charts/SleepTimeChart/BottomInfo.tsx @@ -0,0 +1,18 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../../styles/themes' +import TranslatedText from '../../TranslatedText' + +const BottomInfo = () => { + return PLEASE_SELECT_DATE +} + +export default memo(BottomInfo) + +const Text = styled(TranslatedText)` + align-self: center; + text-align: center; + font-family: ${fonts.medium}; + font-size: 15px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/Charts/SleepTimeChart/DayInfo.tsx b/src/components/Charts/SleepTimeChart/DayInfo.tsx new file mode 100644 index 0000000..f6ee6ad --- /dev/null +++ b/src/components/Charts/SleepTimeChart/DayInfo.tsx @@ -0,0 +1,68 @@ +import moment from 'moment' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { Day } from 'Types/Sleepdata' +import translate from '../../../config/i18n' +import { minutesToHoursString } from '../../../helpers/time' +import colors from '../../../styles/colors' +import { fonts, StyleProps } from '../../../styles/themes' + +interface Props { + selectedDay: Day +} + +const DayInfo = (props: Props) => { + return ( + + + {moment(props.selectedDay.date).format('DD. MMM YYYY')} + + + + + {translate('TIME_IN_BED')}{' '} + {minutesToHoursString(props.selectedDay.inBedDuration)} + + + {translate('TIME_ASLEEP')}{' '} + {minutesToHoursString(props.selectedDay.asleepDuration)} + + + + ) +} + +export default memo(DayInfo) + +const Container = styled.View` + flex-direction: row; + flex: 1; +` + +const TimeInBed = styled.Text` + font-family: ${fonts.bold}; + font-size: 15px; + color: ${(props: StyleProps) => colors.inBedColor}; +` + +const TimeAsleep = styled.Text` + font-family: ${fonts.bold}; + font-size: 15px; + color: ${(props: StyleProps) => colors.asleepColor}; +` + +const TodayContainer = styled.View` + align-items: flex-start; + justify-content: center; + flex: 1; +` + +const Today = styled.Text` + font-family: ${fonts.bold}; + font-size: 15px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const Info = styled.View` + flex-direction: column; +` diff --git a/src/components/Charts/SleepTimeChart/SleepBars.tsx b/src/components/Charts/SleepTimeChart/SleepBars.tsx new file mode 100644 index 0000000..c5c8308 --- /dev/null +++ b/src/components/Charts/SleepTimeChart/SleepBars.tsx @@ -0,0 +1,60 @@ +import { ScaleTime } from 'd3' +import moment from 'moment' +import React, { memo, useMemo } from 'react' +import { G, Rect } from 'react-native-svg' +import { Day, Night, Value } from 'Types/Sleepdata' +import colors from '../../../styles/colors' + +interface Props { + data: Day[] + type: Value + scaleX: ScaleTime + scaleY: ScaleTime + barWidth: number + select: Function +} + +const SleepBars = (props: Props) => { + const color = + props.type === Value.Asleep ? colors.radiantBlue : colors.inBedColor + + const { bars } = useMemo( + () => ({ + bars: props.data.map((datum, index) => { + const select = () => { + props.select(datum) + } + + const dayBars = datum.night + .filter((night) => night.value === props.type) + .map((item: Night, i: number) => { + const y = props.scaleY(moment(item.startDate).valueOf()) + const height = + props.scaleY(moment(item.endDate).valueOf()) - + props.scaleY(moment(item.startDate).valueOf()) + + return ( + + + + ) + }) + + return {dayBars} + }) + }), + [props.data] + ) + + return {bars} +} + +export default memo(SleepBars) diff --git a/src/components/Charts/SleepTimeChart/SleepTimeChartAverage.tsx b/src/components/Charts/SleepTimeChart/SleepTimeChartAverage.tsx new file mode 100644 index 0000000..818c60c --- /dev/null +++ b/src/components/Charts/SleepTimeChart/SleepTimeChartAverage.tsx @@ -0,0 +1,51 @@ +import React, { memo } from 'react' +import { G, Line, Text as SvgText } from 'react-native-svg' +import { minutesToHoursString } from '../../../helpers/time' +import colors from '../../../styles/colors' + +interface SleepTimeChartAverageProps { + averageBedTime: number + chartWidth: number + average: number +} + +const SleepTimeChartAverage = (props: SleepTimeChartAverageProps) => { + if (!props.averageBedTime || !props.average) { + return null + } + return ( + + + + {minutesToHoursString(props.average)} + + + Sleep + + + ) +} + +export default memo(SleepTimeChartAverage) diff --git a/src/components/Charts/SleepTimeChart/XTicks.tsx b/src/components/Charts/SleepTimeChart/XTicks.tsx new file mode 100644 index 0000000..742d378 --- /dev/null +++ b/src/components/Charts/SleepTimeChart/XTicks.tsx @@ -0,0 +1,54 @@ +import { ScaleTime } from 'd3' +import moment from 'moment' +import React, { memo } from 'react' +import { G, Text } from 'react-native-svg' +import { useSelector } from 'react-redux' +import styled from 'styled-components/native' +import { getTextColorOnTheme } from '../../../store/Selectors/UserSelectors' +import { fonts } from '../../../styles/themes' + +type Props = { + scaleX: ScaleTime + chartHeight: number + barWidth: number + ticks: Date[] +} + +const XTicks = ({ scaleX, chartHeight, barWidth, ticks }: Props) => { + const color = useSelector(getTextColorOnTheme) + const tickElements = ticks.map((tick, index) => { + const x = scaleX(tick) + barWidth / 2 + + return ( + + + {moment(tick).format('ddd')} + + + {moment(tick).format('DD')} + + + ) + }) + + return {tickElements} +} + +const Day = styled(Text).attrs(({ theme }) => ({ + fill: theme.PRIMARY_TEXT_COLOR +}))`` + +const LongDate = styled(Text).attrs(({ theme }) => ({ + fill: theme.PRIMARY_TEXT_COLOR +}))`` + +export default memo(XTicks) diff --git a/src/components/Charts/SleepTimeChart/YTicks.tsx b/src/components/Charts/SleepTimeChart/YTicks.tsx new file mode 100644 index 0000000..73eb5a5 --- /dev/null +++ b/src/components/Charts/SleepTimeChart/YTicks.tsx @@ -0,0 +1,48 @@ +import { ScaleTime } from 'd3' +import moment from 'moment' +import React, { memo } from 'react' +import { G, Line, Text } from 'react-native-svg' +import { useSelector } from 'react-redux' +import styled from 'styled-components/native' +import { StyleProps } from '../../../styles/themes' +import { getTextColorOnTheme } from '../../../store/Selectors/UserSelectors' + +interface Props { + chartWidth: number + scaleY: ScaleTime + ticks: Date[] +} + +const YTicks = (props: Props) => { + const color = useSelector(getTextColorOnTheme) + + const ticks = props.ticks.map((tick, index) => { + return ( + + + + {moment(tick).format('HH:mm')} + + + ) + }) + + return {ticks} +} + +export default memo(YTicks) + +const StyledText = styled(Text).attrs((props: StyleProps) => ({ + fill: props.theme.PRIMARY_TEXT_COLOR +}))`` diff --git a/src/components/Charts/SleepTimesChart.tsx b/src/components/Charts/SleepTimesChart.tsx new file mode 100644 index 0000000..865a239 --- /dev/null +++ b/src/components/Charts/SleepTimesChart.tsx @@ -0,0 +1,272 @@ +import * as d3 from 'd3' +import React, { Component } from 'react' +import { + Animated, + Dimensions, + StyleSheet, + Text, + TextInput, + View +} from 'react-native' +import Svg, { Defs, Line, LinearGradient, Path, Stop } from 'react-native-svg' +import * as path from 'svg-path-properties' +import { minutesToHoursString } from '../../helpers/time' +import colors from '../../styles/colors' +import TranslatedText from '../TranslatedText' + +const { width } = Dimensions.get('window') +const cardWidth = width - 40 +const rightAxis = 40 +const linesWidth = cardWidth - rightAxis +const height = 200 + +const verticalPadding = 5 +const cursorRadius = 5 + +class SleepTimesChart extends Component { + constructor(props) { + super(props) + + this.cursor = React.createRef() + this.label = React.createRef() + this.scaleX + this.scaleY + this.scaleLabel + this.line + this.properties + + this.state = { + x: new Animated.Value(0) + } + } + + moveCursor(value) { + const { x, y } = this.properties.getPointAtLength(this.lineLength - value) + if (this.cursos) { + this.cursor.current.setNativeProps({ + top: y - cursorRadius, + left: x - cursorRadius + }) + } + const label = this.scaleLabel(this.scaleY.invert(y)) + this.label.current.setNativeProps({ + text: `${minutesToHoursString(label)} ` + }) + } + + calculateScales() { + const yDomainBed = d3.extent(this.props.days, (day) => day.inBedDuration) + const yDomainScore = d3.extent(this.props.sleepScores, (item) => item.score) + const xDomain = d3.extent(this.props.days, (day) => new Date(day.date)) + + this.axisXScale = d3.scaleLinear().domain([0, 6]).range([0, linesWidth]) + + this.axisYScale = d3.scaleLinear().domain(yDomainBed).range([0, height]) + + this.scaleX = d3.scaleLinear().domain(xDomain).range([0, linesWidth]) + + this.scaleY = d3 + .scaleTime() + .domain(yDomainBed) + .range([height - verticalPadding, verticalPadding]) + + this.scoreScaleY = d3 + .scaleTime() + .domain(yDomainScore) + .range([height - verticalPadding, verticalPadding]) + + this.scaleLabel = d3.scaleLinear().domain(yDomainBed).range(yDomainBed) + + this.line = d3 + .line() + .x((d) => this.scaleX(new Date(d.date))) + .y((d) => this.scaleY(d.inBedDuration ? d.inBedDuration : 0))( + this.props.days + ) + + this.sleepLine = d3 + .line() + .x((d) => this.scaleX(new Date(d.date))) + .y((d) => this.scaleY(d.asleepDuration ? d.asleepDuration : 0))( + this.props.days + ) + + this.scoreLine = d3 + .line() + .x((d) => this.scaleX(new Date(d.date))) + .y((d) => this.scoreScaleY(d.score ? d.score : 0))(this.props.sleepScores) + + this.properties = path.svgPathProperties(this.line) + this.lineLength = this.properties.getTotalLength() + } + + componentDidMount() { + this.calculateScales() + this.state.x.addListener(({ value }) => this.moveCursor(value)) + this.moveCursor(0) + } + + shouldComponentUpdate(nextProps, nextState) { + return true + } + + render() { + if (!this.props.days || this.props.sleepScores) return null + this.calculateScales() + const { x } = this.state + const translateX = x.interpolate({ + inputRange: [0, this.lineLength], + outputRange: [cardWidth, 0], + extrapolate: 'clamp' + }) + + const xAxis = this.props.days.map((item, key) => ( + + )) + + const yAxis = this.props.days.map((item, key) => ( + + )) + + return ( + + + Sleep Goal Trend + + + + {xAxis} + {yAxis} + + + + + + + + + + + + + + + + + + + + + + This here is a chart of your time in bed and asleep. In optimal case + the lines should be the same. + + + + ) + } +} + +export default SleepTimesChart + +const cStyles = StyleSheet.create({ + root: { + flex: 1 + }, + container: { + alignItems: 'center', + height + }, + cursor: { + position: 'absolute', + zIndex: 4, + width: cursorRadius * 2, + height: cursorRadius * 2, + borderRadius: cursorRadius, + borderColor: '#367be2', + borderWidth: 3 + }, + label: { + position: 'absolute', + bottom: 0, + left: 0, + backgroundColor: 'red' + }, + labelText: { + textAlign: 'center' + }, + sectionContainer: { + borderRadius: 5, + marginHorizontal: 20, + marginBottom: 50, + backgroundColor: 'white', + ...shadowStyle + }, + chart: { + height: 200 + }, + subTitle: { + marginLeft: 20, + fontSize: 15, + color: colors.gray2 + }, + + dateContainer: { + flex: 1, + justifyContent: 'space-between' + }, + legend: { + marginVertical: 20, + marginHorizontal: 10 + } +}) diff --git a/src/components/Charts/sleepTimeChart.tsx b/src/components/Charts/sleepTimeChart.tsx new file mode 100644 index 0000000..8385f35 --- /dev/null +++ b/src/components/Charts/sleepTimeChart.tsx @@ -0,0 +1,162 @@ +import * as d3 from 'd3' +import moment from 'moment' +import React, { memo, useMemo, useState } from 'react' +import { Dimensions, View } from 'react-native' +import { ScrollView } from 'react-native-gesture-handler' +import Svg from 'react-native-svg' +import { useSelector } from 'react-redux' +import styled from 'styled-components/native' +import { constants, StyleProps } from '../../styles/themes' +import { getAllDays } from '../../store/Selectors/SleepDataSelectors' +import { getIsDarkMode } from '../../store/Selectors/UserSelectors' +import { Day, Value } from '../../Types/Sleepdata' +import { Container, H3 } from '../Primitives/Primitives' +import BottomInfo from './SleepTimeChart/BottomInfo' +import DayInfo from './SleepTimeChart/DayInfo' +import SleepBars from './SleepTimeChart/SleepBars' +import XTicks from './SleepTimeChart/XTicks' +import YTicks from './SleepTimeChart/YTicks' + +const { height, width } = Dimensions.get('window') + +export const barWidth = width / 12 +export const paddingLeft = 100 +export const paddingRight = 100 +export const chartHeight = height / 3 + +const SleepTimeChart = () => { + const days = useSelector(getAllDays) + const [selectedDay, setSelectedDay] = useState() + + const chartWidth = (barWidth + 10) * days.length + paddingLeft + paddingRight + + const { normalizedSleepData }: any = useMemo( + () => ({ + normalizedSleepData: normalizeSleepData(days, Value.InBed) + }), + [] + ) + + const select = (day: Day) => { + setSelectedDay(day) + } + + const xDomain = d3.extent( + normalizedSleepData, + (day: Day) => new Date(day.date) + ) + const yDomain: any = [ + d3.min(normalizedSleepData, (datum: Day) => + d3.min(datum.night, (night) => + moment(night.startDate).subtract(1, 'hour').valueOf() + ) + ), + d3.max(normalizedSleepData, (datum: Day) => + d3.max(datum.night, (night) => + moment(night.endDate).add(1, 'hour').valueOf() + ) + ) + ] + + const scaleX = d3.scaleTime().domain(xDomain).range([paddingLeft, chartWidth]) + + const scaleY = d3 + .scaleTime() + .domain(yDomain) + .nice() + .range([0, chartHeight - 50]) + const yTicks = scaleY.ticks(4) + const xTicks = scaleX.ticks(d3.timeDay.every(1)) + + return ( + <> + +

Sleep Goal Trend

+
+ + + + + + + + + + + + + + + + {selectedDay ? : } + + + ) +} + +export default memo(SleepTimeChart) + +const Stats = styled.View` + padding: 10px 20px; + height: 60px; + border-top-color: ${(props: StyleProps) => props.theme.HAIRLINE_COLOR}; + border-top-width: ${constants.hairlineWidth}px; + border-bottom-color: ${(props: StyleProps) => props.theme.HAIRLINE_COLOR}; + border-bottom-width: ${constants.hairlineWidth}px; +` + +const ScrollContainer = styled.View`` + +const YTicksContainer = styled(Svg)` + position: absolute; +` + +const normalizeSleepData = (days: Day[], value: Value) => { + const normalized = days.map((day) => { + const normalizedNights = day.night.map((night) => { + const trueDate = moment(day.date) + + const startDifference = moment.duration( + moment(night.startDate).diff(trueDate.startOf('day')) + ) + const newStartDate = moment().startOf('day').add(startDifference) + + const newEndDate = moment(newStartDate).add( + night.totalDuration, + 'minutes' + ) + + return { + ...night, + startDate: newStartDate.valueOf(), + endDate: newEndDate.valueOf() + } + }) + + return { ...day, night: normalizedNights } + }) + + return normalized +} diff --git a/src/components/ChronotypeTest/Chronotype.tsx b/src/components/ChronotypeTest/Chronotype.tsx new file mode 100644 index 0000000..675272e --- /dev/null +++ b/src/components/ChronotypeTest/Chronotype.tsx @@ -0,0 +1,241 @@ +import React, { memo, useRef, useState } from 'react' +import { Dimensions } from 'react-native' +import { FlatList, TouchableOpacity } from 'react-native-gesture-handler' +import Modal from 'react-native-modal' +import Animated from 'react-native-reanimated' +import styled from 'styled-components/native' +import keyExtractor from '../../helpers/KeyExtractor' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import { CheckBox, H4, P, SafeAreaView } from '../Primitives/Primitives' +import TestEnd from './TestEnd' +import TestStart from './TestStart' + +const { width, height } = Dimensions.get('window') + +const cardWidth = width - 40 +const cardMargin = 5 + +const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) +const content = [ + { + question: 'CHRONOTYPE_Q1', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q1_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q1_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q1_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q1_A4' } + ] + }, + { + question: 'CHRONOTYPE_Q2', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q2_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q2_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q2_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q2_A4' } + ] + }, + { + question: 'CHRONOTYPE_Q3', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q3_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q3_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q3_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q3_A4' } + ] + }, + { + question: 'CHRONOTYPE_Q4', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q4_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q4_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q4_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q4_A4' } + ] + }, + { + question: 'CHRONOTYPE_Q5', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q5_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q5_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q5_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q5_A4' } + ] + }, + { + question: 'CHRONOTYPE_Q6', + answers: [ + { weight: 1, answer: 'CHRONOTYPE_Q6_A1' }, + { weight: 2, answer: 'CHRONOTYPE_Q6_A2' }, + { weight: 3, answer: 'CHRONOTYPE_Q6_A3' }, + { weight: 4, answer: 'CHRONOTYPE_Q6_A4' } + ] + } +] + +const answerArray = [1, 2, 3, 4, 1, 2] + +const xOffset = new Animated.Value(0) + +// const transitionAnimation = (index: number) => ({ +// transform: [ +// { perspective: 800 }, +// { +// scale: xOffset.interpolate({ +// inputRange: [(index - 1) * width, index * width, (index + 1) * width], +// outputRange: [0.9, 1, 0.9] +// }) +// } +// ] +// }); + +const ChronotypeTest = () => { + const [answers, setAnswer] = useState([]) + const flatListRef: any = useRef(0) + const [currentIndex, setCurrentIndex] = useState(0) + + const startTest = () => { + scrollToItem(1) + } + + const scrollToItem = (index: number) => { + return flatListRef.current.scrollToIndex({ index, animated: true }) + } + + const nextSlide = () => { + if (currentIndex + 1 < content.length) { + setCurrentIndex(currentIndex + 1) + scrollToItem(currentIndex) + } + } + + const renderItem = ({ item, index }: { item: any; index: number }) => { + const answers = item.answers.map((item: any, subIndex: number) => ( + {}}> + + + + {item.answer} + + + + )) + return ( + + Question {index + 1} +

{item.question}

+ + {answers} +
+ ) + } + + const inset = (width - cardWidth - cardMargin) / 2 + const snapOffets: number[] = content.map( + (item, index) => index * (cardWidth + cardMargin * 2) + ) + + return ( + + + {/* */} + + ({ + index, + length: cardWidth, + offset: (cardWidth + cardMargin * 2) * index + })} + key + contentContainerStyle={{ + paddingLeft: inset, + paddingRight: inset + }} + snapToOffsets={snapOffets} + scrollEventThrottle={16} + showsHorizontalScwrollIndicator={false} + horizontal + data={content} + ref={flatListRef} + decelerationRate="fast" + keyExtractor={keyExtractor} + snapToAlignment="center" + snapToEnd={false} + ListHeaderComponent={} + ListFooterComponent={} + renderItem={renderItem} + /> + + + + + + + + + + + + ) +} + +export default memo(ChronotypeTest) + +const CustomSafeAreaView = styled(SafeAreaView)` + background-color: ${colors.radiantBlue}; + align-items: center; + justify-content: center; +` + +const Answers = styled.View` + flex-grow: 1; +` + +const QuestionNumber = styled.Text` + font-family: ${fonts.bold}; + text-transform: uppercase; + color: ${(Props: StyleProps) => Props.theme.SECONDARY_TEXT_COLOR}; +` + +const AnswerCard = styled(Animated.View)` + background: ${(props: StyleProps) => props.theme.SECONDARY_BACKGROUND_COLOR}; + padding: 10px; + margin: 10px ${cardMargin}px; + border-radius: 10px; + width: ${cardWidth}px; +` + +const Answer = styled.TouchableOpacity`` +const AnswerInner = styled.View` + background: ${(props: StyleProps) => props.theme.SECONDARY_BACKGROUND_COLOR}; + margin: 10px 20px; + flex-direction: row; + align-items: center; +` + +interface AnswerProps { + readonly selected?: boolean +} +const AnswerOption = styled(P)` + font-family: ${(props: AnswerProps) => + props.selected ? fonts.bold : fonts.medium}; +` + +const BottomContainer = styled.View` + justify-content: space-between; + flex-direction: row; +` + +const StyledIcon = styled(IconBold)`` diff --git a/src/components/ChronotypeTest/ChronotypeBox.tsx b/src/components/ChronotypeTest/ChronotypeBox.tsx new file mode 100644 index 0000000..25cdbe0 --- /dev/null +++ b/src/components/ChronotypeTest/ChronotypeBox.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import styled from 'styled-components/native' +import { fonts } from '../../styles/themes' +import { Container } from '../Primitives/Primitives' + +const ChronotypeBox = () => { + return ( + + Your Chronotype + + ) +} + +export default ChronotypeBox + +const Title = styled.Text` + font-family: ${fonts.bold}; + font-size: 15px; +` diff --git a/src/components/ChronotypeTest/TestEnd.tsx b/src/components/ChronotypeTest/TestEnd.tsx new file mode 100644 index 0000000..93b22ea --- /dev/null +++ b/src/components/ChronotypeTest/TestEnd.tsx @@ -0,0 +1,24 @@ +import React, { memo } from 'react' +import { View, Text, Dimensions } from 'react-native' +import styled from 'styled-components/native' +import { H1, P, Container } from '../Primitives/Primitives' +import { PrimaryButton } from '../Buttons/PrimaryButton' + +const TestEnd = () => { + return ( + + + End + + + ) +} + +export default memo(TestEnd) + +const { width } = Dimensions.get('window') + +const Page = styled.View` + width: ${width}px; + flex: 1; +` diff --git a/src/components/ChronotypeTest/TestStart.tsx b/src/components/ChronotypeTest/TestStart.tsx new file mode 100644 index 0000000..66e0968 --- /dev/null +++ b/src/components/ChronotypeTest/TestStart.tsx @@ -0,0 +1,50 @@ +import React, { memo } from 'react' +import { Dimensions } from 'react-native' +import styled from 'styled-components/native' +import { fonts } from '../../styles/themes' +import { PrimaryButton } from '../Buttons/PrimaryButton' +import { Container } from '../Primitives/Primitives' +import TranslatedText from '../TranslatedText' + +interface Props { + start: Function +} + +const TestStart = (props: Props) => { + const startTest = () => { + props.start() + } + + return ( + + + CHRONOTYPE + CHRONOTYPE_INTRO + + + + ) +} + +export default memo(TestStart) + +const { width } = Dimensions.get('window') + +const Page = styled.View` + width: ${width}px; + flex: 1; +` + +const Intro = styled(TranslatedText)` + font-size: 17px; + font-family: ${fonts.medium}; + line-height: 30; + color: white; +` + +const Title = styled(TranslatedText)` + font-family: ${fonts.bold}; + color: white; + font-size: 30px; + margin-bottom: 20px; +` diff --git a/src/components/CoachingMonthCard/CoachingMonthCard.tsx b/src/components/CoachingMonthCard/CoachingMonthCard.tsx new file mode 100644 index 0000000..86e7c1a --- /dev/null +++ b/src/components/CoachingMonthCard/CoachingMonthCard.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { CoachingMonth } from 'typings/state/coaching-state' +import styled from 'styled-components/native' +import moment from 'moment' +import { fonts } from 'styles/themes' +import TranslatedText from 'components/TranslatedText' + +type Props = { + month: CoachingMonth +} + +const CoachingMonthCard = ({ month }: Props) => { + const title = moment(month.started).format('MMMM') + const startDate = moment(month.started).format('DD.MM.YYYY') + const endDate = month.ended ? moment(month.ended).format('DD.MM.YYYY') : '' + const weeks = month.weeks.length + const lessons = month.lessons?.length + + return ( + + {title} + + {startDate} – {endDate} + + + + COACHING_MONTH.WEEKS + COACHING_MONTH.LESSONS + + + ) +} + +export default CoachingMonthCard + +const Container = styled.View` + background-color: ${({ theme }) => theme.SECONDARY_BACKGROUND_COLOR}; + padding: 10px; + border-radius: 10px; + margin: 10px 0px; +` + +const Row = styled.View` + margin-top: 10px; + flex-direction: row; + justify-content: space-between; +` + +const Title = styled.Text` + font-size: 17px; + text-transform: uppercase; + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; + margin-bottom: 5px; +` + +const Started = styled.Text` + font-size: 15px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +const Stat = styled(TranslatedText)` + font-size: 13px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingMonthCard/index.tsx b/src/components/CoachingMonthCard/index.tsx new file mode 100644 index 0000000..3c97b3d --- /dev/null +++ b/src/components/CoachingMonthCard/index.tsx @@ -0,0 +1 @@ +export * from './CoachingMonthCard' diff --git a/src/components/CoachingSpecific/Authors.tsx b/src/components/CoachingSpecific/Authors.tsx new file mode 100644 index 0000000..98024ff --- /dev/null +++ b/src/components/CoachingSpecific/Authors.tsx @@ -0,0 +1,54 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { AuthorCard as AuthorCardType } from '../../Types/CoachingContentState' +import AuthorCard from '../LessonComponents/AuthorCard' +import LargeAuthorCard from '../LessonComponents/LargeAuthorCard' + +const AuthorsComponent = ({ + authorCards +}: { + authorCards?: AuthorCardType[] +}) => { + if (!authorCards) return null + + const mainAuthor = authorCards[0] + const onlyOneAuthor = authorCards?.length === 1 + + const authors = authorCards + .filter((item) => item.name !== mainAuthor.name) + .map((item, index) => { + return ( + + ) + }) + + return ( + + {mainAuthor ? ( + + ) : null} + {!onlyOneAuthor ? ( + + {authors} + + ) : null} + + ) +} + +export default memo(AuthorsComponent) + +const AuthorContainer = styled.View` + margin: 30px 20px 20px; +` + +const Authors = styled.ScrollView`` diff --git a/src/components/CoachingSpecific/BuyCoachingButton.tsx b/src/components/CoachingSpecific/BuyCoachingButton.tsx new file mode 100644 index 0000000..b1b2c62 --- /dev/null +++ b/src/components/CoachingSpecific/BuyCoachingButton.tsx @@ -0,0 +1,134 @@ +import { useNavigation } from '@react-navigation/core' +import React, { useEffect, useState } from 'react' +import Purchases from 'react-native-purchases' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import TranslatedText from '../TranslatedText' + +const BuyCoaching = () => { + const navigation = useNavigation() + const [price, setPrice]: any = useState(null) + + const getProducts = async () => { + try { + const offerings = await Purchases.getOfferings() + if ( + offerings.current !== null && + offerings.current.monthly !== undefined + ) { + setPrice(offerings.current.monthly?.product.price_string) + } + } catch (e) {} + } + + useEffect(() => { + getProducts() + }, []) + + const moveToPurchase = () => { + navigation.navigate('PurchaseView') + } + + return ( + + + + + + ) +} + +export default BuyCoaching + +const LeftColumn = styled.View` + flex: 1; +` +const RightColumn = styled.View` + flex: 1; + flex-direction: row; + align-items: center; + justify-content: flex-end; +` + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.SECONDARY_TEXT_COLOR +}))`` + +const Container = styled.View` + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 10px 20px; +` + +const AndroidContainer = styled.View` + elevation: 3; + align-items: center; + flex-direction: row; + justify-content: space-between; + border-radius: 5px; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; +` + +const Button = styled.TouchableOpacity` + border-radius: 5px; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + padding: 20px; + box-shadow: ${(props: StyleProps) => props.theme.SHADOW}; + flex-direction: row; +` + +const Text = styled(TranslatedText)` + flex: 1; + font-size: 18px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-family: ${fonts.bold}; +` + +const MiniText = styled(TranslatedText)` + flex: 1; + font-size: 11px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-family: ${fonts.bold}; +` +const PriceContainer = styled.View` + margin-right: 15px; + flex-direction: column; +` + +const Price = styled.Text` + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + font-size: 20px; +` + +const Monthly = styled(TranslatedText)` + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + font-size: 11px; +` + +const Loader = styled.ActivityIndicator`` diff --git a/src/components/CoachingSpecific/CoachingHeader.tsx b/src/components/CoachingSpecific/CoachingHeader.tsx new file mode 100644 index 0000000..e730e3f --- /dev/null +++ b/src/components/CoachingSpecific/CoachingHeader.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import styled from 'styled-components/native' +import { PageTitle, H3 } from '../Primitives/Primitives' +import WeekCarousel from './WeekCarousel' +import IntroduceCoaching from './IntroduceCoaching' + +const CoachingHeader = () => ( + <> + Coaching + + + GoalsTitle + +) + +export default CoachingHeader + +const PaddedH3 = styled(H3)` + margin: 30px 20px 10px; +` diff --git a/src/components/CoachingSpecific/CoachingNotStarted.tsx b/src/components/CoachingSpecific/CoachingNotStarted.tsx new file mode 100644 index 0000000..190d421 --- /dev/null +++ b/src/components/CoachingSpecific/CoachingNotStarted.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import styled from 'styled-components/native' +import { useSelector } from 'react-redux' +import { getCoachingNotStarted } from 'store/Selectors/coaching-selectors' +import { getActiveCoaching } from 'store/Selectors/subscription-selectors/SubscriptionSelectors' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +const CoachingNotStarted = () => { + const coachingNotStarted = useSelector(getCoachingNotStarted) + const hasActiveCoaching = useSelector(getActiveCoaching) + + if (!hasActiveCoaching) return null + + if (coachingNotStarted) { + return ( + + NOT_STARTED_TITLE + NOT_STARTED + + ) + } + return null +} + +export default CoachingNotStarted + +const Container = styled.View` + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 10px 20px; + background-color: ${colors.radiantBlue}; +` + +const Title = styled(TranslatedText)` + color: white; + font-family: ${fonts.bold}; + margin-bottom: 5px; +` + +const Explainer = styled(TranslatedText)` + color: white; + font-size: 13px; + font-family: ${fonts.regular}; +` diff --git a/src/components/CoachingSpecific/CoachingSectionHeader.tsx b/src/components/CoachingSpecific/CoachingSectionHeader.tsx new file mode 100644 index 0000000..f5cc308 --- /dev/null +++ b/src/components/CoachingSpecific/CoachingSectionHeader.tsx @@ -0,0 +1,28 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +interface CoachingSectionHeaderProps { + title: string + data: any +} +const CoachingSectionHeader = (props: CoachingSectionHeaderProps) => + props.data.length !== 0 ? ( + + {props.title} + + ) : null + +export default memo(CoachingSectionHeader) + +const SectionHeader = styled.View` + background-color: ${(props) => props.theme.PRIMARY_BACKGROUND_COLOR}; + padding: 10px 20px; +` + +const SectionTitle = styled(TranslatedText)` + font-family: ${fonts.bold}; + font-size: 22px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/Copyright.tsx b/src/components/CoachingSpecific/Copyright.tsx new file mode 100644 index 0000000..530dfd7 --- /dev/null +++ b/src/components/CoachingSpecific/Copyright.tsx @@ -0,0 +1,26 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' + +const Copyright = () => { + return ( + + © 2020 Nyxo All Rights Reserved + + ) +} + +export default memo(Copyright) + +const Container = styled.View` + height: 100px; + padding: 20px; + margin-bottom: 50px; + justify-content: flex-end; +` + +const Text = styled.Text` + font-family: ${fonts.medium}; + text-align: center; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/IntroduceCoaching.tsx b/src/components/CoachingSpecific/IntroduceCoaching.tsx new file mode 100644 index 0000000..5e28931 --- /dev/null +++ b/src/components/CoachingSpecific/IntroduceCoaching.tsx @@ -0,0 +1,37 @@ +import React, { memo } from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import { getActiveCoaching } from '../../store/Selectors/subscription-selectors/SubscriptionSelectors' +import { Container } from '../Primitives/Primitives' +import TranslatedText from '../TranslatedText' + +const IntroduceCoaching = () => { + const hasActiveCoaching = useSelector(getActiveCoaching) + + if (hasActiveCoaching) return null + + return ( + + NYXO_COACHING_INTRO + WHY_BUY_1 + WHY_BUY_2 + + ) +} + +export default memo(IntroduceCoaching) + +const SubTitle = styled(TranslatedText)` + font-family: ${fonts.bold}; + font-size: 17px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + margin-bottom: 10px; +` + +const MiniText = styled(TranslatedText)` + font-family: ${fonts.medium}; + font-size: 13px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + margin-bottom: 10px; +` diff --git a/src/components/CoachingSpecific/RefreshCoaching.tsx b/src/components/CoachingSpecific/RefreshCoaching.tsx new file mode 100644 index 0000000..b552d76 --- /dev/null +++ b/src/components/CoachingSpecific/RefreshCoaching.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import styled from 'styled-components/native' +import { StyleProps } from '../../styles/themes' +import { PrimaryButton } from '../Buttons/PrimaryButton' +import { H3, P } from '../Primitives/Primitives' + +interface Props { + refresh: Function +} + +const RefreshCoaching = (props: Props) => { + const refresh = () => { + props.refresh() + } + + return ( + + COACHING_MISSING + REFRESH_COACHING_INFO + + + ) +} + +export default RefreshCoaching + +const Card = styled.View` + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + width: 250px; + padding: 20px; + border-radius: 5px; + margin-bottom: 30px; +` + +const H3M = styled(H3)` + margin-bottom: 30px; +` + +const PM = styled(P)` + margin-bottom: 30px; +` diff --git a/src/components/CoachingSpecific/StartCoaching.tsx b/src/components/CoachingSpecific/StartCoaching.tsx new file mode 100644 index 0000000..cc98d15 --- /dev/null +++ b/src/components/CoachingSpecific/StartCoaching.tsx @@ -0,0 +1,100 @@ +import { + completeWeek, + startCoachingMonth, + startCoachingWeek +} from '@actions/coaching/coaching-actions' +import moment from 'moment' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + CombinedWeek, + getNextWeek, + getSelectedWeekCompleted, + getSelectedWeekOngoing, + WEEK_STAGE, + getCoachingNotStarted +} from 'store/Selectors/coaching-selectors/coaching-selectors' +import styled from 'styled-components/native' +import { getActiveCoaching } from '../../store/Selectors/subscription-selectors/SubscriptionSelectors' +import { PrimaryButton } from '../Buttons/PrimaryButton' +import BuyCoachingButton from './BuyCoachingButton' +import WeekCompleted from './WeekCompleted' + +type Props = { + week: CombinedWeek +} + +const StartCoaching = ({ week }: Props) => { + const dispatch = useDispatch() + const hasCoaching = useSelector(getActiveCoaching) + const weekCompleted = useSelector(getSelectedWeekCompleted) + const coachingNotStarted = useSelector(getCoachingNotStarted) + const weekOngoing = useSelector(getSelectedWeekOngoing) + const nextWeek = useSelector(getNextWeek) + + const startCoaching = () => { + dispatch(startCoachingMonth({ ...week, started: moment().toISOString() })) + } + + const startWeek = () => { + dispatch(startCoachingWeek(week.slug)) + } + + const handleComplete = () => { + dispatch(completeWeek(week.slug, nextWeek?.slug)) + } + + if (!hasCoaching) { + return + } + + if (weekCompleted || weekOngoing) { + return null + } + + if (coachingNotStarted) { + return ( + + + + ) + } + + switch (week.stage) { + case WEEK_STAGE.CAN_BE_COMPLETED: + return ( + + + + ) + case WEEK_STAGE.CAN_BE_STARTED: + return ( + + + + ) + + case WEEK_STAGE.ONGOING: + return null + case WEEK_STAGE.COMPLETED: + return + default: + return null + } +} + +export default StartCoaching + +const FixedContainer = styled.View` + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 10px 20px; +` diff --git a/src/components/CoachingSpecific/TopHeader.tsx b/src/components/CoachingSpecific/TopHeader.tsx new file mode 100644 index 0000000..bfa6d3e --- /dev/null +++ b/src/components/CoachingSpecific/TopHeader.tsx @@ -0,0 +1,90 @@ +import React, { memo } from 'react' +import Animated from 'react-native-reanimated' +import styled from 'styled-components/native' +import { + HEADER_HALF, + HEADER_MAX_HEIGHT, + HEADER_MIN_HEIGHT, + SMART_TOP_PADDING +} from '../../helpers/Dimensions' +import { fonts } from '../../styles/themes' +import GoBack from '../Buttons/GoBack' + +interface Props { + yOffset: any + title: string +} + +const TopHeader = ({ yOffset, title }: Props) => { + const fadeIn = () => ({ + opacity: yOffset.interpolate({ + inputRange: [HEADER_HALF, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [0, 1], + extrapolate: Animated.Extrapolate.CLAMP + }) + }) + + const fadeColor = () => ({ + opacity: yOffset.interpolate({ + inputRange: [HEADER_HALF, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [0, 1], + extrapolate: Animated.Extrapolate.CLAMP + }) + }) + return ( + + + + + + + + {title} + + + + + ) +} + +export default memo(TopHeader) + +const BackButtonContainer = styled(Animated.View)` + position: absolute; + z-index: 10; + top: 0; + padding: ${parseInt(SMART_TOP_PADDING)}px 20px 0px; + flex-direction: row; + align-items: center; + width: 100%; +` + +const BackButton = styled.View` + flex: 1; +` + +const Background = styled(Animated.View)` + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + box-shadow: ${({ theme }) => theme.SHADOW}; + background-color: ${({ theme }) => theme.SECONDARY_BACKGROUND_COLOR}; +` + +const Placeholder = styled.View` + flex: 1; +` + +const TitleContainer = styled.View` + flex: 1; +` + +const WeekTitleSmall = styled(Animated.Text)` + text-align: center; + font-size: 13px; + justify-content: center; + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/Week.Cover.tsx b/src/components/CoachingSpecific/Week.Cover.tsx new file mode 100644 index 0000000..1a45348 --- /dev/null +++ b/src/components/CoachingSpecific/Week.Cover.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import LinearGradient from 'react-native-linear-gradient' +import Animated from 'react-native-reanimated' +import styled from 'styled-components/native' +import FastImage from 'react-native-fast-image' +import { + HEADER_MAX_HEIGHT, + HEADER_MIN_HEIGHT, + WIDTH +} from '../../helpers/Dimensions' +import { StyleProps } from '../../styles/themes' +import AnimatedFastImage from '../AnimatedFastImage/AnimatedFastImage' + +const Cover = ({ cover, yOffset }: { cover: string; yOffset: any }) => { + const headerHeight = (yOffset: any) => ({ + // transform: [ + // { + // scale: yOffset.interpolate({ + // inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + // outputRange: [2, 1], + // extrapolateRight: Animated.Extrapolate.CLAMP, + // }), + // }, + // ], + opacity: yOffset.interpolate({ + inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [1, 0], + extrapolateRight: Animated.Extrapolate.CLAMP, + extrapolateLeft: Animated.Extrapolate.CLAMP + }) + }) + + return ( + + + + + ) +} + +export default Cover + +const Container = styled.View` + height: ${HEADER_MAX_HEIGHT}px; + width: 100%; + overflow: hidden; + position: absolute; + background-color: ${(props: StyleProps) => + props.theme.PRIMARY_BACKGROUND_COLOR}; +` + +const Gradient = styled(LinearGradient).attrs((props: StyleProps) => ({ + colors: props.theme.GRADIENT +}))` + position: absolute; + left: 0; + top: 0; + right: 0; + z-index: 10; + bottom: 0; +` + +const CoverPhoto = styled(AnimatedFastImage)` + z-index: 0; + width: 100%; + height: 100%; +` diff --git a/src/components/CoachingSpecific/WeekCard.tsx b/src/components/CoachingSpecific/WeekCard.tsx new file mode 100644 index 0000000..7a7e6f1 --- /dev/null +++ b/src/components/CoachingSpecific/WeekCard.tsx @@ -0,0 +1,168 @@ +import { useNavigation } from '@react-navigation/native' +import React, { memo } from 'react' +import FastImage from 'react-native-fast-image' +import LinearGradient from 'react-native-linear-gradient' +import Animated from 'react-native-reanimated' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { CombinedWeek } from 'store/Selectors/coaching-selectors/coaching-selectors' +import { IconBold } from 'components/iconRegular' +import ROUTE from 'config/routes/Routes' +import { selectWeek } from '@actions/coaching/coaching-actions' +import TranslatedText from 'components/TranslatedText' +import WeekCardTitle from './WeekCardTitle' +import ScalingButton from '../Buttons/ScalingButton' +import { constants, fonts, StyleProps } from '../../styles/themes' +import colors from '../../styles/colors' + +interface WeekCardProps { + week: CombinedWeek + cardWidth: number + cardMargin: number + xOffset: any +} + +const WeekCard = (props: WeekCardProps) => { + const { cardWidth, cardMargin, week } = props + const navigation = useNavigation() + const dispatch = useDispatch() + + const locked = week.locked !== undefined ? week.locked : week.defaultLocked + + const handlePress = () => { + dispatch(selectWeek(week.slug)) + navigation.navigate(ROUTE.WEEK, { + week + }) + } + + const formatedIntro = week.intro ? week.intro.replace('–', '\n – ') : '' + const lessonCount = week.lessons.length + const { habitCount } = week + return ( + + + + + + + + + + + + {formatedIntro} + + + + + + + + + + WEEK_VIEW.LESSON_COUNT + {habitCount > 0 && ( + <> + + WEEK_VIEW.HABIT_COUNT + + )} + + WEEK_VIEW.DAY_COUNT + + + + + ) +} + +export default memo(WeekCard) + +const Info = styled(TranslatedText)` + margin-left: 10px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +const CardContainer = styled(Animated.View)` + flex: 1; +` + +const LessonIcon = styled(IconBold).attrs(({ theme }) => ({ + height: 15, + width: 15, + fill: theme.SECONDARY_TEXT_COLOR, + name: 'bookLamp' +}))`` + +const HabitIcon = styled(IconBold).attrs(({ theme }) => ({ + height: 15, + width: 15, + fill: theme.SECONDARY_TEXT_COLOR, + name: 'checklist' +}))` + margin-left: 20px; +` + +const CalendarIcon = styled(IconBold).attrs(({ theme }) => ({ + height: 15, + width: 15, + fill: theme.SECONDARY_TEXT_COLOR, + name: 'calendar' +}))` + margin-left: 20px; +` + +const Card = styled.View` + padding: 20px 0px; + z-index: 20; + min-height: 200px; +` + +const Border = styled.View` + border-bottom-color: ${({ theme }) => theme.HAIRLINE_COLOR}; + border-bottom-width: ${constants.hairlineWidth}px; + margin: 20px 10px 0px; +` + +const CoverPhotoContainer = styled.View` + flex: 1; + height: 150px; + overflow: hidden; + border-radius: 5px; +` + +const CoverPhoto = styled(FastImage)` + flex: 1; + z-index: 0; + max-height: 250px; + height: 100%; + width: 100%; +` +const GradientContainer = styled.View` + position: absolute; + bottom: 0; + left: 0; + right: 0; +` + +const Intro = styled(Animated.Text)` + margin-top: 5px; + font-size: 13px; + font-family: ${fonts.medium}; + color: ${colors.white}; +` + +const Row = styled(Animated.View)` + flex-direction: row; + margin: 0px 10px; + align-items: center; +` diff --git a/src/components/CoachingSpecific/WeekCardTitle.tsx b/src/components/CoachingSpecific/WeekCardTitle.tsx new file mode 100644 index 0000000..5d14d82 --- /dev/null +++ b/src/components/CoachingSpecific/WeekCardTitle.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { View } from 'react-native' +import { WEEK_STAGE } from 'store/Selectors/coaching-selectors' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import IconBold from '../iconBold' +import TranslatedText from '../TranslatedText' + +interface Props { + weekName: string + stage?: WEEK_STAGE +} + +const WeekCardTitle = ({ weekName, stage }: Props) => { + const { stageTitle, icon } = getStageString(stage) + return ( + + + + {stageTitle} + + + {weekName} + + ) +} + +export default WeekCardTitle + +const getStageString = ( + stage?: WEEK_STAGE +): { stageTitle: string; icon: string } => { + switch (stage) { + case WEEK_STAGE.COMPLETED: + return { stageTitle: 'WEEK_COMPLETED', icon: 'crown' } + case WEEK_STAGE.ONGOING: + return { stageTitle: 'ONGOING_WEEK', icon: 'targetCenter' } + case WEEK_STAGE.UPCOMING: + return { stageTitle: 'UPCOMING_WEEK', icon: 'targetCenter' } + default: + return { stageTitle: 'UPCOMING_WEEK', icon: 'targetCenter' } + } +} + +const Row = styled.View` + flex: 1; + margin-bottom: 10px; + flex-direction: column; + justify-content: center; +` + +const Title = styled(TranslatedText)` + margin-left: 5px; + text-transform: uppercase; + font-size: 12px; + font-family: ${fonts.medium}; + color: ${colors.radiantBlue}; +` + +const Theme = styled.Text` + font-size: 21px; + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/WeekCarousel.tsx b/src/components/CoachingSpecific/WeekCarousel.tsx new file mode 100644 index 0000000..2c24c49 --- /dev/null +++ b/src/components/CoachingSpecific/WeekCarousel.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { View } from 'react-native' +import Animated from 'react-native-reanimated' +import { useSelector } from 'react-redux' +import { + CombinedWeek, + getCombinedWeeks, + getCurrentWeek +} from 'store/Selectors/coaching-selectors' +import styled from 'styled-components/native' +import { WIDTH } from '../../helpers/Dimensions' +import { AnimatedFlatList, H3 } from '../Primitives/Primitives' +import WeekCard from './WeekCard' + +export const cardWidth = WIDTH - 40 +export const cardMargin = 5 +const xOffset = new Animated.Value(0) + +const WeekCarousel = () => { + const currentWeek = useSelector(getCurrentWeek) + const combined = useSelector(getCombinedWeeks) + const ongoing = combined + + const renderWeekCard = ({ item }: { item: CombinedWeek }) => { + return ( + + ) + } + + const activeWeekIndex = ongoing.findIndex( + (week: CombinedWeek) => week.contentId === currentWeek + ) + const snapOffets: number[] = ongoing.map( + (item, index) => index * (cardWidth + cardMargin * 2) + ) + const inset = (WIDTH - cardWidth - cardMargin) / 2 + + return ( + + +

COACHING_WEEKS

+
+ ({ + index, + length: cardWidth, + offset: (cardWidth + cardMargin) * index + })} + onScroll={Animated.event( + [{ nativeEvent: { contentOffset: { x: xOffset } } }], + { + useNativeDriver: true + } + )} + snapToAlignment="center" + snapToEnd={false} + data={ongoing} + renderItem={renderWeekCard} + /> +
+ ) +} + +export default WeekCarousel + +const Container = styled.View` + padding: 0px 20px; +` diff --git a/src/components/CoachingSpecific/WeekCompleted.tsx b/src/components/CoachingSpecific/WeekCompleted.tsx new file mode 100644 index 0000000..fd9425e --- /dev/null +++ b/src/components/CoachingSpecific/WeekCompleted.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { getSelectedWeekCompleted } from 'store/Selectors/coaching-selectors/coaching-selectors' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +const WeekCompleted = () => { + const weekCompleted = useSelector(getSelectedWeekCompleted) + + if (weekCompleted) { + return ( +
+ CoachingSessionCompleted +
+ ) + } + return null +} + +export default React.memo(WeekCompleted) + +const Section = styled.View` + position: absolute; + bottom: 20; + left: 0; + right: 0; + margin: 20px; + border-radius: 5px; + padding: 10px; + background-color: ${({ theme }) => theme.SECONDARY_BACKGROUND_COLOR}; + box-shadow: ${({ theme }) => theme.SHADOW}; +` + +const Started = styled.Text`` + +const SectionTitle = styled(TranslatedText)` + font-size: 15px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/WeekIntro.tsx b/src/components/CoachingSpecific/WeekIntro.tsx new file mode 100644 index 0000000..37399d4 --- /dev/null +++ b/src/components/CoachingSpecific/WeekIntro.tsx @@ -0,0 +1,102 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import TranslatedText from 'components/TranslatedText' +import { completeWeek } from '@actions/coaching/coaching-actions' +import { useDispatch } from 'react-redux' +import moment from 'moment' +import { PN } from '../Primitives/Primitives' +import { StyleProps, fonts, constants } from '../../styles/themes' + +interface Props { + intro: string + description: string + habitCount: number + lessonCount: number + started?: string + ended?: string +} + +const WeekIntro = ({ + intro, + description, + habitCount, + lessonCount, + started, + ended +}: Props) => { + const startTime = started ? moment(started).format('DD.MM.') : '' + const endTime = ended ? moment(ended).format('DD.MM.') : '' + + return ( + + {intro} + {description} + + WEEK_VIEW.HABIT_COUNT + {lessonCount > 0 && ( + + WEEK_VIEW.LESSON_COUNT + + )} + + + {started && ( + + WEEK_VIEW.START_DATE + + )} + {ended && ( + WEEK_VIEW.END_DATE + )} + + + ) +} + +export default memo(WeekIntro) + +const Container = styled.View` + background-color: ${(props: StyleProps) => + props.theme.PRIMARY_BACKGROUND_COLOR}; + padding: 10px 20px 30px; +` + +const Intro = styled.Text` + margin: 10px 0px 5px; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + font-size: 17px; +` + +const Information = styled.View` + flex-direction: row; + border-bottom-color: ${({ theme }) => theme.HAIRLINE_COLOR}; + padding: 10px 0px 5px; + border-bottom-width: ${constants.hairlineWidth}px; +` + +const Habits = styled(TranslatedText)` + font-size: 13px; + margin-right: 10px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +const DurationRow = styled.View` + flex-direction: row; + padding: 10px; +` + +const Started = styled(TranslatedText)` + font-size: 13px; + margin-right: 10px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +const Ended = styled(TranslatedText)` + font-size: 13px; + margin-right: 10px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/CoachingSpecific/WeekSummary.tsx b/src/components/CoachingSpecific/WeekSummary.tsx new file mode 100644 index 0000000..e0c656c --- /dev/null +++ b/src/components/CoachingSpecific/WeekSummary.tsx @@ -0,0 +1,101 @@ +import React, { memo } from 'react' +import { View } from 'react-native' +import { AnimatedCircularProgress } from 'react-native-circular-progress' +import ViewOverflow from 'react-native-view-overflow' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import { Container, H2 } from '../Primitives/Primitives' +import TranslatedText from '../TranslatedText' + +const WeekSummary = () => { + const requirements = [ + { requirement: 'Complete at least 4 lessons', completed: false }, + { requirement: 'Create at least 2 Actions', completed: true }, + { requirement: 'Survive through the week', completed: true } + ].map((item, index) => { + return ( + + {item.requirement} + {item.completed ? ( + + ) : ( + + )} + + ) + }) + + return ( + + +

Week Summary

+ + {(fill: number) => {`${Math.round(fill)}%`}} + +
+ {requirements} +
+ ) +} + +export default memo(WeekSummary) + +const List = styled.View` + margin-right: 25px; +` + +const Icon = styled(IconBold)` + position: absolute; + right: -10; + bottom: 0; + background-color: transparent; +` + +const ListItem = styled.View` + border-right-width: 2; + border-right-color: purple; + flex-direction: row; + padding-top: 30px; + align-items: center; +` + +const ListItemTitle = styled(TranslatedText)` + font-family: ${fonts.medium}; + font-size: 15; + color: black; +` + +const Percentage = styled.Text` + font-size: 10; + font-family: ${fonts.bold}; + color: ${colors.radiantBlue}; +` + +const IncompleteIcon = styled(ViewOverflow)` + height: 20; + width: 20; + border-radius: 30px; + position: absolute; + right: -10; + bottom: 0; + background-color: green; +` diff --git a/src/components/CoachingSpecific/WeekViewHeader.tsx b/src/components/CoachingSpecific/WeekViewHeader.tsx new file mode 100644 index 0000000..9217c6f --- /dev/null +++ b/src/components/CoachingSpecific/WeekViewHeader.tsx @@ -0,0 +1,74 @@ +import React, { memo } from 'react' +import LinearGradient from 'react-native-linear-gradient' +import Animated from 'react-native-reanimated' +import styled from 'styled-components/native' +import { HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT } from '../../helpers/Dimensions' +import { fonts, StyleProps } from '../../styles/themes' + +interface Props { + yOffset: any + title?: string +} + +const WeekViewHeader = (props: Props) => { + const { yOffset, title } = props + + const titleSize = (yOffset: any) => ({ + opacity: yOffset.interpolate({ + inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [1, 0.2], + extrapolateRight: Animated.Extrapolate.CLAMP, + extrapolateLeft: Animated.Extrapolate.CLAMP + }) + }) + + return ( +
+ + + {title} + + +
+ ) +} + +export default memo(WeekViewHeader) + +const Header = styled(Animated.View)` + width: 100%; + height: ${HEADER_MAX_HEIGHT}px; + z-index: 1; + overflow: hidden; +` + +const GradientContainer = styled.View` + position: absolute; + height: ${HEADER_MAX_HEIGHT}px; + bottom: 0; + left: 0; + right: 0; +` + +const Gradient = styled(LinearGradient).attrs((props: StyleProps) => ({ + colors: props.theme.GRADIENT +}))` + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + padding: 0px 20px 20px; +` + +const WeekTitle = styled(Animated.Text)` + font-family: ${fonts.bold}; + z-index: 20; + font-size: 35px; + text-align: left; + position: absolute; + bottom: 0; + left: 20px; + right: 20px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/CurrentCoaching.tsx b/src/components/CurrentCoaching.tsx new file mode 100644 index 0000000..84d7c31 --- /dev/null +++ b/src/components/CurrentCoaching.tsx @@ -0,0 +1,27 @@ +import React, { memo } from 'react' +import { SafeAreaView, View, Text } from 'react-native ' +import BottomSheet from 'reanimated-bottom-sheet' + +const CurrentCoaching = () => { + const renderInner = () => { + return + } + + const renderHeader = () => { + return + } + + return ( + + + + + + ) +} + +export default memo(CurrentCoaching) diff --git a/src/components/DayStrip.tsx b/src/components/DayStrip.tsx new file mode 100644 index 0000000..b9119b8 --- /dev/null +++ b/src/components/DayStrip.tsx @@ -0,0 +1,126 @@ +import { default as Moment, default as moment } from 'moment' +import React, { memo } from 'react' +import { SectionList, Text, View } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components/native' +import { setActiveIndex } from '../actions/sleep/sleep-data-actions' +import { WIDTH } from '../helpers/Dimensions' +import keyExtractor from '../helpers/KeyExtractor' +import { fonts, StyleProps } from '../styles/themes' +import { + getActiveIndex, + getWeekSelector +} from '../store/Selectors/SleepDataSelectors' +import { Day } from '../Types/Sleepdata' + +const dayWidth = WIDTH / 7 +const cardMargin = 5 + +const DayStrip = () => { + const days: Day[] = useSelector(getWeekSelector) + const activeIndex = useSelector(getActiveIndex) + const dispatch = useDispatch() + + const renderItem = ({ item, index }: { item: any; index: number }) => { + const isToday = moment(item.date).isSame(new Date(), 'day') + + const handleOnPress = () => { + dispatch(setActiveIndex(index)) + } + + return ( + + + {Moment(item.date).format('ddd')} + + + {Moment(item.date).format('DD')} + + + ) + } + + const snapOffets: number[] = days.map( + (item, index) => index * (dayWidth + cardMargin * 2) + ) + + const renderSectionHeader = ({ index, section }: any) => { + return ( + + {section.title} + + ) + } + + return ( + ({ + index, + length: dayWidth, + offset: (dayWidth + cardMargin) * index + })} + snapToAlignment="center" + snapToEnd={false} + keyExtractor={keyExtractor} + showsHorizontalScrollIndicator={false} + renderItem={renderItem} + horizontal + /> + ) +} + +export default memo(DayStrip) + +const Segments = styled(SectionList)` + width: ${WIDTH}px; + height: ${dayWidth}px; + margin: 20px 0px; +` + +interface SegmentProps extends StyleProps { + readonly active?: boolean + readonly today?: boolean +} + +const Segment = styled.TouchableOpacity` + width: ${dayWidth}px; + height: ${dayWidth}px; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 5; + border-radius: 5px; + background-color: ${(props: SegmentProps) => + props.active + ? props.theme.PRIMARY_TEXT_COLOR + : props.theme.PRIMARY_BACKGROUND_COLOR}; +` + +const DateText = styled.Text` + font-size: 12px; + color: ${(props: SegmentProps) => + props.active + ? props.theme.PRIMARY_BACKGROUND_COLOR + : props.theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + margin-bottom: 5px; + text-align: center; +` + +const DateNumber = styled.Text` + font-size: 15px; + font-weight: bold; + font-family: ${fonts.bold}; + text-align: center; + color: ${(props: SegmentProps) => + props.active + ? props.theme.PRIMARY_BACKGROUND_COLOR + : props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/DetailView/SampleRow.tsx b/src/components/DetailView/SampleRow.tsx new file mode 100644 index 0000000..d11a56f --- /dev/null +++ b/src/components/DetailView/SampleRow.tsx @@ -0,0 +1,95 @@ +import React, { memo } from 'react' +import { Animated, View } from 'react-native' +import { RectButton } from 'react-native-gesture-handler' +import Swipeable from 'react-native-gesture-handler/Swipeable' +import styled from 'styled-components/native' +import { fonts } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +interface Night { + value: string + source: string | undefined + durationString: string +} + +const SampleRow = (night: Night) => { + const close = () => {} + + const renderRightActions = (progress: any, dragX: any) => { + const trans = dragX.interpolate({ + inputRange: [0, 50], + outputRange: [0, 1] + }) + return ( + + + + Archive + + + + ) + } + + return ( + + + + {night.source} + {night.value} + + {night.durationString} + + + ) +} + +export default memo(SampleRow) + +const Row = styled.Text` + flex-direction: row; + padding: 10px 20px; + flex: 1; + height: 80px; + background-color: white; + justify-content: space-between; + + /* flexDirection: 'row', + paddingVertical: 10, + paddingHorizontal: 20, + flex: 1, + height: 80, + backgroundColor: 'white', + justifyContent: 'space-between', */ +` + +const DurationText = styled.Text` + font-size: 17px; + font-family: ${fonts.medium}; + color: black; +` + +const SourceText = styled.Text` + font-size: 17px; + font-family: ${fonts.bold}; + color: black; +` + +const ValueText = styled(TranslatedText)` + font-size: 17px; + font-family: ${fonts.medium}; + color: black; +` diff --git a/src/components/ElegantAnimation.tsx b/src/components/ElegantAnimation.tsx new file mode 100644 index 0000000..6ab7328 --- /dev/null +++ b/src/components/ElegantAnimation.tsx @@ -0,0 +1,47 @@ +import React, { useState, useEffect, memo } from 'react' +import { Animated, Easing } from 'react-native' + +interface ElegantAnimationProps { + delay: number + children: any + style?: any +} + +const ElegantAnimation = (props: ElegantAnimationProps) => { + const [fadeAnim] = useState(new Animated.Value(0)) + const [scaleAnim] = useState(new Animated.Value(0)) + + const delay = 30 * parseInt(props.delay, 0) + const duration = 350 + + useEffect(() => { + Animated.parallel([ + Animated.timing(fadeAnim, { + delay, + duration, + toValue: 1, + useNativeDriver: true, + easing: Easing.easeInOutBack + }), + Animated.spring(scaleAnim, { + delay, + toValue: 1, + friction: 7, + useNativeDriver: true + }) + ]).start() + }, []) + + return ( + + {props.children} + + ) +} + +export default memo(ElegantAnimation) diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..17a1d79 --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,39 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../styles/themes' +import { IconBold } from './iconRegular' +import TranslatedText from './TranslatedText' + +interface Props { + text?: string +} + +const EmptyState = (props: Props) => { + return ( + + + {props.text ? props.text : 'EmptyState'} + + ) +} + +export default memo(EmptyState) + +const Container = styled.View` + margin: 40px 0px; + padding: 0px 20px; + flex-direction: column; + align-items: center; +` + +const Text = styled(TranslatedText)` + margin-top: 10px; + font-size: 17px; + text-align: center; + font-family: ${fonts.bold}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.SECONDARY_TEXT_COLOR +}))`` diff --git a/src/components/HabitCard/ActionComplete.tsx b/src/components/HabitCard/ActionComplete.tsx new file mode 100644 index 0000000..94c18c6 --- /dev/null +++ b/src/components/HabitCard/ActionComplete.tsx @@ -0,0 +1,63 @@ +import React, { memo } from 'react' +import { Animated } from 'react-native' +import { BorderlessButton } from 'react-native-gesture-handler' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import { AnimatedTranslatedText } from '../TranslatedText' + +type Props = { + direction: 'LEFT' | 'RIGHT' + action: Function + buttonText: string + icon: string + animation: any +} + +const ActionComplete = (props: Props) => { + const { direction, action, icon, buttonText, animation } = props + + const markCompleted = () => { + action() + } + + return ( + + + {direction === 'LEFT' ? ( + <> + + {buttonText} + + ) : ( + <> + {buttonText} + + + )} + + + ) +} + +export default memo(ActionComplete) + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.PRIMARY_TEXT_COLOR +}))`` + +const ButtonText = styled(AnimatedTranslatedText)` + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + margin: 0px 5px; +` + +const Container = styled(Animated.View)` + flex-direction: row; + margin: 0px 20px; +` + +const SlideAction = styled(BorderlessButton)` + flex-direction: row; + align-items: center; +` diff --git a/src/components/HabitCard/ExampleHabit.tsx b/src/components/HabitCard/ExampleHabit.tsx new file mode 100644 index 0000000..4d084d8 --- /dev/null +++ b/src/components/HabitCard/ExampleHabit.tsx @@ -0,0 +1,155 @@ +import { Document } from '@contentful/rich-text-types' +import RichText from 'components/RichText' +import React, { memo, useState } from 'react' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { addHabit } from '@actions/habit/habit-actions' +import { documentToPlainTextString } from '@contentful/rich-text-plain-text-renderer' +import { Period } from 'Types/State/Periods' +import { WIDTH } from '../../helpers/Dimensions' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import { Container, StyledModal } from '../Primitives/Primitives' +import TranslatedText from '../TranslatedText' +import { getIcon } from './TopRow' + +interface Props { + title?: string + period?: string + description?: Document +} + +const ExampleHabit = (props: Props) => { + const { title = 'Custom Habit', period = Period.morning, description } = props + const dispatch = useDispatch() + const [show, setShow] = useState(false) + const [habitAdded, setHabitAdded] = useState(false) + + const createHabit = async () => { + await dispatch( + addHabit(title, documentToPlainTextString(description), period) + ) + await setHabitAdded(true) + } + + const toggleModal = () => { + setShow(!show) + } + + if (!title || !period || !description) return null + + const { color, icon } = getIcon(period) + + return ( + <> + + + + {`HABIT.EVERY_${period.toUpperCase()}`} + + + {title} + + + + + READ_MORE + + + + + {habitAdded ? 'HABIT_ADDED' : 'ADD_HABIT'} + + + + + + + {title} + + + + + ) +} + +export default memo(ExampleHabit) + +const Habit = styled.View` + padding: 20px; + margin: 0px 20px; + flex: 1; + width: ${WIDTH - 40}px; + background-color: ${({ theme }) => theme.SECONDARY_BACKGROUND_COLOR}; + box-shadow: 1px 1px 15px rgba(32, 33, 37, 0.1); + border-radius: 5px; +` + +const TimePeriod = styled(TranslatedText)` + font-size: 13px; + text-transform: uppercase; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; + margin-left: 10px; +` + +const Title = styled.Text` + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; + font-size: 21px; + margin: 10px 0px; +` + +const ModalTitle = styled.Text` + font-family: ${fonts.bold}; + margin: 30px 0px 40px; + font-size: 21px; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; +` + +const Row = styled.View` + flex-direction: row; + align-items: center; +` + +const Icon = styled(IconBold)`` + +const ReadMore = styled(TranslatedText)` + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; + font-size: 15px; + margin-left: 10px; +` + +const ReadMoreButton = styled.TouchableOpacity` + flex-direction: row; + align-items: center; + margin: 10px 0px; +` + +const Buttons = styled.View` + flex-direction: row; + justify-content: space-between; + align-items: center; +` + +const QuestionIcon = styled(IconBold).attrs(({ theme }) => ({ + fill: theme.SECONDARY_TEXT_COLOR +}))`` + +const AddHabit = styled.TouchableOpacity`` + +const AddHabitText = styled(TranslatedText)` + color: ${colors.radiantBlue}; + font-family: ${fonts.bold}; + font-size: 15px; + text-align: right; +` diff --git a/src/components/HabitCard/HabitCard.tsx b/src/components/HabitCard/HabitCard.tsx new file mode 100644 index 0000000..f89dcfe --- /dev/null +++ b/src/components/HabitCard/HabitCard.tsx @@ -0,0 +1,222 @@ +import { + archiveHabit as archiveHabitThunk, + deleteHabitById, + draftEditHabit, + markTodayHabitAsCompleted +} from '@actions/habit/habit-actions' +import React, { memo, useRef } from 'react' +import { Animated } from 'react-native' +import { BorderlessButton } from 'react-native-gesture-handler' +import Swipeable from 'react-native-gesture-handler/Swipeable' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { Habit } from 'Types/State/habit-state' +import { toggleEditHabitModal } from '../../actions/modal/modal-actions' +import { isCompletedToday } from '../../helpers/habits' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import TranslatedText from '../TranslatedText' +import ActionComplete from './ActionComplete' +import { getIcon } from './TopRow' + +export const cardHeight = 100 + +type Props = { + habit: Habit +} + +const HabitCard = (props: Props) => { + const ref: any = useRef() + const dispatch = useDispatch() + const { habit } = props + const { + habit: { period, dayStreak = 0 } + } = props + const { color } = getIcon(period) + + const completed = isCompletedToday(habit) + + const toggleCompleted = () => { + dispatch(markTodayHabitAsCompleted(habit)) + close() + } + + const editHabit = () => { + dispatch(draftEditHabit(habit)) + dispatch(toggleEditHabitModal()) + } + + const deleteHabit = () => { + dispatch(deleteHabitById(habit)) + close() + } + const archiveHabit = () => { + dispatch(archiveHabitThunk(habit)) + close() + } + + const close = () => { + ref!.current!.close() + } + + const renderLeftActions = ( + progress: Animated.AnimatedInterpolation, + dragX: Animated.AnimatedInterpolation + ) => { + const animation = dragX.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1] + }) + return ( + <> + + + + ) + } + + const renderRightActions = ( + progress: Animated.AnimatedInterpolation, + dragX: Animated.AnimatedInterpolation + ) => { + const animation = dragX.interpolate({ + inputRange: [-150, 0], + outputRange: [1, 0] + }) + + return ( + <> + + + ) + } + + return ( + + + + + + + + {`HABIT.EVERY_${period.toUpperCase()}`} + + + {habit.title} + + + + {dayStreak} + + + + + {completed && ( + + )} + + + + + ) +} + +export default memo(HabitCard) + +const Card = styled.View` + margin: 8px 20px; + border-radius: 5px; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + + box-shadow: 1px 3px 2px rgba(32, 33, 37, 0.2); + flex-direction: row; + elevation: 8; +` + +const PeriodBarIndicator = styled.View` + flex-direction: column; + width: 3px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + background-color: ${(props: { backgroundColor: string }) => + props.backgroundColor}; +` + +const MiddleSector = styled.View` + flex-direction: column; + padding: 6px 10px; + flex: 1; +` + +interface TimeProps { + accent: string +} + +const PeriodIndicator = styled(TranslatedText)` + font-size: 11px; + text-transform: uppercase; + color: ${(props: TimeProps) => props.accent}; + font-family: ${fonts.medium}; +` + +interface TitleHolderProps extends StyleProps { + completedToday: boolean +} + +const TitleHolder = styled.Text` + font-family: ${fonts.medium}; + font-size: 15px; + text-decoration: ${(props: TitleHolderProps) => + props.completedToday ? 'line-through' : 'none'}; + color: ${(props: TitleHolderProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const DayStreakContainer = styled.View` + flex-direction: row; + align-items: center; +` + +const DayStreak = styled.Text` + margin-left: 5px; + font-size: 12px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const Separator = styled.View` + height: 5px; +` + +const CheckIconHolder = styled.View` + justify-content: center; + align-items: center; + padding: 0px 20px; +` diff --git a/src/components/HabitCard/HabitCardManage.tsx b/src/components/HabitCard/HabitCardManage.tsx new file mode 100644 index 0000000..d6498a1 --- /dev/null +++ b/src/components/HabitCard/HabitCardManage.tsx @@ -0,0 +1,141 @@ +import Moment from 'moment' +import React, { memo, useRef } from 'react' +import { BorderlessButton } from 'react-native-gesture-handler' +import Swipeable from 'react-native-gesture-handler/Swipeable' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { + archiveMicrotask, + deleteMicroTaskById, + markTodayAsCompleted +} from '../../actions/habit/habit-actions' +import { fonts, StyleProps } from '../../styles/themes' +import { MicroTask } from '../../Types/Microtask' +import TranslatedText from '../TranslatedText' +import ActionComplete from './ActionComplete' +import TopRow from './TopRow' + +export const cardHeight = 100 + +interface Props { + task: MicroTask +} + +const completedToday = (task: MicroTask) => { + const today = Moment() + const microTaskStart = Moment(task.date).startOf('day') + const difference = today.diff(microTaskStart, 'days') + return task.days[difference] !== 0 +} + +const HabitCardManage = (props: Props) => { + const ref: any = useRef() + const dispatch = useDispatch() + const period = props.task.period.toLowerCase() + const completed = completedToday(props.task) + const daysLeft = props.task.days ? props.task.days.length : 0 + + const markCompleted = () => { + dispatch(markTodayAsCompleted(props.task)) + ref!.current!.close() + } + + const deleteHabit = () => { + dispatch(deleteMicroTaskById(props.task.id)) + } + + const archiveHabit = () => { + dispatch(archiveMicrotask(props.task)) + } + + const renderLeftActions = (progress: any, dragX: any) => { + const animation = dragX.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1] + }) + return ( + + ) + } + + const renderRightActions = (progress: any, dragX: any) => { + const animation = dragX.interpolate({ + inputRange: [-50, 0], + outputRange: [1, 0] + }) + return ( + + ) + } + + return ( + + + + + + + {props.task.title} + + + + + + ) +} + +export default memo(HabitCardManage) + +const Card = styled.View` + margin: 10px 20px; + border-radius: 5px; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + padding: 20px 10px; + box-shadow: 1px 1px 15px rgba(32, 33, 37, 0.1); +` + +export const StreakRow = styled.View` + flex-direction: row; + margin-bottom: 10px; +` + +export const Streak = styled(TranslatedText)` + margin-left: 10px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const ActionRow = styled.TouchableOpacity` + flex: 1; +` + +interface ActionProps extends StyleProps { + completedToday: boolean +} + +const Action = styled.Text` + font-family: ${fonts.medium}; + font-size: 15px; + text-decoration: ${(props: ActionProps) => + props.completedToday ? 'line-through' : 'none'}; + color: ${(props: ActionProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/HabitCard/TopRow.tsx b/src/components/HabitCard/TopRow.tsx new file mode 100644 index 0000000..d4d6b9b --- /dev/null +++ b/src/components/HabitCard/TopRow.tsx @@ -0,0 +1,71 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import { Period } from '../../Types/State/Periods' +import { IconBold } from '../iconRegular' +import TranslatedText from '../TranslatedText' + +interface Props { + period: Period | string + daysLeft: number +} + +// const bgColor = cardColors[props.task.period]?.backgroundColor; +// const accent = cardColors[props.task.period]?.accentColor; + +export const getIcon = (period?: string): { color: string; icon: string } => { + if (!period) return { icon: 'sun', color: 'red' } + + switch (period.toLowerCase()) { + case Period.morning: + return { icon: 'daySunrise', color: colors.morningAccent } + case Period.afternoon: + return { icon: 'sun', color: colors.afternoonAccent } + case Period.evening: + return { icon: 'daySunset', color: colors.eveningAccent } + default: + return { icon: 'sun', color: 'red' } + } +} + +const TopRow = (props: Props) => { + const { period, daysLeft } = props + + const { color, icon } = getIcon(period) + + return ( + + + + + TASK_DAYS_LEFT + + ) +} + +export default memo(TopRow) + +const Container = styled.View` + flex-direction: row; + align-items: center; + margin-bottom: 15px; +` + +interface TimeProps { + accent: string +} + +const Time = styled(TranslatedText)` + font-size: 11px; + color: ${(props: TimeProps) => props.accent}; + font-family: ${fonts.medium}; + margin: 0px 20px 0px 10px; +` + +export const Streak = styled(TranslatedText)` + margin-left: 10px; + font-size: 12px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/HabitList/HabitList.tsx b/src/components/HabitList/HabitList.tsx new file mode 100644 index 0000000..1f9362d --- /dev/null +++ b/src/components/HabitList/HabitList.tsx @@ -0,0 +1,129 @@ +import { useNavigation } from '@react-navigation/native' +import { toggleNewHabitModal } from '@actions/modal/modal-actions' +import React, { memo, ReactElement } from 'react' +import { SectionList } from 'react-native' +import { TouchableOpacity } from 'react-native-gesture-handler' +import { useDispatch, useSelector } from 'react-redux' +import { getHabitSections } from 'store/Selectors/habit-selectors/habit-selectors' +import styled from 'styled-components/native' +import HabitCard from '../HabitCard/HabitCard' +import { H3 } from '../Primitives/Primitives' +import TranslatedText from '../TranslatedText' +import { getEditMode } from '../../store/Selectors/ManualDataSelectors' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' + +type Props = { + refreshControl?: ReactElement + footer?: ReactElement + header?: ReactElement +} + +const HabitList = (props: Props) => { + const editMode = useSelector(getEditMode) + const { refreshControl, footer, header } = props + const dispatch = useDispatch() + const navigation = useNavigation() + const sections = useSelector(getHabitSections) + + const renderItem = ({ item, index }: any) => { + return + } + + const goToHabits = () => { + navigation.navigate('Habits') + } + + const toggleModal = () => { + dispatch(toggleNewHabitModal()) + } + + const keyExtractor = (item: any, index: number) => { + return `habit_${item.title}_${index}` + } + + return ( + ( + + {header} + + +

HABIT.HABIT_TITLE

+
+ + + +
+
+ )} + keyExtractor={keyExtractor} + sections={sections} + renderItem={renderItem} + ListFooterComponent={() => ( + + + SEE_ALL + + {footer} + + )} + /> + ) +} + +export default memo(HabitList) + +const List = styled(SectionList).attrs((props: StyleProps) => ({ + contentContainerStyle: { + backgroundColor: props.theme.PRIMARY_BACKGROUND_COLOR + } +}))` + background-color: ${colors.radiantBlue}; +` + +const Fill = styled.View` + background-color: ${({ theme }) => theme.PRIMARY_BACKGROUND_COLOR}; + width: 100%; +` + +const TitleRow = styled.View` + margin-top: 30px; + padding: 0px 20px; + flex: 1; + flex-direction: row; + justify-content: space-between; + align-items: center; +` + +const NewHabitButton = styled.TouchableOpacity` + padding: 3px; + border-radius: 50px; + justify-content: center; + align-items: center; +` + +const ShowAllText = styled(TranslatedText)` + font-family: ${fonts.bold}; + color: ${colors.radiantBlue}; +` + +const FooterRow = styled.View` + margin-top: 20px; + margin-bottom: 60px; + padding: 0px 20px; + flex-direction: row; + justify-content: flex-end; +` + +const TitleContainer = styled.View` + flex: 1; +` diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..c9240c9 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import { Animated, StyleSheet } from 'react-native' +import { ifIphoneX, getStatusBarHeight } from 'react-native-iphone-x-helper' +import TranslatedText from './TranslatedText' +import { HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT } from '../helpers/Dimensions' + +const c = StyleSheet.create({ + header: { + ...ifIphoneX({ paddingTop: getStatusBarHeight() + 20 }, { paddingTop: 20 }), + paddingBottom: 20, + position: 'absolute', + left: 0, + top: 0, + right: 0, + backgroundColor: 'white', + zIndex: 2, + justifyContent: 'center', + flexDirection: 'row', + alignItems: 'center' + }, + headerTitle: { + fontSize: 17, + fontWeight: 'bold' + } +}) + +const Header = (props: any) => { + const headerOpacity = () => ({ + opacity: props.yOffset.interpolate({ + inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [0, 1], + extrapolate: 'clamp' + }) + }) + + return ( + + {/* */} + {props.title} + + ) +} + +export default Header diff --git a/src/components/IAPComponents/PerkList.spec.tsx b/src/components/IAPComponents/PerkList.spec.tsx new file mode 100644 index 0000000..a0c6de8 --- /dev/null +++ b/src/components/IAPComponents/PerkList.spec.tsx @@ -0,0 +1,10 @@ +import 'react-native' +import React from 'react' +import { matchComponentToSnapshot } from '../../helpers/snapshot' +import PerkList from './PerkList' + +describe('', () => { + it('it renders correctly', () => { + matchComponentToSnapshot() + }) +}) diff --git a/src/components/IAPComponents/PerkList.tsx b/src/components/IAPComponents/PerkList.tsx new file mode 100644 index 0000000..c4a5a58 --- /dev/null +++ b/src/components/IAPComponents/PerkList.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import TranslatedText from '../TranslatedText' + +const slides = [ + { + title: 'PURCHASE_COACHING_SLIDE_TITLE_1', + text: 'PURCHASE_COACHING_SLIDE_TEXT_1' + }, + { + title: 'PURCHASE_COACHING_SLIDE_TITLE_2', + text: 'PURCHASE_COACHING_SLIDE_TEXT_2' + }, + { + title: 'PURCHASE_COACHING_SLIDE_TITLE_3', + text: 'PURCHASE_COACHING_SLIDE_TEXT_3' + } +] + +const PerkList = () => { + return ( + + {slides.map((perk, index) => ( + + + {perk.text} + + ))} + + ) +} + +export default PerkList + +const Icon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.SECONDARY_TEXT_COLOR +}))` + margin-right: 10px; +` + +const PerksList = styled.View` + margin: 30px; + padding: 0px 10px; +` + +const Perk = styled.View` + flex-direction: row; + align-items: center; + margin-bottom: 10px; +` + +const PerkText = styled(TranslatedText)` + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.medium}; + font-size: 15px; +` diff --git a/src/components/IAPComponents/SubscriptionItem.spec.tsx... b/src/components/IAPComponents/SubscriptionItem.spec.tsx... new file mode 100644 index 0000000..bd5766a --- /dev/null +++ b/src/components/IAPComponents/SubscriptionItem.spec.tsx... @@ -0,0 +1,20 @@ +import "react-native"; +import React from "react"; +import { matchComponentToSnapshot } from "../../helpers/snapshot"; +import SubscriptionItem from "./SubscriptionItem"; +import { Subscription } from "react-native-iap"; + +describe("", () => { + const subscription: Subscription = { + type: "sub", + productId: "fi.nyxo.sleepcoaching.monthly", + title: "Nyxo Coaching", + description: "1 Month Subscription", + currency: "€", + price: "9.99", + localizedPrice: "€ 9.99" + }; + it("renders correctly", () => { + matchComponentToSnapshot(); + }); +}); diff --git a/src/components/IAPComponents/SubscriptionItem.tsx b/src/components/IAPComponents/SubscriptionItem.tsx new file mode 100644 index 0000000..1a9196b --- /dev/null +++ b/src/components/IAPComponents/SubscriptionItem.tsx @@ -0,0 +1,120 @@ +import React from 'react' +import { PurchasesPackage } from 'react-native-purchases' +import { useDispatch } from 'react-redux' +import styled from 'styled-components/native' +import { purchaseSubscription } from '../../actions/subscription/subscription-actions' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +const SubscriptionItem = ({ + subscription +}: { + subscription: PurchasesPackage +}) => { + const dispatch = useDispatch() + + const { + packageType, + product: { price, title, price_string } + } = subscription + + const purchaseItem = async () => { + dispatch(purchaseSubscription(subscription)) + } + + const perMonthPrice = + Math.ceil((price / monthlyPrice[packageType]) * 100) / 100 + + return ( + + ) +} + +export default SubscriptionItem + +const monthlyPrice = { + MONTHLY: 1, + THREE_MONTH: 3, + ANNUAL: 12, + UNKNOWN: 1, + CUSTOM: 1, + LIFETIME: 1, + SIX_MONTH: 6, + WEEKLY: 0.25, + TWO_MONTH: 2 +} + +const TimeContainer = styled.View` + background-color: ${(props: StyleProps) => + props.theme.mode === 'light' ? colors.afternoon : colors.afternoonAccent}; + flex: 1; + height: 100%; + width: 100%; + justify-content: center; + + padding: 20px 10px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +` + +const Time = styled.Text` + font-family: ${fonts.medium}; + font-size: 18px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Months = styled(TranslatedText)` + font-family: ${fonts.medium}; + text-transform: uppercase; + margin-top: 5px; + font-size: 13px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Button = styled.TouchableOpacity` + flex-direction: row; + flex: 1; +` + +const SubscriptionOption = styled.View` + align-items: center; + flex: 1; + flex-direction: row; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; + border-radius: 5px; + margin: 10px 5px; + min-height: 100px; + elevation: 3; + box-shadow: ${(props: StyleProps) => props.theme.SHADOW}; +` + +const PriceContainer = styled.View` + justify-content: center; + align-items: center; + padding: 20px 10px; +` +const Price = styled.Text` + font-size: 15px; + font-family: ${fonts.bold}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Offer = styled(TranslatedText)` + font-size: 12px; + margin-top: 5px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-family: ${fonts.medium}; +` diff --git a/src/components/IAPComponents/__snapshots__/PerkList.spec.tsx.snap b/src/components/IAPComponents/__snapshots__/PerkList.spec.tsx.snap new file mode 100644 index 0000000..6c4d28e --- /dev/null +++ b/src/components/IAPComponents/__snapshots__/PerkList.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` it renders correctly 1`] = `"View"`; diff --git a/src/components/InfoRow.tsx b/src/components/InfoRow.tsx new file mode 100644 index 0000000..7655013 --- /dev/null +++ b/src/components/InfoRow.tsx @@ -0,0 +1,44 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { constants, fonts, StyleProps } from '../styles/themes' +import TranslatedText from './TranslatedText' + +interface InfoRowProps { + title: string + figure: string +} + +const InfoRow = (props: InfoRowProps) => { + return ( + + {props.title} +
{props.figure}
+
+ ) +} + +export default memo(InfoRow) + +const Row = styled.View` + padding: 20px 0px; + margin: 0px 20px; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-bottom-width: ${constants.hairlineWidth}px; + border-bottom-color: ${(props) => props.theme.SECONDARY_TEXT_COLOR}; +` + +export const Title = styled(TranslatedText)` + text-align: center; + font-size: 17px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Figure = styled.Text` + text-align: center; + font-size: 17px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` diff --git a/src/components/Lesson/ExtraInfo.tsx b/src/components/Lesson/ExtraInfo.tsx new file mode 100644 index 0000000..fba904c --- /dev/null +++ b/src/components/Lesson/ExtraInfo.tsx @@ -0,0 +1,34 @@ +import { Document } from '@contentful/rich-text-types' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { H3 } from '../Primitives/Primitives' +import RichText from '../RichText' + +interface Props { + additionalInformation?: Document +} + +const ExtraInfo = ({ additionalInformation }: Props) => { + const correctFormat = typeof additionalInformation === 'object' + + return ( + <> + {correctFormat && ( + + LESSON_EXTRA_INFORMATION + + + )} + + ) +} + +export default memo(ExtraInfo) + +const Container = styled.View` + margin: 20px 20px 100px; +` + +const Title = styled(H3)` + margin-bottom: 30px; +` diff --git a/src/components/Lesson/LessonContent.tsx b/src/components/Lesson/LessonContent.tsx new file mode 100644 index 0000000..4704d71 --- /dev/null +++ b/src/components/Lesson/LessonContent.tsx @@ -0,0 +1,30 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { Document } from '@contentful/rich-text-types' +import RichText from '../RichText' + +interface Props { + handleOnLayout: Function + lessonContent?: Document +} + +const LessonContent = (props: Props) => { + const { lessonContent } = props + + const contentIsHtml = typeof lessonContent === 'string' + if (!lessonContent || contentIsHtml) { + return null + } + + return ( + + + + ) +} + +export default memo(LessonContent) + +const Container = styled.View` + margin: 20px; +` diff --git a/src/components/Lesson/LessonCover.tsx b/src/components/Lesson/LessonCover.tsx new file mode 100644 index 0000000..202d921 --- /dev/null +++ b/src/components/Lesson/LessonCover.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import LinearGradient from 'react-native-linear-gradient' +import Animated from 'react-native-reanimated' +import styled from 'styled-components/native' +import { + HEADER_MAX_HEIGHT, + HEADER_MIN_HEIGHT, + WIDTH +} from '../../helpers/Dimensions' +import { StyleProps } from '../../styles/themes' +import AnimatedFastImage from '../AnimatedFastImage/AnimatedFastImage' + +interface Props { + yOffset: any + cover: string +} + +const LessonCover = (props: Props) => { + const { yOffset, cover } = props + const headerHeight = (yOffset: any) => ({ + transform: [ + { + scale: yOffset.interpolate({ + inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [2, 1], + extrapolateRight: Animated.Extrapolate.CLAMP + }) + } + ], + opacity: yOffset.interpolate({ + inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], + outputRange: [1, 0], + extrapolateRight: Animated.Extrapolate.CLAMP, + extrapolateLeft: Animated.Extrapolate.CLAMP + }) + }) + + return ( + + + + + ) +} + +export default LessonCover + +const Container = styled.View` + height: ${HEADER_MAX_HEIGHT}; + width: 100%; + overflow: hidden; + position: absolute; + background-color: ${(props: StyleProps) => + props.theme.PRIMARY_BACKGROUND_COLOR}; +` + +const Gradient = styled(LinearGradient).attrs((props: StyleProps) => ({ + colors: props.theme.GRADIENT +}))` + position: absolute; + left: 0; + top: 0; + right: 0; + z-index: 10; + bottom: 0; +` + +const CoverPhoto = styled(AnimatedFastImage)` + z-index: 0; + width: 100%; + height: 100%; +` diff --git a/src/components/Lesson/RateLesson.tsx b/src/components/Lesson/RateLesson.tsx new file mode 100644 index 0000000..56d45bd --- /dev/null +++ b/src/components/Lesson/RateLesson.tsx @@ -0,0 +1,80 @@ +import * as Analytics from 'appcenter-analytics' +import React, { memo, useState } from 'react' +import Intercom from 'react-native-intercom' +import { AirbnbRating, Rating } from 'react-native-ratings' +import styled from 'styled-components/native' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +interface Props { + lesson?: string +} + +const RateLesson = (props: Props) => { + const [showThanks, setShowThanks] = useState(false) + + const submitRating = async (rating: number) => { + await Analytics.trackEvent(`Rated lesson ${props.lesson}`, { + rating: `${rating}` + }) + Intercom.logEvent(`Rated lesson ${props.lesson}`, { rating: `${rating}` }) + await setShowThanks(true) + } + + return ( + + RATE_LESSON + WHY_RATE_LESSON + + + {showThanks ? THANK_YOU_FOR_RATING : null} + + + ) +} + +export default memo(RateLesson) + +const Container = styled.View` + padding: 20px 20px 50px; + background-color: ${(props: StyleProps) => + props.theme.SECONDARY_BACKGROUND_COLOR}; +` + +const Title = styled(TranslatedText)` + font-size: 20px; + text-align: center; + font-family: ${fonts.bold}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Subtitle = styled(TranslatedText)` + margin: 20px 0px; + font-size: 13px; + text-align: center; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` +const ThankYou = styled(TranslatedText)` + margin: 20px 0px; + font-size: 13px; + text-align: center; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` + +const CustomRating = styled(Rating).attrs((props: StyleProps) => ({ + tintColor: props.theme.PRIMARY_BACKGROUND_COLOR, + ratingBackgroundColor: props.theme.PRIMARY_BACKGROUND_COLOR, + ratingColor: colors.radiantBlue +}))`` + +const ThanksContainer = styled.View` + min-height: 20px; +` diff --git a/src/components/LessonComponents/AuthorCard.tsx b/src/components/LessonComponents/AuthorCard.tsx new file mode 100644 index 0000000..f2327f0 --- /dev/null +++ b/src/components/LessonComponents/AuthorCard.tsx @@ -0,0 +1,54 @@ +import React, { memo } from 'react' +import FastImage from 'react-native-fast-image' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' + +interface AuthorCardProps { + avatarURL: string + name: string + credentials: string +} + +const AuthorCard = (props: AuthorCardProps) => { + return ( + + + + {props.name} + {props.credentials} + + + ) +} + +export default memo(AuthorCard) + +const Card = styled.View` + flex-direction: row; + align-items: center; + margin-right: 10px; +` + +const Avatar = styled(FastImage)` + width: 30px; + height: 30px; + border-radius: 15px; + overflow: hidden; + margin-right: 20px; +` + +const Name = styled.Text` + font-size: 12px; + font-family: ${fonts.bold}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Container = styled.View` + flex: 1; +` + +const Credentials = styled.Text` + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-size: 11px; +` diff --git a/src/components/LessonComponents/ExampleHabitSection.tsx b/src/components/LessonComponents/ExampleHabitSection.tsx new file mode 100644 index 0000000..2f01721 --- /dev/null +++ b/src/components/LessonComponents/ExampleHabitSection.tsx @@ -0,0 +1,65 @@ +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { FlatList } from 'react-native' +import { ExampleHabit as ExampleHabitType } from '../../Types/CoachingContentState' +import { H3Margin } from '../Primitives/Primitives' +import { fonts, StyleProps } from '../../styles/themes' +import ExampleHabit from '../HabitCard/ExampleHabit' +import TranslatedText from '../TranslatedText' +import keyExtractor from '../../helpers/KeyExtractor' + +const ExampleHabitSection = ({ + habits +}: { + habits: ExampleHabitType[] | undefined +}) => { + if (!habits) return null + + const renderHabit = ({ + item: habit, + index + }: { + item: ExampleHabitType + index: number + }) => { + return ( + + ) + } + + return ( + <> +

EXAMPLE_HABITS

+ TRY_THIS_HABIT + + + ) +} + +export default memo(ExampleHabitSection) + +const TextSmall = styled(TranslatedText)` + font-family: ${fonts.medium}; + font-size: 13px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + margin: 10px 20px; +` + +// EXAMPLE_HABIT_EXPLANATION + +const H3 = styled(H3Margin)` + margin-top: 20px; +` diff --git a/src/components/LessonComponents/LargeAuthorCard.tsx b/src/components/LessonComponents/LargeAuthorCard.tsx new file mode 100644 index 0000000..a792c2f --- /dev/null +++ b/src/components/LessonComponents/LargeAuthorCard.tsx @@ -0,0 +1,59 @@ +import React, { memo } from 'react' +import { View } from 'react-native' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' +import TranslatedText from '../TranslatedText' + +interface AuthorCardProps { + avatarURL: string + name: string + credentials: string +} + +const LargeAuthorCard = ({ avatarURL, name, credentials }: AuthorCardProps) => { + return ( + + + + {name} + {credentials} + + + ) +} + +export default memo(LargeAuthorCard) + +const Card = styled.View` + flex-direction: row; + align-items: center; + margin-bottom: 20px; +` + +const Avatar = styled.Image` + width: 50px; + height: 50px; + border-radius: 50px; + overflow: hidden; + margin-right: 20px; +` + +const Name = styled.Text` + font-family: ${fonts.bold}; + font-size: 17px; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; +` + +const Credentials = styled.Text` + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.PRIMARY_TEXT_COLOR}; + font-size: 12px; +` + +const Title = styled(TranslatedText)` + font-family: ${fonts.medium}; + font-size: 13px; + text-transform: uppercase; + margin-bottom: 5px; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; +` diff --git a/src/components/LessonComponents/LessonListItem.tsx b/src/components/LessonComponents/LessonListItem.tsx new file mode 100644 index 0000000..17b1ee5 --- /dev/null +++ b/src/components/LessonComponents/LessonListItem.tsx @@ -0,0 +1,233 @@ +import { + completeLesson, + selectLesson +} from '@actions/coaching/coaching-actions' +import Analytics from 'appcenter-analytics' +import React, { memo } from 'react' +import { Animated } from 'react-native' +import FastImage from 'react-native-fast-image' +import { BorderlessButton } from 'react-native-gesture-handler' +import Swipeable from 'react-native-gesture-handler/Swipeable' +import { useDispatch, useSelector } from 'react-redux' +import { CombinedLesson } from 'store/Selectors/coaching-selectors/coaching-selectors' +import styled from 'styled-components/native' +import ROUTE from 'config/routes/Routes' +import { useNavigation } from '@react-navigation/core' +import { getReadingTime } from '../../helpers/reading-time' +import { getActiveCoaching } from '../../store/Selectors/subscription-selectors/SubscriptionSelectors' +import colors from '../../styles/colors' +import { fonts, StyleProps } from '../../styles/themes' +import IconBold from '../iconBold' +import TranslatedText, { AnimatedTranslatedText } from '../TranslatedText' + +interface Props { + lesson: CombinedLesson + locked?: boolean +} + +const LessonListItem = ({ lesson, locked }: Props) => { + const dispatch = useDispatch() + const { navigate } = useNavigation() + const hasActiveCoaching = useSelector(getActiveCoaching) + const time = getReadingTime(lesson.lessonContent) + + const handlePress = () => { + if (hasActiveCoaching) { + Analytics.trackEvent(`Open lesson ${lesson.lessonName}`) + dispatch(selectLesson(lesson.slug)) + navigate(ROUTE.LESSON, {}) + } + } + + const markComplete = async () => { + dispatch(completeLesson(lesson.slug)) + } + + const author = lesson.authorCards + ? { + name: lesson.authorCards[0]?.name + } + : { + name: 'Pietari Nurmi' + } + + const renderRightActions = (progress: any, dragX: any) => { + if (!hasActiveCoaching) { + return + } + const trans = progress.interpolate({ + inputRange: [-50, 0], + outputRange: [0, 1] + }) + return ( + + + MARK_COMPLETE + + + + ) + } + + return ( + + + + + {lesson.lessonName} + {author.name} + + + + {time === 1 ? 'READING_TIME.SINGULAR' : 'READING_TIME.PLURAL'} + + + {lesson.exampleHabit?.length ? ( + <> + + + {lesson.exampleHabit?.length > 1 + ? 'HABITS_COUNT_SHORT' + : 'HABIT_COUNT_SHORT'} + + + ) : null} + + + + + + + {lesson.completed ? ( + + ) : null} + + + + + + ) +} + +export default memo(LessonListItem) + +const NoAction = styled.View`` + +const StyledIcon = styled(IconBold).attrs(({ theme }) => ({ + fill: theme.SECONDARY_TEXT_COLOR +}))` + margin-right: 5px; +` + +const HabitCount = styled(TranslatedText)` + margin: 0px 5px 0px 5px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; + font-size: 12px; +` + +const Touchable = styled.TouchableOpacity` + padding: 15px 20px 15px 20px; + background-color: ${({ theme }) => theme.PRIMARY_BACKGROUND_COLOR}; +` + +const Container = styled.View` + flex-direction: row; + align-items: center; + background-color: ${({ theme }) => theme.PRIMARY_BACKGROUND_COLOR}; +` +const Author = styled.Text` + font-size: 13px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +const LessonName = styled.Text` + font-size: 15px; + font-family: ${fonts.bold}; + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; + margin-bottom: 5px; +` + +const ReadingTime = styled(TranslatedText)` + font-size: 12px; + margin-right: 10px; + font-family: ${fonts.medium}; + color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; +` + +interface CompletedProps { + readonly completed?: boolean +} +const Completed = styled.View` + width: 25px; + height: 25px; + border-radius: 5px; + position: absolute; + left: 0px; + bottom: 0px; + z-index: 20; + align-items: center; + justify-content: center; + overflow: hidden; + background-color: ${({ completed }: CompletedProps) => + completed ? colors.radiantBlue : 'transparent'}; +` + +const LessonInfo = styled.View` + flex: 1; + margin-right: 10px; + justify-content: flex-start; +` + +const ImageContainer = styled.View` + background-color: gray; + border-radius: 10px; +` + +const WeekImage = styled(FastImage)` + flex: 1; + width: 60px; + height: 60px; + border-radius: 5px; +` + +const SlideAction = styled(BorderlessButton)` + flex-direction: row; + align-items: center; +` + +const Icon = styled(IconBold).attrs(({ theme }) => ({ + fill: theme.PRIMARY_TEXT_COLOR +}))`` + +const ButtonText = styled(AnimatedTranslatedText)` + color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; + font-family: ${fonts.bold}; + margin: 0px 5px; +` + +const SlideContainer = styled(Animated.View)` + margin: 0px 20px; + flex-direction: row; +` + +const InfoRow = styled.View` + flex-direction: row; + align-items: center; + margin-top: 5px; +` diff --git a/src/components/LessonComponents/ReadingTime.tsx b/src/components/LessonComponents/ReadingTime.tsx new file mode 100644 index 0000000..2d0c602 --- /dev/null +++ b/src/components/LessonComponents/ReadingTime.tsx @@ -0,0 +1,67 @@ +import { Document } from '@contentful/rich-text-types' +import React, { memo } from 'react' +import styled from 'styled-components/native' +import { getReadingTime } from '../../helpers/reading-time' +import { fonts, StyleProps } from '../../styles/themes' +import { IconBold } from '../iconRegular' +import TranslatedText from '../TranslatedText' + +const ReadingTime = ({ + content, + habitCount +}: { + content?: Document + habitCount?: number +}) => { + if (!content) return null + + const time = getReadingTime(content) + + return ( + + + + {time === 1 ? 'READING_TIME.SINGULAR' : 'READING_TIME.PLURAL'} + + {habitCount ? ( + <> + + + {habitCount > 1 ? 'HABITS_COUNT' : 'HABIT_COUNT'} + + + ) : null} + + ) +} + +export default memo(ReadingTime) + +const ReadTime = styled(TranslatedText)` + margin: 0px 25px 0px 5px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-size: 13px; +` + +const HabitCount = styled(TranslatedText)` + margin: 0px 5px 0px 5px; + font-family: ${fonts.medium}; + color: ${(props: StyleProps) => props.theme.SECONDARY_TEXT_COLOR}; + font-size: 13px; +` + +const Container = styled.View` + margin: 20px 20px 0px; + align-items: center; + padding: 10px; + flex-direction: row; + border-top-color: ${(props: StyleProps) => props.theme.HAIRLINE_COLOR}; + border-top-width: 1px; + border-bottom-color: ${(props: StyleProps) => props.theme.HAIRLINE_COLOR}; + border-bottom-width: 1px; +` + +const StyledIcon = styled(IconBold).attrs((props: StyleProps) => ({ + fill: props.theme.SECONDARY_TEXT_COLOR +}))`` diff --git a/src/components/LessonComponents/SectionFooter.tsx b/src/components/LessonComponents/SectionFooter.tsx new file mode 100644 index 0000000..ca5867e --- /dev/null +++ b/src/components/LessonComponents/SectionFooter.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import styled from 'styled-components/native' +import { fonts, StyleProps } from '../../styles/themes' + +interface Props { + title: string + description: any +} + +const SectionFooter = (props: Props) => { + const { description } = props + + if (!description) return null + + return