version 0.2.0
4
.expo-shared/assets.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
29
.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: ID PASS - MOSIP Resident Application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
npm install
|
||||
- name: Build App Newlogic Release
|
||||
run: |
|
||||
cd android && ./gradlew :app:assembleNewlogicRelease
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: output
|
||||
path: android/app/build/outputs/apk/newlogic/release/
|
||||
retention-days: 1
|
||||
133
.gitignore
vendored
@@ -1,85 +1,80 @@
|
||||
node_modules/
|
||||
.expo/
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8
|
||||
# The following patterns were generated by expo-cli
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
android/app/debug/output-metadata.json
|
||||
android/app/release/output-metadata.json
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
*.hprof
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
# Expo
|
||||
.expo/
|
||||
web-build/
|
||||
dist/
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
# @end expo-cli
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
.vscode/
|
||||
temp/
|
||||
13
.prettierrc.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
24
App.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useContext } from 'react';
|
||||
import AppLoading from 'expo-app-loading';
|
||||
import { AppLayout } from './screens/AppLayout';
|
||||
import { useFont } from './shared/hooks/useFont';
|
||||
import { GlobalContextProvider } from './components/GlobalContextProvider';
|
||||
import { GlobalContext } from './shared/GlobalContext';
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { selectIsReady } from './machines/app';
|
||||
|
||||
const AppInitialization: React.FC = (props) => {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const hasFontsLoaded = useFont();
|
||||
const isReady = useSelector(appService, selectIsReady);
|
||||
|
||||
return isReady && hasFontsLoaded ? <AppLayout /> : <AppLoading />;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<GlobalContextProvider>
|
||||
<AppInitialization />
|
||||
</GlobalContextProvider>
|
||||
);
|
||||
}
|
||||
48
README.md
@@ -1,2 +1,48 @@
|
||||
# inji
|
||||
This is a mobile id client.
|
||||
This is a mobile id client.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Be sure to have the following build tools installed before proceeding:
|
||||
|
||||
- [Gradle](https://gradle.org/install/)
|
||||
- [Java 8](https://www.oracle.com/ph/java/technologies/javase/javase8-archive-downloads.html)
|
||||
- [Expo](https://docs.expo.dev/get-started/installation/)
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# Install all dependencies
|
||||
npm install
|
||||
# run dev client
|
||||
npm start
|
||||
# run Mosip ID PASS directly to connected emulator or device (Default)
|
||||
npm run android:newlogic
|
||||
# run Mosip Philippines directly to connected emulator or device
|
||||
npm run android:ph
|
||||
```
|
||||
|
||||
# Building from Source
|
||||
|
||||
## Build via Android Studio
|
||||
|
||||
The app is available in this repository's `frontend/android` directory. Open this directory in Android Studio (version 4.1 and above) and the app can be built and run from there.
|
||||
|
||||
More info here: [Build your app using Android Studio](https://developer.android.com/studio/run)
|
||||
|
||||
## Build via command line
|
||||
|
||||
1. Build for Mosip Philipines
|
||||
```bash
|
||||
npm run build:android:ph
|
||||
```
|
||||
|
||||
2. Build for ID PASS
|
||||
|
||||
```bash
|
||||
npm run build:android:newlogic
|
||||
```
|
||||
|
||||
Note for release builds you will need to have a keystore: [Create a Keystore](https://medium.com/@tom.truyen/create-an-android-keystore-using-keytool-commandline-10399a62e774)
|
||||
|
||||
More info here: [Build your app from the command line](https://developer.android.com/studio/build/building-cmdline)
|
||||
|
||||
28
android/.project
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>MOSIP Resident App</name>
|
||||
<comment>Project android created by Buildship.</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1637742618855</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
13
android/.settings/org.eclipse.buildship.core.prefs
Normal file
@@ -0,0 +1,13 @@
|
||||
arguments=
|
||||
auto.sync=false
|
||||
build.scans.enabled=false
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
||||
gradle.user.home=
|
||||
java.home=C\:/Program Files/OpenJDK/openjdk-11.0.12_7
|
||||
jvm.arguments=
|
||||
offline.mode=false
|
||||
override.workspace.settings=true
|
||||
show.console.view=true
|
||||
show.executions.view=true
|
||||
55
android/app/BUCK
Normal file
@@ -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 = "io.mosip.residentapp",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "io.mosip.residentapp",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
287
android/app/build.gradle
Normal file
@@ -0,0 +1,287 @@
|
||||
plugins {
|
||||
id 'com.gladed.androidgitversion' version '0.4.14'
|
||||
}
|
||||
|
||||
apply plugin: "com.android.application"
|
||||
import com.android.build.OutputFile
|
||||
|
||||
/**
|
||||
* 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. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||
* bundleCommand: "ram-bundle",
|
||||
*
|
||||
* // 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 = [
|
||||
enableHermes: (findProperty('expo.jsEngine') ?: "jsc") == "hermes",
|
||||
bundleInDebug: true,
|
||||
bundleInRelease: true,
|
||||
devDisabledInRelease: true,
|
||||
cliPath: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/cli.js",
|
||||
hermesCommand: new File(["node", "--print", "require.resolve('hermes-engine/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/%OS-BIN%/hermesc",
|
||||
composeSourceMapsPath: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/scripts/compose-source-maps.js",
|
||||
]
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.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", false);
|
||||
|
||||
androidGitVersion {
|
||||
baseCode 1
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
ext {
|
||||
APP_NAME_RELEASE = "@string/app_name"
|
||||
APP_NAME_PH = "@string/app_name_ph"
|
||||
APP_NAME_NEWLOGIC = "@string/app_name_newlogic"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'io.mosip.residentapp'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Update versionName and/or versionCode via git tag <XX.xx.xx>
|
||||
// More info here:
|
||||
// https://github.com/gladed/gradle-android-git-version#3-use-a-git-tag-to-specify-your-version-number-see-semantic-versioning
|
||||
versionName androidGitVersion.name()
|
||||
versionCode androidGitVersion.code()
|
||||
manifestPlaceholders = [
|
||||
APP_NAME : APP_NAME_RELEASE
|
||||
]
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
// TODO add proper release keystore via local.properties
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "mosip"
|
||||
|
||||
productFlavors {
|
||||
ph {
|
||||
versionName defaultConfig.versionName + "-ph"
|
||||
buildConfigField "boolean", "ENABLE_LOG", "true"
|
||||
manifestPlaceholders = [
|
||||
APP_NAME: APP_NAME_PH
|
||||
]
|
||||
dimension "mosip"
|
||||
}
|
||||
newlogic {
|
||||
buildConfigField "boolean", "ENABLE_LOG", "false"
|
||||
versionName defaultConfig.versionName + "-newlogic"
|
||||
manifestPlaceholders = [
|
||||
APP_NAME: APP_NAME_NEWLOGIC
|
||||
]
|
||||
dimension "mosip"
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
def datetime = new Date().format('yyyyMMdd_HHmm')
|
||||
outputFileName = "${defaultConfig.applicationId}-${variant.versionName}_${datetime}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
// If your app supports Android versions before Ice Cream Sandwich (API level 14)
|
||||
// All fresco packages should use the same version
|
||||
if (isGifEnabled || isWebpEnabled) {
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.0.0'
|
||||
}
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation 'com.facebook.fresco:animated-gif:2.0.0'
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation 'com.facebook.fresco:webpsupport:2.0.0'
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation 'com.facebook.fresco:animated-webp:2.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
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'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
debugImplementation files(new File(["node", "--print", "require.resolve('hermes-engine/package.json')"].execute(null, rootDir).text.trim(), "../android/hermes-debug.aar"))
|
||||
releaseImplementation files(new File(["node", "--print", "require.resolve('hermes-engine/package.json')"].execute(null, rootDir).text.trim(), "../android/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: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
|
||||
applyNativeModulesAppBuildGradle(project)
|
||||
apply from: "./eas-build.gradle"
|
||||
19
android/app/build_defs.bzl
Normal file
@@ -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,
|
||||
)
|
||||
BIN
android/app/debug.keystore
Normal file
74
android/app/eas-build.gradle
Normal file
@@ -0,0 +1,74 @@
|
||||
// Build integration with EAS
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
release {
|
||||
// This is necessary to avoid needing the user to define a release signing config manually
|
||||
// If no release config is defined, and this is not present, build for assembleRelease will crash
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// This is necessary to avoid needing the user to define a release build type manually
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def isEasBuildConfigured = false
|
||||
|
||||
tasks.whenTaskAdded {
|
||||
def debug = gradle.startParameter.taskNames.any { it.toLowerCase().contains('debug') }
|
||||
|
||||
if (debug) {
|
||||
return
|
||||
}
|
||||
|
||||
// We only need to configure EAS build once
|
||||
if (isEasBuildConfigured) {
|
||||
return
|
||||
}
|
||||
|
||||
isEasBuildConfigured = true;
|
||||
|
||||
android.signingConfigs.release {
|
||||
def credentialsJson = rootProject.file("../credentials.json");
|
||||
|
||||
if (credentialsJson.exists()) {
|
||||
if (storeFile && !System.getenv("EAS_BUILD")) {
|
||||
println("Path to release keystore file is already set, ignoring 'credentials.json'")
|
||||
} else {
|
||||
try {
|
||||
def credentials = new groovy.json.JsonSlurper().parse(credentialsJson)
|
||||
def keystorePath = Paths.get(credentials.android.keystore.keystorePath);
|
||||
def storeFilePath = keystorePath.isAbsolute()
|
||||
? keystorePath
|
||||
: rootProject.file("..").toPath().resolve(keystorePath);
|
||||
|
||||
storeFile storeFilePath.toFile()
|
||||
storePassword credentials.android.keystore.keystorePassword
|
||||
keyAlias credentials.android.keystore.keyAlias
|
||||
if (credentials.android.keystore.containsKey("keyPassword")) {
|
||||
keyPassword credentials.android.keystore.keyPassword
|
||||
} else {
|
||||
// key password is required by Gradle, but PKCS keystores don't have one
|
||||
// using the keystore password seems to satisfy the requirement
|
||||
keyPassword credentials.android.keystore.keystorePassword
|
||||
}
|
||||
} catch (Exception e) {
|
||||
println("An error occurred while parsing 'credentials.json': " + e.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (storeFile == null) {
|
||||
println("Couldn't find a 'credentials.json' file, skipping release keystore configuration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android.buildTypes.release {
|
||||
signingConfig android.signingConfigs.release
|
||||
}
|
||||
}
|
||||
10
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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:
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package io.mosip.residentapp;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.mosip.residentapp">
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="43.0.0"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@nlpaolo/mosip-resident-app"/>
|
||||
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustPan" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="io.mosip.residentapp"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
127
android/app/src/main/java/io/mosip/residentapp/MainActivity.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package io.mosip.residentapp;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
import expo.modules.ReactActivityDelegateWrapper;
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: The Android permission flow here works
|
||||
* for Android 10 and below, and Android 11,
|
||||
* and under continuous investigation if other manufacturers
|
||||
* fails to work, etc.
|
||||
*/
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
private static final String[] REQUIRED_PERMISSIONS = new String[] {
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
Manifest.permission.ACCESS_WIFI_STATE,
|
||||
Manifest.permission.CHANGE_WIFI_STATE,
|
||||
Manifest.permission.CHANGE_WIFI_MULTICAST_STATE,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
};
|
||||
|
||||
private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "main";
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!hasPermissions(this, REQUIRED_PERMISSIONS)) {
|
||||
this.requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
// TODO Commenting this only for now if permission is not working for other Android 11 manifacturer/devices
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// WifiManager wifi = (WifiManager)getSystemService( Context.WIFI_SERVICE );
|
||||
// if (wifi != null){
|
||||
// WifiManager.MulticastLock lock = wifi.createMulticastLock("IdpassSmartshareExample");
|
||||
// lock.acquire();
|
||||
// }
|
||||
// }
|
||||
// Must add this to onDestroy/onStop or disconnect to save battery
|
||||
// lock.release();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the app was granted all the permissions. Otherwise, returns false.
|
||||
*/
|
||||
private static boolean hasPermissions(Context context, String... permissions) {
|
||||
for (String permission : permissions) {
|
||||
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user acceptance (or denial) of our permission request.
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int grantResult : grantResults) {
|
||||
if (grantResult == PackageManager.PERMISSION_DENIED) {
|
||||
// Toast.makeText(this, R.string.error_missing_permissions, Toast.LENGTH_LONG).show();
|
||||
// connectButton.setEnabled(false);
|
||||
Log.d("Main", "Denied");
|
||||
return;
|
||||
}
|
||||
}
|
||||
recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegateWrapper(
|
||||
this,
|
||||
new ReactActivityDelegate(this, getMainComponentName()) {
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
return new RNGestureHandlerEnabledRootView(MainActivity.this);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.mosip.residentapp;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "main";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegateWrapper(
|
||||
this,
|
||||
new ReactActivityDelegate(this, getMainComponentName()) {
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
return new RNGestureHandlerEnabledRootView(MainActivity.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package io.mosip.residentapp;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
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 expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper(
|
||||
this,
|
||||
new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new ReanimatedJSIModulePackage();
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*
|
||||
* @param context
|
||||
* @param reactInstanceManager
|
||||
*/
|
||||
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("io.mosip.residentapp.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/splashscreen_image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splashscreen_image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splashscreen_image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
74
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/drawable/mosip_logo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
5
android/app/src/main/res/drawable/splashscreen.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@color/splashscreen_background"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_mosip"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_mosip"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_mosip.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_mosip.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_mosip.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_mosip.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 23 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_mosip.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
2
android/app/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources/>
|
||||
7
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
</resources>
|
||||
8
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<string name="app_name">MOSIP Resident App</string>
|
||||
<string name="app_name_ph">MOSIP Resident App - PH</string>
|
||||
<string name="app_name_newlogic">MOSIP Resident App - Newlogic</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
</resources>
|
||||
17
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
<item name="android:editTextStyle">@style/ResetEditText</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
</style>
|
||||
</resources>
|
||||
42
android/build.gradle
Normal file
@@ -0,0 +1,42 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.1.0")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../android"))
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url(new File(["node", "--print", "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), "../dist"))
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
allprojects { repositories { maven { url "$rootDir/../node_modules/expo-camera/android/maven" } } }
|
||||
41
android/gradle.properties
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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.54.0
|
||||
|
||||
# The hosted JavaScript engine
|
||||
# Supported values: expo.jsEngine = "hermes" | "jsc"
|
||||
expo.jsEngine=jsc
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=true
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
183
android/gradlew
vendored
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## 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" "-Xms64m"'
|
||||
|
||||
# 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=`expr $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"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
103
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@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 Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@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="-Xmx64m" "-Xms64m"
|
||||
|
||||
@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
|
||||
9
android/settings.gradle
Normal file
@@ -0,0 +1,9 @@
|
||||
rootProject.name = 'MOSIP Resident App'
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
|
||||
useExpoModules()
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
|
||||
applyNativeModulesSettingsGradle(settings)
|
||||
|
||||
include ':app'
|
||||
36
app.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "MOSIP Resident App",
|
||||
"slug": "mosip-resident-app",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"bundleIdentifier": "io.mosip.residentapp",
|
||||
"buildNumber": "1.0.0",
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"package": "io.mosip.residentapp",
|
||||
"versionCode": 1,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
assets/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/favicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/idpass-logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/mosip-logo.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/splash.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
6
babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
31
components/DeviceInfoList.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { TextItem } from './ui/TextItem';
|
||||
|
||||
export const DeviceInfoList: React.FC<DeviceInfoProps> = (props) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextItem
|
||||
divider
|
||||
label={props.of === 'receiver' ? 'Requested by' : 'Sent by'}
|
||||
text={props.deviceInfo.deviceName}
|
||||
/>
|
||||
<TextItem divider label="Name" text={props.deviceInfo.name} />
|
||||
<TextItem
|
||||
divider
|
||||
label="Device reference number"
|
||||
text={props.deviceInfo.deviceId}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceInfoProps {
|
||||
of: 'sender' | 'receiver';
|
||||
deviceInfo: DeviceInfo;
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
deviceName: string;
|
||||
name: string;
|
||||
deviceId: string;
|
||||
}
|
||||
55
components/EditableListItem.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import { ListItem, Overlay, Input } from 'react-native-elements';
|
||||
import { Text, Column, Row, Button } from './ui';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
|
||||
export const EditableListItem: React.FC<EditableListItemProps> = (props) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newValue, setNewValue] = useState(props.value);
|
||||
|
||||
return (
|
||||
<ListItem bottomDivider onPress={() => setIsEditing(true)}>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title>
|
||||
<Text>{props.label}</Text>
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<Text color={Colors.Grey}>{props.value}</Text>
|
||||
<Overlay
|
||||
overlayStyle={{ padding: 24, elevation: 6 }}
|
||||
isVisible={isEditing}
|
||||
onBackdropPress={dismiss}>
|
||||
<Column width={Dimensions.get('screen').width * 0.8}>
|
||||
<Text>Edit {props.label}</Text>
|
||||
<Input autoFocus value={newValue} onChangeText={setNewValue} />
|
||||
<Row>
|
||||
<Button
|
||||
fill
|
||||
type="clear"
|
||||
title="Cancel"
|
||||
onPress={() => setIsEditing(false)}
|
||||
/>
|
||||
<Button fill title="Save" onPress={edit} />
|
||||
</Row>
|
||||
</Column>
|
||||
</Overlay>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
function edit() {
|
||||
props.onEdit(newValue);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
setNewValue('');
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
interface EditableListItemProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onEdit: (newValue: string) => void;
|
||||
}
|
||||
18
components/GlobalContextProvider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useInterpret } from '@xstate/react';
|
||||
import { appMachine, logState } from '../machines/app';
|
||||
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
|
||||
export const GlobalContextProvider: React.FC = (props) => {
|
||||
const appService = useInterpret(appMachine);
|
||||
|
||||
// TODO: remove in production builds
|
||||
appService.subscribe(logState);
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ appService }}>
|
||||
{props.children}
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
};
|
||||
26
components/LanguageSelector.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
// import { Picker } from '@react-native-community/picker';
|
||||
// import { ItemValue } from '@react-native-community/picker/typings/Picker';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export function LanguageSelector() {
|
||||
// const [language, setLanguage] = useState<ItemValue>(DEFAULT_LANGUAGE);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Icon name="language" color={Colors.Orange} />
|
||||
{/* <Picker
|
||||
mode="dropdown"
|
||||
selectedValue={language}
|
||||
style={{ height: 50, width: 150 }}
|
||||
onValueChange={(itemValue: ItemValue) => setLanguage(itemValue)}>
|
||||
<Picker.Item label="English" value="en" />
|
||||
<Picker.Item label="Tagalog" value="tl" />
|
||||
</Picker> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
18
components/Logo.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { View, Image } from 'react-native';
|
||||
|
||||
export const Logo: React.FC<LogoProps> = (props) => {
|
||||
return (
|
||||
<View>
|
||||
<Image
|
||||
style={{ resizeMode: 'contain', ...props }}
|
||||
source={require('../assets/mosip-logo.png')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogoProps {
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
42
components/MessageOverlay.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, StyleSheet } from 'react-native';
|
||||
import { Overlay, LinearProgress } from 'react-native-elements';
|
||||
import { Column, Text } from './ui';
|
||||
import { Colors, elevation } from './ui/styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...elevation(5),
|
||||
backgroundColor: Colors.White,
|
||||
},
|
||||
});
|
||||
|
||||
export const MessageOverlay: React.FC<MessageOverlayProps> = (props) => {
|
||||
return (
|
||||
<Overlay
|
||||
isVisible={props.isVisible}
|
||||
overlayStyle={styles.overlay}
|
||||
onBackdropPress={props.onBackdropPress}
|
||||
>
|
||||
<Column padding="24" width={Dimensions.get('screen').width * 0.8}>
|
||||
{props.title && (
|
||||
<Text weight="semibold" margin="0 0 12 0">
|
||||
{props.title}
|
||||
</Text>
|
||||
)}
|
||||
{props.message && <Text margin="0 0 12 0">{props.message}</Text>}
|
||||
{props.hasProgress && (
|
||||
<LinearProgress variant="indeterminate" color={Colors.Orange} />
|
||||
)}
|
||||
</Column>
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
interface MessageOverlayProps {
|
||||
isVisible: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
hasProgress?: boolean;
|
||||
onBackdropPress?: () => void;
|
||||
}
|
||||
30
components/PasscodeVerify.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PinInput } from './PinInput';
|
||||
|
||||
export const MAX_PIN = 6;
|
||||
|
||||
export const PasscodeVerify: React.FC<PasscodeVerifyProps> = (props) => {
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVerified) {
|
||||
props.onSuccess();
|
||||
}
|
||||
}, [isVerified]);
|
||||
|
||||
return <PinInput length={MAX_PIN} onDone={verify} />;
|
||||
|
||||
function verify(value: string) {
|
||||
if (props.passcode === value) {
|
||||
setIsVerified(true);
|
||||
} else {
|
||||
props.onError('Passcode did not match.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface PasscodeVerifyProps {
|
||||
passcode: string;
|
||||
onSuccess: () => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
61
components/PinInput.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { StyleSheet, TextInput } from 'react-native';
|
||||
import { usePinInput } from '../machines/pinInput';
|
||||
import { Row } from './ui';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
borderBottomWidth: 1,
|
||||
borderColor: Colors.Grey,
|
||||
color: Colors.Black,
|
||||
flex: 1,
|
||||
fontFamily: 'Poppins_600SemiBold',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
height: 40,
|
||||
lineHeight: 28,
|
||||
margin: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export const PinInput: React.FC<PinInputProps> = (props) => {
|
||||
const { state, send, events } = usePinInput(props.length);
|
||||
const { inputRefs, values } = state.context;
|
||||
const { UPDATE_INPUT, FOCUS_INPUT, KEY_PRESS } = events;
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onDone && values.filter(Boolean).length === inputRefs.length) {
|
||||
props.onDone(values.join(''));
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<Row width="100%">
|
||||
{inputRefs.map((input, index) => (
|
||||
<TextInput
|
||||
selectTextOnFocus
|
||||
keyboardType="numeric"
|
||||
maxLength={1}
|
||||
selectionColor={Colors.Orange}
|
||||
style={styles.input}
|
||||
key={index}
|
||||
ref={input}
|
||||
value={values[index]}
|
||||
// KNOWN ISSUE: https://github.com/facebook/react-native/issues/19507
|
||||
onKeyPress={({ nativeEvent }) => send(KEY_PRESS(nativeEvent.key))}
|
||||
onChangeText={(value: string) =>
|
||||
send(UPDATE_INPUT(value.replace(/[^0-9]/g, ''), index))
|
||||
}
|
||||
onFocus={() => send(FOCUS_INPUT(index))}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
interface PinInputProps {
|
||||
length: number;
|
||||
onDone?: (value: string) => void;
|
||||
}
|
||||
99
components/QrScanner.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Camera } from 'expo-camera';
|
||||
import { BarCodeEvent, BarCodeScanner } from 'expo-barcode-scanner';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
import { Button, Text } from './ui';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { selectIsActive } from '../machines/app';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scannerContainer: {
|
||||
borderWidth: 4,
|
||||
borderColor: Colors.Black,
|
||||
borderRadius: 32,
|
||||
justifyContent: 'center',
|
||||
height: 300,
|
||||
width: 300,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
scanner: {
|
||||
height: 400,
|
||||
},
|
||||
buttonContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
buttonStyle: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
bottom: -90,
|
||||
},
|
||||
});
|
||||
|
||||
export const QrScanner: React.FC<QrScannerProps> = (props) => {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const [hasPermission, setHasPermission] = useState(null);
|
||||
const [scanned, setScanned] = useState(false);
|
||||
|
||||
const isActive = useSelector(appService, selectIsActive);
|
||||
|
||||
const openSettings = () => {
|
||||
Linking.openSettings();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await Camera.requestCameraPermissionsAsync();
|
||||
setHasPermission(response.granted);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && hasPermission === false) {
|
||||
(async () => {
|
||||
const response = await Camera.requestCameraPermissionsAsync();
|
||||
setHasPermission(response.granted);
|
||||
})();
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
if (hasPermission === null) {
|
||||
return <View />;
|
||||
}
|
||||
|
||||
if (hasPermission === false) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Text align="center">
|
||||
This app uses the camera to scan the QR code of another device.
|
||||
</Text>
|
||||
<View style={styles.buttonStyle}>
|
||||
<Button title="Allow access to camera" onPress={openSettings} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.scannerContainer}>
|
||||
<Camera
|
||||
style={styles.scanner}
|
||||
barCodeScannerSettings={{
|
||||
barcodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
|
||||
}}
|
||||
onBarCodeScanned={scanned ? undefined : onBarcodeScanned}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
function onBarcodeScanned(event: BarCodeEvent) {
|
||||
props.onQrFound(event.data);
|
||||
setScanned(true);
|
||||
}
|
||||
};
|
||||
|
||||
interface QrScannerProps {
|
||||
onQrFound: (data: string) => void;
|
||||
}
|
||||
39
components/RotatingIcon.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, Easing } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
|
||||
export const RotatingIcon: React.FC<RotatingIconProps> = (props) => {
|
||||
const rotationValue = useRef(
|
||||
new Animated.Value(props.clockwise ? 0 : 1)
|
||||
).current;
|
||||
|
||||
const rotation = rotationValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Animated.loop(
|
||||
Animated.timing(rotationValue, {
|
||||
toValue: props.clockwise ? 1 : 0,
|
||||
duration: props.duration || 3000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
||||
<Icon name={props.name} size={props.size} color={props.color} />
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
interface RotatingIconProps {
|
||||
name: string;
|
||||
size?: number;
|
||||
duration?: number;
|
||||
clockwise?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
55
components/TextEditOverlay.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Dimensions, StyleSheet } from 'react-native';
|
||||
import { Overlay, Input } from 'react-native-elements';
|
||||
import { Button, Column, Row, Text } from './ui';
|
||||
import { Colors, elevation } from './ui/styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...elevation(5),
|
||||
backgroundColor: Colors.White,
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export const TextEditOverlay: React.FC<EditOverlayProps> = (props) => {
|
||||
const [value, setValue] = useState(props.value);
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
isVisible={props.isVisible}
|
||||
overlayStyle={styles.overlay}
|
||||
onBackdropPress={props.onDismiss}>
|
||||
<Column padding="24" width={Dimensions.get('screen').width * 0.8}>
|
||||
<Text weight="semibold" margin="0 0 16 0">
|
||||
{props.label}
|
||||
</Text>
|
||||
<Input autoFocus value={value} onChangeText={setValue} />
|
||||
<Row>
|
||||
<Button
|
||||
fill
|
||||
type="clear"
|
||||
title="Cancel"
|
||||
onPress={dismiss}
|
||||
margin="0 8 0 0"
|
||||
/>
|
||||
<Button fill title="Save" onPress={() => props.onSave(value)} />
|
||||
</Row>
|
||||
</Column>
|
||||
</Overlay>
|
||||
);
|
||||
|
||||
function dismiss() {
|
||||
setValue('');
|
||||
props.onDismiss();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
interface EditOverlayProps {
|
||||
isVisible: boolean;
|
||||
label: string;
|
||||
value: string;
|
||||
onSave: (value: string) => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
143
components/VidDetails.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import { Image } from 'react-native';
|
||||
import { ListItem } from 'react-native-elements';
|
||||
import { VID, VIDCredential } from '../types/vid';
|
||||
import { Column, Row, Text } from './ui';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
|
||||
export const VidDetails: React.FC<VidDetailsProps> = (props) => {
|
||||
return (
|
||||
<Column>
|
||||
<Row padding="16 24">
|
||||
<Column fill elevation={1} padding="12 16" margin="0 16 0 0">
|
||||
<Text size="smaller" color={Colors.Grey}>
|
||||
Generated
|
||||
</Text>
|
||||
<Text weight="bold" size="smaller">
|
||||
{new Date(props.vid?.generatedOn).toLocaleDateString()}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column fill elevation={1} padding="12 16" margin="0 16 0 0">
|
||||
<Text size="smaller" color={Colors.Grey}>
|
||||
UIN
|
||||
</Text>
|
||||
<Text weight="bold" size="smaller">
|
||||
{props.vid?.uin}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column fill elevation={1} padding="12 16" margin="">
|
||||
<Text size="smaller" color={Colors.Grey}>
|
||||
Status
|
||||
</Text>
|
||||
<Text weight="bold" size="smaller">
|
||||
Valid
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
{props.vid?.credential.biometrics && (
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Photo</ListItem.Subtitle>
|
||||
<ListItem.Content>
|
||||
<Image
|
||||
source={{ uri: props.vid?.credential.biometrics.face }}
|
||||
style={{
|
||||
width: 110,
|
||||
height: 110,
|
||||
resizeMode: 'cover',
|
||||
marginTop: 8,
|
||||
}}
|
||||
/>
|
||||
</ListItem.Content>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Full name</ListItem.Subtitle>
|
||||
<ListItem.Title>{props.vid?.credential.fullName}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Gender</ListItem.Subtitle>
|
||||
<ListItem.Title>
|
||||
{getLocalizedField(props.vid?.credential.gender)}
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Date of birth</ListItem.Subtitle>
|
||||
<ListItem.Title>{props.vid?.credential.dateOfBirth}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Phone number</ListItem.Subtitle>
|
||||
<ListItem.Title>{props.vid?.credential.phone}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Email</ListItem.Subtitle>
|
||||
<ListItem.Title>{props.vid?.credential.email}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Address</ListItem.Subtitle>
|
||||
<ListItem.Title>
|
||||
{getFullAddress(props.vid?.credential)}
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
{Boolean(props.vid?.reason) && (
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Subtitle>Reason for sharing</ListItem.Subtitle>
|
||||
<ListItem.Title>{props.vid?.reason}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
interface VidDetailsProps {
|
||||
vid: VID;
|
||||
}
|
||||
|
||||
interface LocalizedField {
|
||||
language: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function getFullAddress(credential: VIDCredential) {
|
||||
if (!credential) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fields = [
|
||||
'addressLine1',
|
||||
'addressLine2',
|
||||
'addressLine3',
|
||||
'city',
|
||||
'province',
|
||||
];
|
||||
return (
|
||||
fields.map((field) => getLocalizedField(credential[field])).join(', ') +
|
||||
', ' +
|
||||
credential.postalCode
|
||||
);
|
||||
}
|
||||
|
||||
function getLocalizedField(rawField: string) {
|
||||
try {
|
||||
const locales: LocalizedField[] = JSON.parse(rawField);
|
||||
// TODO: language switching
|
||||
return locales.find((locale) => locale.language === 'eng').value;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
107
components/VidItem.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import { useInterpret, useSelector } from '@xstate/react';
|
||||
import { Pressable, StyleSheet } from 'react-native';
|
||||
import { CheckBox, Icon } from 'react-native-elements';
|
||||
import { ActorRefFrom } from 'xstate';
|
||||
import {
|
||||
createVidItemMachine,
|
||||
selectCredential,
|
||||
selectGeneratedOn,
|
||||
selectTag,
|
||||
selectUin,
|
||||
vidItemMachine,
|
||||
} from '../machines/vidItem';
|
||||
import { Column, Row, Text } from './ui';
|
||||
import { Colors } from './ui/styleUtils';
|
||||
import { RotatingIcon } from './RotatingIcon';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
color: Colors.Black,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
loadingTitle: {
|
||||
color: 'transparent',
|
||||
backgroundColor: Colors.Grey5,
|
||||
borderRadius: 4,
|
||||
},
|
||||
subtitle: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
loadingSubtitle: {
|
||||
backgroundColor: Colors.Grey,
|
||||
borderRadius: 4,
|
||||
},
|
||||
container: {
|
||||
backgroundColor: Colors.White,
|
||||
},
|
||||
loadingContainer: {
|
||||
backgroundColor: Colors.Grey6,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export const VidItem: React.FC<VidItemProps> = (props) => {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const machine = useRef(
|
||||
createVidItemMachine(
|
||||
appService.getSnapshot().context.serviceRefs,
|
||||
props.vidKey
|
||||
)
|
||||
);
|
||||
const service = useInterpret(machine.current);
|
||||
const uin = useSelector(service, selectUin);
|
||||
const tag = useSelector(service, selectTag);
|
||||
const credential = useSelector(service, selectCredential);
|
||||
const generatedOn = useSelector(service, selectGeneratedOn);
|
||||
|
||||
return (
|
||||
<Pressable onPress={() => props.onPress(service)} disabled={!credential}>
|
||||
<Row
|
||||
elevation={!credential ? 0 : 2}
|
||||
crossAlign="center"
|
||||
margin={props.margin}
|
||||
backgroundColor={!credential ? Colors.Grey6 : Colors.White}
|
||||
padding="16 24"
|
||||
style={!credential ? styles.loadingContainer : styles.container}>
|
||||
<Column fill margin="0 24 0 0">
|
||||
<Text
|
||||
weight="semibold"
|
||||
style={!credential ? styles.loadingTitle : styles.title}
|
||||
margin="0 0 6 0">
|
||||
{!credential ? '' : tag || uin}
|
||||
</Text>
|
||||
<Text
|
||||
size="smaller"
|
||||
numLines={1}
|
||||
style={!credential ? styles.loadingSubtitle : styles.subtitle}>
|
||||
{!credential ? '' : credential.fullName + ' · ' + generatedOn}
|
||||
</Text>
|
||||
</Column>
|
||||
{credential ? (
|
||||
props.selectable ? (
|
||||
<CheckBox
|
||||
checked={props.selected}
|
||||
checkedIcon={<Icon name="radio-button-checked" />}
|
||||
uncheckedIcon={<Icon name="radio-button-unchecked" />}
|
||||
onPress={() => props.onPress(service)}
|
||||
/>
|
||||
) : (
|
||||
<Icon name="chevron-right" />
|
||||
)
|
||||
) : (
|
||||
<RotatingIcon name="sync" color={Colors.Grey5} />
|
||||
)}
|
||||
</Row>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
interface VidItemProps {
|
||||
vidKey: string;
|
||||
margin?: string;
|
||||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
onPress?: (vidRef?: ActorRefFrom<typeof vidItemMachine>) => void;
|
||||
}
|
||||
84
components/ui/Button.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button as RNEButton,
|
||||
ButtonProps as RNEButtonProps,
|
||||
} from 'react-native-elements';
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Text } from './Text';
|
||||
import { Colors, spacing } from './styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fill: {
|
||||
flex: 1,
|
||||
},
|
||||
solid: {
|
||||
backgroundColor: Colors.Orange,
|
||||
},
|
||||
clear: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: Colors.Orange,
|
||||
},
|
||||
container: {
|
||||
height: 48,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
export const Button: React.FC<ButtonProps> = (props) => {
|
||||
const type = props.type || 'solid';
|
||||
const buttonStyle: StyleProp<ViewStyle> = [styles.fill, styles[type]];
|
||||
|
||||
const containerStyle: StyleProp<ViewStyle> = [
|
||||
styles.container,
|
||||
props.disabled ? styles.disabled : null,
|
||||
props.margin ? spacing('margin', props.margin) : null,
|
||||
];
|
||||
|
||||
const handleOnPress = (event: GestureResponderEvent) => {
|
||||
if (!props.disabled && props.onPress) {
|
||||
props.onPress(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RNEButton
|
||||
buttonStyle={buttonStyle}
|
||||
containerStyle={[props.fill ? styles.fill : null, containerStyle]}
|
||||
type={props.type}
|
||||
raised={props.raised}
|
||||
title={
|
||||
<Text
|
||||
weight="semibold"
|
||||
color={type === 'solid' ? Colors.White : Colors.Orange}>
|
||||
{props.title}
|
||||
</Text>
|
||||
}
|
||||
icon={props.icon}
|
||||
onPress={handleOnPress}
|
||||
loading={props.loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonProps {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
margin?: string;
|
||||
type?: RNEButtonProps['type'];
|
||||
onPress?: RNEButtonProps['onPress'];
|
||||
fill?: boolean;
|
||||
raised?: boolean;
|
||||
loading?: boolean;
|
||||
icon?: RNEButtonProps['icon'];
|
||||
}
|
||||
77
components/ui/Layout.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FlexStyle,
|
||||
StyleProp,
|
||||
SafeAreaView,
|
||||
ViewStyle,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
RefreshControlProps,
|
||||
} from 'react-native';
|
||||
import { elevation, ElevationLevel, spacing } from './styleUtils';
|
||||
|
||||
function createLayout(
|
||||
direction: FlexStyle['flexDirection'],
|
||||
mainAlign?: FlexStyle['justifyContent'],
|
||||
crossAlign?: FlexStyle['alignItems']
|
||||
) {
|
||||
const layoutStyles = StyleSheet.create({
|
||||
base: {
|
||||
flexDirection: direction,
|
||||
justifyContent: mainAlign,
|
||||
alignItems: crossAlign,
|
||||
},
|
||||
fill: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const Layout: React.FC<LayoutProps> = (props) => {
|
||||
const styles: StyleProp<ViewStyle> = [
|
||||
layoutStyles.base,
|
||||
props.fill ? layoutStyles.fill : null,
|
||||
props.padding ? spacing('padding', props.padding) : null,
|
||||
props.margin ? spacing('margin', props.margin) : null,
|
||||
props.backgroundColor ? { backgroundColor: props.backgroundColor } : null,
|
||||
props.width ? { width: props.width } : null,
|
||||
props.height ? { height: props.height } : null,
|
||||
props.align ? { justifyContent: props.align } : null,
|
||||
props.crossAlign ? { alignItems: props.crossAlign } : null,
|
||||
props.elevation ? elevation(props.elevation) : null,
|
||||
props.style ? props.style : null,
|
||||
];
|
||||
|
||||
return props.scroll ? (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles}
|
||||
refreshControl={props.refreshControl}>
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<SafeAreaView style={styles}>{props.children}</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
return Layout;
|
||||
}
|
||||
|
||||
export const Row = createLayout('row');
|
||||
|
||||
export const Column = createLayout('column');
|
||||
|
||||
export const Centered = createLayout('column', 'center', 'center');
|
||||
|
||||
interface LayoutProps {
|
||||
fill?: boolean;
|
||||
align?: FlexStyle['justifyContent'];
|
||||
crossAlign?: FlexStyle['alignItems'];
|
||||
padding?: string;
|
||||
margin?: string;
|
||||
backgroundColor?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
elevation?: ElevationLevel;
|
||||
scroll?: boolean;
|
||||
refreshControl?: React.ReactElement<RefreshControlProps>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
53
components/ui/Modal.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, Modal as RNModal, StyleSheet } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { Column, Row, Text } from '.';
|
||||
import { Colors, ElevationLevel } from './styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modal: {
|
||||
width: Dimensions.get('screen').width,
|
||||
height: Dimensions.get('screen').height,
|
||||
},
|
||||
});
|
||||
|
||||
export const Modal: React.FC<ModalProps> = (props) => {
|
||||
return (
|
||||
<RNModal
|
||||
animationType="slide"
|
||||
style={styles.modal}
|
||||
visible={props.isVisible}
|
||||
onRequestClose={props.onDismiss}>
|
||||
<Column fill>
|
||||
<Row padding="16 32" elevation={props.headerElevation}>
|
||||
{props.headerRight ? (
|
||||
<Icon
|
||||
name="chevron-left"
|
||||
onPress={props.onDismiss}
|
||||
color={Colors.Orange}
|
||||
/>
|
||||
) : null}
|
||||
<Row fill align="center">
|
||||
<Text weight="semibold">{props.headerTitle}</Text>
|
||||
</Row>
|
||||
{props.headerRight || (
|
||||
<Icon
|
||||
name="close"
|
||||
onPress={props.onDismiss}
|
||||
color={Colors.Orange}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
{props.children}
|
||||
</Column>
|
||||
</RNModal>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ModalProps {
|
||||
isVisible: boolean;
|
||||
onDismiss: () => void;
|
||||
headerTitle?: string;
|
||||
headerElevation?: ElevationLevel;
|
||||
headerRight?: React.ReactElement;
|
||||
}
|
||||
60
components/ui/Text.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { StyleProp, TextStyle, StyleSheet, Text as RNText } from 'react-native';
|
||||
import { Colors, spacing } from './styleUtils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
color: Colors.Black,
|
||||
fontSize: 18,
|
||||
lineHeight: 28,
|
||||
},
|
||||
regular: {
|
||||
fontFamily: 'Poppins_400Regular',
|
||||
},
|
||||
semibold: {
|
||||
fontFamily: 'Poppins_600SemiBold',
|
||||
},
|
||||
bold: {
|
||||
fontFamily: 'Poppins_700Bold',
|
||||
},
|
||||
small: {
|
||||
fontSize: 14,
|
||||
lineHeight: 21,
|
||||
},
|
||||
smaller: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
export const Text: React.FC<TextProps> = (props: TextProps) => {
|
||||
const weight = props.weight || 'regular';
|
||||
|
||||
const textStyles: StyleProp<TextStyle> = [
|
||||
styles.base,
|
||||
styles[weight],
|
||||
props.color ? { color: props.color } : null,
|
||||
props.align ? { textAlign: props.align } : null,
|
||||
props.margin ? spacing('margin', props.margin) : null,
|
||||
props.size ? styles[props.size] : null,
|
||||
props.style ? props.style : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<RNText style={textStyles} numberOfLines={props.numLines}>
|
||||
{props.children}
|
||||
</RNText>
|
||||
);
|
||||
};
|
||||
|
||||
interface TextProps {
|
||||
children: React.ReactNode;
|
||||
color?: string;
|
||||
weight?: 'regular' | 'semibold' | 'bold';
|
||||
align?: TextStyle['textAlign'];
|
||||
margin?: string;
|
||||
size?: 'small' | 'smaller' | 'regular';
|
||||
lineHeight?: number;
|
||||
numLines?: number;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
32
components/ui/TextItem.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Column, Text } from '.';
|
||||
import { Colors } from './styleUtils';
|
||||
|
||||
export const TextItem: React.FC<TextItemProps> = (props) => {
|
||||
return (
|
||||
<Column
|
||||
backgroundColor={Colors.White}
|
||||
margin={props.margin}
|
||||
padding={props.label ? '16 24' : '12 24'}
|
||||
style={{
|
||||
borderBottomColor: Colors.Grey6,
|
||||
borderBottomWidth: props.divider ? 1 : 0,
|
||||
}}>
|
||||
{props.label && (
|
||||
<Text size="smaller" color={Colors.Grey} weight="semibold">
|
||||
{props.label}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={Colors.Black} weight={props.label ? 'semibold' : 'regular'}>
|
||||
{props.text}
|
||||
</Text>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
interface TextItemProps {
|
||||
text: string;
|
||||
label?: string;
|
||||
divider?: boolean;
|
||||
margin?: string;
|
||||
}
|
||||
3
components/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Text } from './Text';
|
||||
export { Button } from './Button';
|
||||
export { Row, Column, Centered } from './Layout';
|
||||
50
components/ui/styleUtils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ViewStyle } from 'react-native';
|
||||
|
||||
export const Colors = {
|
||||
Black: '#231F20',
|
||||
Grey: '#B0B0B0',
|
||||
Grey5: '#E0E0E0',
|
||||
Grey6: '#F2F2F2',
|
||||
Orange: '#F2811D',
|
||||
LightGrey: '#FAF9FF',
|
||||
White: '#FFFFFF',
|
||||
Red: '#EB5757',
|
||||
Green: '#219653',
|
||||
};
|
||||
|
||||
export function spacing(type: 'margin' | 'padding', values: string) {
|
||||
const [top, end, bottom, start] = values.split(' ').map(Number);
|
||||
|
||||
return {
|
||||
[`${type}Top`]: top,
|
||||
[`${type}End`]: end != null ? end : top,
|
||||
[`${type}Bottom`]: bottom != null ? bottom : top,
|
||||
[`${type}Start`]: start != null ? start : end != null ? end : top,
|
||||
};
|
||||
}
|
||||
|
||||
export type ElevationLevel = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
export function elevation(level: ElevationLevel): ViewStyle {
|
||||
// https://ethercreative.github.io/react-native-shadow-generator/
|
||||
|
||||
if (level === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = level - 1;
|
||||
|
||||
return {
|
||||
shadowColor: Colors.Black,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: [1, 1, 1, 2, 2][index],
|
||||
},
|
||||
shadowOpacity: [0.18, 0.2, 0.22, 0.23, 0.25][index],
|
||||
shadowRadius: [1.0, 1.41, 2.22, 2.62, 3.84][index],
|
||||
elevation: level,
|
||||
zIndex: level,
|
||||
borderRadius: 4,
|
||||
backgroundColor: Colors.White,
|
||||
};
|
||||
}
|
||||
26
eas.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 0.37.0"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"ph": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"newlogic": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
9
index.android.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
9
index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
127
machines/activityLog.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { EventFrom, send, sendParent, StateFrom } from 'xstate';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
import { AppServices } from '../shared/GlobalContext';
|
||||
import { ACTIVITY_LOG_STORE_KEY } from '../shared/storeKeys';
|
||||
import { StoreEvents } from './store';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
serviceRefs: {} as AppServices,
|
||||
activities: [] as ActivityLog[],
|
||||
},
|
||||
{
|
||||
events: {
|
||||
STORE_RESPONSE: (response: any) => ({ response }),
|
||||
LOG_ACTIVITY: (log: ActivityLog) => ({ log }),
|
||||
REFRESH: () => ({}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const ActivityLogEvents = model.events;
|
||||
|
||||
type StoreResponseEvent = EventFrom<typeof model, 'STORE_RESPONSE'>;
|
||||
type LogActivityEvent = EventFrom<typeof model, 'LOG_ACTIVITY'>;
|
||||
|
||||
export const activityLogMachine = model.createMachine(
|
||||
{
|
||||
id: 'activityLog',
|
||||
context: model.initialContext,
|
||||
initial: 'init',
|
||||
states: {
|
||||
init: {
|
||||
entry: ['loadActivities'],
|
||||
on: {
|
||||
STORE_RESPONSE: {
|
||||
target: 'ready',
|
||||
actions: ['setActivities', sendParent('READY')],
|
||||
},
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
LOG_ACTIVITY: 'logging',
|
||||
REFRESH: 'refreshing',
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
entry: ['storeActivity'],
|
||||
on: {
|
||||
STORE_RESPONSE: {
|
||||
target: 'idle',
|
||||
actions: ['prependActivity'],
|
||||
},
|
||||
},
|
||||
},
|
||||
refreshing: {
|
||||
entry: ['loadActivities'],
|
||||
on: {
|
||||
STORE_RESPONSE: {
|
||||
target: 'idle',
|
||||
actions: ['setActivities'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
loadActivities: send(StoreEvents.GET(ACTIVITY_LOG_STORE_KEY), {
|
||||
to: (context) => context.serviceRefs.store,
|
||||
}),
|
||||
|
||||
setActivities: model.assign({
|
||||
activities: (_, event: StoreResponseEvent) => event.response || [],
|
||||
}),
|
||||
|
||||
storeActivity: send(
|
||||
(_, event: LogActivityEvent) =>
|
||||
StoreEvents.PREPEND(ACTIVITY_LOG_STORE_KEY, event.log),
|
||||
{ to: (context) => context.serviceRefs.store }
|
||||
),
|
||||
|
||||
prependActivity: model.assign({
|
||||
activities: (context, event: StoreResponseEvent) => [
|
||||
event.response,
|
||||
...context.activities,
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function createActivityLogMachine(serviceRefs: AppServices) {
|
||||
return activityLogMachine.withContext({
|
||||
...activityLogMachine.context,
|
||||
serviceRefs,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ActivityLog {
|
||||
_vidKey: string;
|
||||
timestamp: number;
|
||||
deviceName: string;
|
||||
vidLabel: string;
|
||||
action: ActivityLogAction;
|
||||
}
|
||||
|
||||
export type ActivityLogAction =
|
||||
| 'shared'
|
||||
| 'received'
|
||||
| 'deleted'
|
||||
| 'downloaded';
|
||||
|
||||
type State = StateFrom<typeof activityLogMachine>;
|
||||
|
||||
export function selectActivities(state: State) {
|
||||
return state.context.activities;
|
||||
}
|
||||
|
||||
export function selectIsRefreshing(state: State) {
|
||||
return state.matches('ready.refreshing');
|
||||
}
|
||||
262
machines/app.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import {
|
||||
getDeviceId,
|
||||
getDeviceName,
|
||||
getDeviceNameSync,
|
||||
} from 'react-native-device-info';
|
||||
import { EventFrom, spawn, StateFrom } from 'xstate';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
import { authMachine, createAuthMachine } from './auth';
|
||||
import { createSettingsMachine, settingsMachine } from './settings';
|
||||
import { storeMachine } from './store';
|
||||
import { createVidMachine, vidMachine } from './vid';
|
||||
import { createActivityLogMachine, activityLogMachine } from './activityLog';
|
||||
import { createRequestMachine, requestMachine } from './request';
|
||||
import { createScanMachine, scanMachine } from './scan';
|
||||
import { respond } from 'xstate/lib/actions';
|
||||
import { AppServices } from '../shared/GlobalContext';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
info: {} as AppInfo,
|
||||
serviceRefs: {} as AppServices,
|
||||
},
|
||||
{
|
||||
events: {
|
||||
ACTIVE: () => ({}),
|
||||
INACTIVE: () => ({}),
|
||||
OFFLINE: () => ({}),
|
||||
ONLINE: (networkType: NetInfoStateType) => ({ networkType }),
|
||||
REQUEST_DEVICE_INFO: () => ({}),
|
||||
READY: (data?: unknown) => ({ data }),
|
||||
APP_INFO_RECEIVED: (info: AppInfo) => ({ info }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type AppInfoReceived = EventFrom<typeof model, 'APP_INFO_RECEIVED'>;
|
||||
|
||||
export const appMachine = model.createMachine(
|
||||
{
|
||||
id: 'app',
|
||||
context: model.initialContext,
|
||||
initial: 'init',
|
||||
states: {
|
||||
init: {
|
||||
initial: 'store',
|
||||
states: {
|
||||
store: {
|
||||
entry: ['spawnStoreActor', 'logStoreEvents'],
|
||||
on: {
|
||||
READY: 'services',
|
||||
},
|
||||
},
|
||||
// TODO: SafetyNet Attestation check
|
||||
// safetyNet: {
|
||||
// invoke: {
|
||||
// id: 'safetynet',
|
||||
// src: safetyNetMachine
|
||||
// },
|
||||
// },
|
||||
services: {
|
||||
entry: ['spawnServiceActors', 'logServiceEvents'],
|
||||
on: {
|
||||
READY: 'info',
|
||||
},
|
||||
},
|
||||
info: {
|
||||
invoke: {
|
||||
src: 'getAppInfo',
|
||||
},
|
||||
on: {
|
||||
APP_INFO_RECEIVED: {
|
||||
target: '#ready',
|
||||
actions: ['setAppInfo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
id: 'ready',
|
||||
type: 'parallel',
|
||||
on: {
|
||||
REQUEST_DEVICE_INFO: {
|
||||
actions: ['requestDeviceInfo'],
|
||||
},
|
||||
},
|
||||
states: {
|
||||
focus: {
|
||||
invoke: {
|
||||
src: 'checkFocusState',
|
||||
},
|
||||
on: {
|
||||
ACTIVE: '.active',
|
||||
INACTIVE: '.inactive',
|
||||
},
|
||||
initial: 'checking',
|
||||
states: {
|
||||
checking: {},
|
||||
active: {},
|
||||
inactive: {},
|
||||
},
|
||||
},
|
||||
network: {
|
||||
invoke: {
|
||||
src: 'checkNetworkState',
|
||||
},
|
||||
on: {
|
||||
ONLINE: '.online',
|
||||
OFFLINE: '.offline',
|
||||
},
|
||||
initial: 'checking',
|
||||
states: {
|
||||
checking: {},
|
||||
online: {},
|
||||
offline: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
requestDeviceInfo: respond((context) => ({
|
||||
type: 'RECEIVE_DEVICE_INFO',
|
||||
info: {
|
||||
...context.info,
|
||||
name: context.serviceRefs.settings.getSnapshot().context.name,
|
||||
},
|
||||
})),
|
||||
|
||||
spawnStoreActor: model.assign({
|
||||
serviceRefs: (context) => ({
|
||||
...context.serviceRefs,
|
||||
store: spawn(storeMachine, storeMachine.id),
|
||||
}),
|
||||
}),
|
||||
|
||||
logStoreEvents: (context) => {
|
||||
context.serviceRefs.store.subscribe(logState);
|
||||
},
|
||||
|
||||
spawnServiceActors: model.assign({
|
||||
serviceRefs: (context) => {
|
||||
const serviceRefs = {
|
||||
...context.serviceRefs,
|
||||
};
|
||||
serviceRefs.auth = spawn(
|
||||
createAuthMachine(serviceRefs),
|
||||
authMachine.id
|
||||
);
|
||||
serviceRefs.vid = spawn(createVidMachine(serviceRefs), vidMachine.id);
|
||||
serviceRefs.settings = spawn(
|
||||
createSettingsMachine(serviceRefs),
|
||||
settingsMachine.id
|
||||
);
|
||||
serviceRefs.activityLog = spawn(
|
||||
createActivityLogMachine(serviceRefs),
|
||||
activityLogMachine.id
|
||||
);
|
||||
serviceRefs.scan = spawn(
|
||||
createScanMachine(serviceRefs),
|
||||
scanMachine.id
|
||||
);
|
||||
serviceRefs.request = spawn(
|
||||
createRequestMachine(serviceRefs),
|
||||
requestMachine.id
|
||||
);
|
||||
return serviceRefs;
|
||||
},
|
||||
}),
|
||||
|
||||
logServiceEvents: (context) => {
|
||||
context.serviceRefs.auth.subscribe(logState);
|
||||
context.serviceRefs.vid.subscribe(logState);
|
||||
context.serviceRefs.settings.subscribe(logState);
|
||||
context.serviceRefs.activityLog.subscribe(logState);
|
||||
context.serviceRefs.scan.subscribe(logState);
|
||||
context.serviceRefs.request.subscribe(logState);
|
||||
},
|
||||
|
||||
setAppInfo: model.assign({
|
||||
info: (_, event: AppInfoReceived) => event.info,
|
||||
}),
|
||||
},
|
||||
|
||||
services: {
|
||||
getAppInfo: () => async (callback) => {
|
||||
const appInfo = {
|
||||
deviceId: getDeviceId(),
|
||||
deviceName: await getDeviceName(),
|
||||
};
|
||||
|
||||
callback(model.events.APP_INFO_RECEIVED(appInfo));
|
||||
},
|
||||
|
||||
checkFocusState: () => (callback) => {
|
||||
const handler = (newState: AppStateStatus) => {
|
||||
switch (newState) {
|
||||
case 'background':
|
||||
case 'inactive':
|
||||
callback({ type: 'INACTIVE' });
|
||||
break;
|
||||
case 'active':
|
||||
callback({ type: 'ACTIVE' });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
AppState.addEventListener('change', handler);
|
||||
|
||||
return () => AppState.removeEventListener('change', handler);
|
||||
},
|
||||
|
||||
checkNetworkState: () => (callback) => {
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
if (state.isConnected) {
|
||||
callback({ type: 'ONLINE', networkType: state.type });
|
||||
} else {
|
||||
callback({ type: 'OFFLINE' });
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface AppInfo {
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
}
|
||||
|
||||
type State = StateFrom<typeof appMachine>;
|
||||
|
||||
export function selectAppInfo(state: State) {
|
||||
return state.context.info;
|
||||
}
|
||||
|
||||
export function selectIsReady(state: State) {
|
||||
return state.matches('ready');
|
||||
}
|
||||
|
||||
export function selectIsOnline(state: State) {
|
||||
return state.matches('ready.network.online');
|
||||
}
|
||||
|
||||
export function selectIsActive(state: State) {
|
||||
return state.matches('ready.focus.active');
|
||||
}
|
||||
|
||||
export function logState(state) {
|
||||
const data = JSON.stringify(state.event);
|
||||
console.log(
|
||||
`[${getDeviceNameSync()}] ${state.machine.id}: ${state
|
||||
.toStrings()
|
||||
.join(' ')} ${data.length > 1000 ? data.slice(0, 1000) + '...' : data}`
|
||||
);
|
||||
}
|
||||
154
machines/auth.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { ActorRefFrom, ContextFrom, EventFrom, send, StateFrom } from 'xstate';
|
||||
import { log } from 'xstate/lib/actions';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
import { AppServices } from '../shared/GlobalContext';
|
||||
import { StoreEvents, storeMachine, StoreResponseEvent } from './store';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
serviceRefs: {} as AppServices,
|
||||
passcode: '',
|
||||
biometrics: '',
|
||||
canUseBiometrics: false,
|
||||
},
|
||||
{
|
||||
events: {
|
||||
SETUP_PASSCODE: (passcode: string) => ({ passcode }),
|
||||
SETUP_BIOMETRICS: () => ({}),
|
||||
LOGOUT: () => ({}),
|
||||
LOGIN: () => ({}),
|
||||
STORE_RESPONSE: (response?: unknown) => ({ response }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const AuthEvents = model.events;
|
||||
|
||||
type Context = ContextFrom<typeof model>;
|
||||
|
||||
type SetupPasscodeEvent = EventFrom<typeof model, 'SETUP_PASSCODE'>;
|
||||
|
||||
export const authMachine = model.createMachine(
|
||||
{
|
||||
id: 'auth',
|
||||
context: model.initialContext,
|
||||
initial: 'init',
|
||||
states: {
|
||||
init: {
|
||||
entry: ['requestStoredContext'],
|
||||
on: {
|
||||
STORE_RESPONSE: [
|
||||
{
|
||||
cond: 'hasData',
|
||||
target: 'checkingAuth',
|
||||
actions: ['setContext'],
|
||||
},
|
||||
{ target: 'savingDefaults' },
|
||||
],
|
||||
},
|
||||
},
|
||||
savingDefaults: {
|
||||
entry: ['storeContext'],
|
||||
on: {
|
||||
STORE_RESPONSE: 'checkingAuth',
|
||||
},
|
||||
},
|
||||
checkingAuth: {
|
||||
always: [
|
||||
{ cond: 'hasPasscodeSet', target: 'unauthorized' },
|
||||
{ target: 'settingUp' },
|
||||
],
|
||||
},
|
||||
settingUp: {
|
||||
on: {
|
||||
SETUP_PASSCODE: {
|
||||
target: 'authorized',
|
||||
actions: ['setPasscode', 'storeContext'],
|
||||
},
|
||||
// TODO: biometrics login
|
||||
SETUP_BIOMETRICS: {
|
||||
target: 'authorized',
|
||||
actions: ['setBiometrics', 'storeContext'],
|
||||
},
|
||||
},
|
||||
},
|
||||
unauthorized: {
|
||||
on: {
|
||||
LOGIN: 'authorized',
|
||||
},
|
||||
},
|
||||
authorized: {
|
||||
on: {
|
||||
LOGOUT: 'unauthorized',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
requestStoredContext: send(StoreEvents.GET('auth'), {
|
||||
to: (context) => context.serviceRefs.store,
|
||||
}),
|
||||
|
||||
storeContext: send(
|
||||
(context: Context) => {
|
||||
const { serviceRefs, ...data } = context;
|
||||
return StoreEvents.SET('auth', data);
|
||||
},
|
||||
{ to: (context) => context.serviceRefs.store }
|
||||
),
|
||||
|
||||
setContext: model.assign((_, event: StoreResponseEvent) => {
|
||||
const { serviceRefs, ...data } = event.response;
|
||||
return data;
|
||||
}),
|
||||
|
||||
setPasscode: model.assign({
|
||||
passcode: (_, event: SetupPasscodeEvent) => event.passcode,
|
||||
}),
|
||||
|
||||
setBiometrics: model.assign({
|
||||
biometrics: '', // TODO
|
||||
}),
|
||||
},
|
||||
|
||||
guards: {
|
||||
hasData: (_, event: StoreResponseEvent) => event.response != null,
|
||||
|
||||
hasPasscodeSet: (context) => context.passcode !== '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function createAuthMachine(serviceRefs: AppServices) {
|
||||
return authMachine.withContext({
|
||||
...authMachine.context,
|
||||
serviceRefs,
|
||||
});
|
||||
}
|
||||
|
||||
type State = StateFrom<typeof authMachine>;
|
||||
|
||||
export function selectPasscode(state: State) {
|
||||
return state.context.passcode;
|
||||
}
|
||||
|
||||
export function selectBiometrics(state: State) {
|
||||
return state.context.biometrics;
|
||||
}
|
||||
|
||||
export function selectCanUseBiometrics(state: State) {
|
||||
return state.context.canUseBiometrics;
|
||||
}
|
||||
|
||||
export function selectAuthorized(state: State) {
|
||||
return state.matches('authorized');
|
||||
}
|
||||
|
||||
export function selectUnauthorized(state: State) {
|
||||
return state.matches('unauthorized');
|
||||
}
|
||||
|
||||
export function selectSettingUp(state: State) {
|
||||
return state.matches('settingUp');
|
||||
}
|
||||
31
machines/notifications.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// TODO: notifications logic (including push to notify VID was downloaded)
|
||||
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
|
||||
const model = createModel(
|
||||
{},
|
||||
{
|
||||
events: {
|
||||
MARK_READ: (index: number) => ({ index }),
|
||||
STORE_READY: () => ({}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const notificationsMachine = model.createMachine({
|
||||
id: 'notifications',
|
||||
context: model.initialContext,
|
||||
initial: 'init',
|
||||
states: {
|
||||
init: {
|
||||
on: {},
|
||||
},
|
||||
idle: {},
|
||||
},
|
||||
});
|
||||
|
||||
interface Notification {
|
||||
timestamp: Date;
|
||||
message: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
156
machines/pinInput.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useRef, createRef, MutableRefObject } from 'react';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { EventFrom } from 'xstate';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
TextInput,
|
||||
TextInputKeyPressEventData,
|
||||
} from 'react-native';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
selectedIndex: 0,
|
||||
error: '',
|
||||
inputRefs: [] as PinInputRef[],
|
||||
values: [] as string[],
|
||||
},
|
||||
{
|
||||
events: {
|
||||
FOCUS_INPUT: (index: number) => ({ index }),
|
||||
UPDATE_INPUT: (value: string, index: number) => ({ value, index }),
|
||||
KEY_PRESS: (key: string) => ({ key }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type SelectInputEvent = EventFrom<typeof model, 'FOCUS_INPUT'>;
|
||||
type UpdateInputEvent = EventFrom<typeof model, 'UPDATE_INPUT'>;
|
||||
type KeyPressEvent = EventFrom<typeof model, 'KEY_PRESS'>;
|
||||
|
||||
export const pinInputMachine = model.createMachine(
|
||||
{
|
||||
id: 'pinInput',
|
||||
context: model.initialContext,
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
FOCUS_INPUT: {
|
||||
actions: ['selectInput'],
|
||||
},
|
||||
UPDATE_INPUT: [
|
||||
{
|
||||
cond: 'isBlank',
|
||||
actions: ['updateInput'],
|
||||
},
|
||||
{
|
||||
cond: 'hasNextInput',
|
||||
target: 'selectingNext',
|
||||
actions: ['updateInput'],
|
||||
},
|
||||
{
|
||||
actions: ['updateInput'],
|
||||
},
|
||||
],
|
||||
KEY_PRESS: {
|
||||
cond: 'canGoBack',
|
||||
target: 'selectingPrev',
|
||||
},
|
||||
},
|
||||
after: {
|
||||
// allowance to wait for route transition to end
|
||||
INITIAL_FOCUS_DELAY: {
|
||||
actions: ['focusSelected'],
|
||||
},
|
||||
},
|
||||
},
|
||||
selectingNext: {
|
||||
entry: ['selectNextInput', 'focusSelected'],
|
||||
always: 'idle',
|
||||
},
|
||||
selectingPrev: {
|
||||
entry: ['selectPrevInput', 'clearInput', 'focusSelected'],
|
||||
always: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
selectInput: model.assign({
|
||||
selectedIndex: (_, event: SelectInputEvent) => event.index,
|
||||
}),
|
||||
|
||||
selectNextInput: model.assign({
|
||||
selectedIndex: ({ selectedIndex }) => selectedIndex + 1,
|
||||
}),
|
||||
|
||||
selectPrevInput: model.assign({
|
||||
selectedIndex: ({ selectedIndex }) => selectedIndex - 1,
|
||||
}),
|
||||
|
||||
focusSelected: ({ selectedIndex, inputRefs }) => {
|
||||
inputRefs[selectedIndex].current.focus();
|
||||
},
|
||||
|
||||
clearInput: model.assign({
|
||||
values: ({ values, selectedIndex }) => {
|
||||
const newValues = [...values];
|
||||
newValues[selectedIndex] = '';
|
||||
return newValues;
|
||||
},
|
||||
}),
|
||||
|
||||
updateInput: model.assign({
|
||||
values: ({ values }, event: UpdateInputEvent) => {
|
||||
const newValues = [...values];
|
||||
newValues[event.index] = event.value;
|
||||
return newValues;
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
guards: {
|
||||
hasNextInput: ({ inputRefs, selectedIndex }) => {
|
||||
return selectedIndex + 1 < inputRefs.length;
|
||||
},
|
||||
|
||||
isBlank: (_, event: UpdateInputEvent) => {
|
||||
return !event.value;
|
||||
},
|
||||
|
||||
canGoBack: ({ values, selectedIndex }, event: KeyPressEvent) => {
|
||||
return (
|
||||
selectedIndex - 1 >= 0 &&
|
||||
!values[selectedIndex] &&
|
||||
event.key === 'Backspace'
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
delays: {
|
||||
INITIAL_FOCUS_DELAY: 100,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function usePinInput(length: number) {
|
||||
const machine = useRef(
|
||||
pinInputMachine.withContext({
|
||||
...pinInputMachine.context,
|
||||
inputRefs: Array(length)
|
||||
.fill(null)
|
||||
.map(() => createRef()),
|
||||
})
|
||||
);
|
||||
|
||||
const [state, send] = useMachine(machine.current);
|
||||
|
||||
return {
|
||||
state,
|
||||
send,
|
||||
events: model.events,
|
||||
};
|
||||
}
|
||||
|
||||
export type PinInputRef = MutableRefObject<TextInput>;
|
||||
487
machines/request.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
import SmartShare from 'react-native-idpass-smartshare';
|
||||
import BluetoothStateManager from 'react-native-bluetooth-state-manager';
|
||||
import { EmitterSubscription } from 'react-native';
|
||||
import { EventFrom, send, sendParent, StateFrom } from 'xstate';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
import { DeviceInfo } from '../components/DeviceInfoList';
|
||||
import { Message } from '../shared/Message';
|
||||
import { getDeviceNameSync } from 'react-native-device-info';
|
||||
import { StoreEvents } from './store';
|
||||
import { VID } from '../types/vid';
|
||||
import { AppServices } from '../shared/GlobalContext';
|
||||
import {
|
||||
RECEIVED_VIDS_STORE_KEY,
|
||||
VID_ITEM_STORE_KEY,
|
||||
} from '../shared/storeKeys';
|
||||
import { ActivityLogEvents } from './activityLog';
|
||||
import { VidEvents } from './vid';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
serviceRefs: {} as AppServices,
|
||||
senderInfo: {} as DeviceInfo,
|
||||
receiverInfo: {} as DeviceInfo,
|
||||
incomingVid: {} as VID,
|
||||
connectionParams: '',
|
||||
loggers: [] as EmitterSubscription[],
|
||||
},
|
||||
{
|
||||
events: {
|
||||
ACCEPT: () => ({}),
|
||||
REJECT: () => ({}),
|
||||
CANCEL: () => ({}),
|
||||
DISMISS: () => ({}),
|
||||
VID_RECEIVED: (vid: VID) => ({ vid }),
|
||||
RESPONSE_SENT: () => ({}),
|
||||
CONNECTED: () => ({}),
|
||||
DISCONNECT: () => ({}),
|
||||
EXCHANGE_DONE: (senderInfo: DeviceInfo) => ({ senderInfo }),
|
||||
SCREEN_FOCUS: () => ({}),
|
||||
SCREEN_BLUR: () => ({}),
|
||||
BLUETOOTH_ENABLED: () => ({}),
|
||||
BLUETOOTH_DISABLED: () => ({}),
|
||||
STORE_READY: () => ({}),
|
||||
STORE_RESPONSE: (response: any) => ({ response }),
|
||||
RECEIVE_DEVICE_INFO: (info: DeviceInfo) => ({ info }),
|
||||
RECEIVED_VIDS_UPDATED: () => ({}),
|
||||
VID_RESPONSE: (response: any) => ({ response }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const RequestEvents = model.events;
|
||||
|
||||
type ExchangeDoneEvent = EventFrom<typeof model, 'EXCHANGE_DONE'>;
|
||||
type VidReceivedEvent = EventFrom<typeof model, 'VID_RECEIVED'>;
|
||||
type ReceiveDeviceInfoEvent = EventFrom<typeof model, 'RECEIVE_DEVICE_INFO'>;
|
||||
type StoreResponseEvent = EventFrom<typeof model, 'STORE_RESPONSE'>;
|
||||
type VidResponseEvent = EventFrom<typeof model, 'VID_RESPONSE'>;
|
||||
|
||||
export const requestMachine = model.createMachine(
|
||||
{
|
||||
id: 'request',
|
||||
context: model.initialContext,
|
||||
initial: 'inactive',
|
||||
on: {
|
||||
SCREEN_BLUR: 'inactive',
|
||||
SCREEN_FOCUS: 'checkingBluetoothService',
|
||||
},
|
||||
states: {
|
||||
inactive: {
|
||||
entry: ['removeLoggers'],
|
||||
},
|
||||
checkingBluetoothService: {
|
||||
initial: 'checking',
|
||||
states: {
|
||||
checking: {
|
||||
invoke: {
|
||||
src: 'checkBluetoothService',
|
||||
},
|
||||
on: {
|
||||
BLUETOOTH_ENABLED: 'enabled',
|
||||
BLUETOOTH_DISABLED: 'requesting',
|
||||
},
|
||||
},
|
||||
requesting: {
|
||||
invoke: {
|
||||
src: 'requestBluetooth',
|
||||
},
|
||||
on: {
|
||||
BLUETOOTH_ENABLED: 'enabled',
|
||||
BLUETOOTH_DISABLED: '#bluetoothDenied',
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
always: '#clearingConnection',
|
||||
},
|
||||
},
|
||||
},
|
||||
bluetoothDenied: {
|
||||
id: 'bluetoothDenied',
|
||||
},
|
||||
clearingConnection: {
|
||||
id: 'clearingConnection',
|
||||
entry: ['disconnect'],
|
||||
after: {
|
||||
250: 'waitingForConnection',
|
||||
},
|
||||
},
|
||||
waitingForConnection: {
|
||||
id: 'waitingForConnection',
|
||||
entry: ['removeLoggers', 'registerLoggers', 'generateConnectionParams'],
|
||||
meta: {
|
||||
message: 'Waiting for connection...',
|
||||
},
|
||||
invoke: {
|
||||
src: 'advertiseDevice',
|
||||
},
|
||||
on: {
|
||||
CONNECTED: 'preparingToExchangeInfo',
|
||||
DISCONNECT: 'disconnected',
|
||||
},
|
||||
},
|
||||
preparingToExchangeInfo: {
|
||||
entry: ['requestReceiverInfo'],
|
||||
on: {
|
||||
RECEIVE_DEVICE_INFO: {
|
||||
target: 'exchangingDeviceInfo',
|
||||
actions: ['setReceiverInfo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
exchangingDeviceInfo: {
|
||||
meta: {
|
||||
message: 'Exchanging device info...',
|
||||
},
|
||||
invoke: {
|
||||
src: 'exchangeDeviceInfo',
|
||||
},
|
||||
on: {
|
||||
EXCHANGE_DONE: {
|
||||
target: 'waitingForVid',
|
||||
actions: ['setSenderInfo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
waitingForVid: {
|
||||
meta: {
|
||||
message: 'Connected to device. Waiting for VID...',
|
||||
},
|
||||
invoke: {
|
||||
src: 'receiveVid',
|
||||
},
|
||||
on: {
|
||||
DISCONNECT: 'disconnected',
|
||||
VID_RECEIVED: {
|
||||
target: 'reviewing',
|
||||
actions: ['setIncomingVid'],
|
||||
},
|
||||
},
|
||||
},
|
||||
reviewing: {
|
||||
on: {
|
||||
ACCEPT: '.accepting',
|
||||
REJECT: '.rejected',
|
||||
CANCEL: '.rejected',
|
||||
},
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {},
|
||||
accepting: {
|
||||
initial: 'requestingReceivedVids',
|
||||
states: {
|
||||
requestingReceivedVids: {
|
||||
entry: ['requestReceivedVids'],
|
||||
on: {
|
||||
VID_RESPONSE: [
|
||||
{
|
||||
cond: 'hasExistingVid',
|
||||
target: '#accepted',
|
||||
},
|
||||
{
|
||||
target: 'prependingReceivedVid',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
prependingReceivedVid: {
|
||||
entry: ['prependReceivedVid'],
|
||||
on: {
|
||||
STORE_RESPONSE: 'storingVid',
|
||||
},
|
||||
},
|
||||
storingVid: {
|
||||
entry: ['storeVid'],
|
||||
on: {
|
||||
STORE_RESPONSE: {
|
||||
target: '#accepted',
|
||||
actions: ['sendVidReceived'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accepted: {
|
||||
entry: ['logReceived'],
|
||||
id: 'accepted',
|
||||
invoke: {
|
||||
src: {
|
||||
type: 'sendVidResponse',
|
||||
status: 'accepted',
|
||||
},
|
||||
},
|
||||
on: {
|
||||
DISMISS: 'navigatingToHome',
|
||||
},
|
||||
},
|
||||
rejected: {
|
||||
invoke: {
|
||||
src: {
|
||||
type: 'sendVidResponse',
|
||||
status: 'rejected',
|
||||
},
|
||||
},
|
||||
on: {
|
||||
DISMISS: '#waitingForConnection',
|
||||
},
|
||||
},
|
||||
navigatingToHome: {},
|
||||
},
|
||||
exit: ['disconnect'],
|
||||
},
|
||||
disconnected: {
|
||||
on: {
|
||||
DISMISS: 'waitingForConnection',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
requestReceivedVids: send(VidEvents.GET_RECEIVED_VIDS(), {
|
||||
to: (context) => context.serviceRefs.vid,
|
||||
}),
|
||||
|
||||
requestReceiverInfo: sendParent('REQUEST_DEVICE_INFO'),
|
||||
|
||||
setReceiverInfo: model.assign({
|
||||
receiverInfo: (_, event: ReceiveDeviceInfoEvent) => event.info,
|
||||
}),
|
||||
|
||||
disconnect: () => {
|
||||
try {
|
||||
SmartShare.destroyConnection();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
},
|
||||
|
||||
generateConnectionParams: model.assign({
|
||||
connectionParams: () => SmartShare.getConnectionParameters(),
|
||||
}),
|
||||
|
||||
setSenderInfo: model.assign({
|
||||
senderInfo: (_, event: ExchangeDoneEvent) => event.senderInfo,
|
||||
}),
|
||||
|
||||
setIncomingVid: model.assign({
|
||||
incomingVid: (_, event: VidReceivedEvent) => event.vid,
|
||||
}),
|
||||
|
||||
registerLoggers: model.assign({
|
||||
loggers: () => [
|
||||
SmartShare.handleNearbyEvents((event) => {
|
||||
console.log(
|
||||
getDeviceNameSync(),
|
||||
'<Receiver.Event>',
|
||||
JSON.stringify(event)
|
||||
);
|
||||
}),
|
||||
SmartShare.handleLogEvents((event) => {
|
||||
console.log(
|
||||
getDeviceNameSync(),
|
||||
'<Receiver.Log>',
|
||||
JSON.stringify(event)
|
||||
);
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
removeLoggers: model.assign({
|
||||
loggers: ({ loggers }) => {
|
||||
loggers?.forEach((logger) => logger.remove());
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
|
||||
prependReceivedVid: send(
|
||||
(context) =>
|
||||
StoreEvents.PREPEND(
|
||||
RECEIVED_VIDS_STORE_KEY,
|
||||
VID_ITEM_STORE_KEY(
|
||||
context.incomingVid.uin,
|
||||
context.incomingVid.requestId
|
||||
)
|
||||
),
|
||||
{ to: (context) => context.serviceRefs.store }
|
||||
),
|
||||
|
||||
storeVid: send(
|
||||
(context) =>
|
||||
StoreEvents.SET(
|
||||
VID_ITEM_STORE_KEY(
|
||||
context.incomingVid.uin,
|
||||
context.incomingVid.requestId
|
||||
),
|
||||
context.incomingVid
|
||||
),
|
||||
{ to: (context) => context.serviceRefs.store }
|
||||
),
|
||||
|
||||
logReceived: send(
|
||||
(context) =>
|
||||
ActivityLogEvents.LOG_ACTIVITY({
|
||||
_vidKey: VID_ITEM_STORE_KEY(
|
||||
context.incomingVid.uin,
|
||||
context.incomingVid.requestId
|
||||
),
|
||||
action: 'received',
|
||||
timestamp: Date.now(),
|
||||
deviceName:
|
||||
context.senderInfo.name || context.senderInfo.deviceName,
|
||||
vidLabel: context.incomingVid.tag || context.incomingVid.uin,
|
||||
}),
|
||||
{ to: (context) => context.serviceRefs.activityLog }
|
||||
),
|
||||
|
||||
sendVidReceived: send(
|
||||
(context) => {
|
||||
return VidEvents.VID_RECEIVED(
|
||||
VID_ITEM_STORE_KEY(
|
||||
context.incomingVid.uin,
|
||||
context.incomingVid.requestId
|
||||
)
|
||||
);
|
||||
},
|
||||
{ to: (context) => context.serviceRefs.vid }
|
||||
),
|
||||
},
|
||||
|
||||
services: {
|
||||
checkBluetoothService: () => (callback) => {
|
||||
const subscription = BluetoothStateManager.onStateChange((state) => {
|
||||
if (state === 'PoweredOn') {
|
||||
callback(model.events.BLUETOOTH_ENABLED());
|
||||
} else {
|
||||
callback(model.events.BLUETOOTH_DISABLED());
|
||||
}
|
||||
}, true);
|
||||
|
||||
return () => subscription.remove();
|
||||
},
|
||||
|
||||
requestBluetooth: () => (callback) => {
|
||||
BluetoothStateManager.requestToEnable()
|
||||
.then(() => callback(model.events.BLUETOOTH_ENABLED()))
|
||||
.catch(() => callback(model.events.BLUETOOTH_DISABLED()));
|
||||
},
|
||||
|
||||
advertiseDevice: () => (callback) => {
|
||||
SmartShare.createConnection('advertiser', () => {
|
||||
callback({ type: 'CONNECTED' });
|
||||
});
|
||||
},
|
||||
|
||||
exchangeDeviceInfo: (context) => (callback) => {
|
||||
const subscription = SmartShare.handleNearbyEvents((event) => {
|
||||
if (event.type === 'onDisconnected') {
|
||||
callback({ type: 'DISCONNECT' });
|
||||
}
|
||||
|
||||
if (event.type !== 'msg') return;
|
||||
|
||||
const message = Message.fromString<DeviceInfo>(event.data);
|
||||
if (message.type === 'exchange:sender-info') {
|
||||
const response = new Message(
|
||||
'exchange:receiver-info',
|
||||
context.receiverInfo
|
||||
);
|
||||
SmartShare.send(response.toString(), () => {
|
||||
callback({ type: 'EXCHANGE_DONE', senderInfo: message.data });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
},
|
||||
|
||||
receiveVid: () => (callback) => {
|
||||
const subscription = SmartShare.handleNearbyEvents((event) => {
|
||||
if (event.type === 'onDisconnected') {
|
||||
callback({ type: 'DISCONNECT' });
|
||||
}
|
||||
|
||||
if (event.type !== 'msg') return;
|
||||
|
||||
const message = Message.fromString<VID>(event.data);
|
||||
if (message.type === 'send:vid') {
|
||||
callback({ type: 'VID_RECEIVED', vid: message.data });
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
},
|
||||
|
||||
// tslint:disable-next-line
|
||||
sendVidResponse: (context, event, meta) => (callback) => {
|
||||
const response = new Message('send:vid:response', {
|
||||
status: meta.src.status,
|
||||
});
|
||||
|
||||
SmartShare.send(response.toString(), () => {
|
||||
callback({ type: 'RESPONSE_SENT' });
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
guards: {
|
||||
hasExistingVid: (context, event: VidResponseEvent) => {
|
||||
const receivedVids: string[] = event.response;
|
||||
const vidKey = VID_ITEM_STORE_KEY(
|
||||
context.incomingVid.uin,
|
||||
context.incomingVid.requestId
|
||||
);
|
||||
return receivedVids.includes(vidKey);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function createRequestMachine(serviceRefs: AppServices) {
|
||||
return requestMachine.withContext({
|
||||
...requestMachine.context,
|
||||
serviceRefs,
|
||||
});
|
||||
}
|
||||
|
||||
type State = StateFrom<typeof requestMachine>;
|
||||
|
||||
export function selectSenderInfo(state: State) {
|
||||
return state.context.senderInfo;
|
||||
}
|
||||
|
||||
export function selectConnectionParams(state: State) {
|
||||
return state.context.connectionParams;
|
||||
}
|
||||
|
||||
export function selectStatusMessage(state: State) {
|
||||
return state.meta[`${state.machine.id}.${state.value}`]?.message || '';
|
||||
}
|
||||
|
||||
export function selectIncomingVid(state: State) {
|
||||
return state.context.incomingVid;
|
||||
}
|
||||
|
||||
export function selectReviewing(state: State) {
|
||||
return state.matches('reviewing');
|
||||
}
|
||||
|
||||
export function selectAccepted(state: State) {
|
||||
return state.matches('reviewing.accepted');
|
||||
}
|
||||
|
||||
export function selectRejected(state: State) {
|
||||
return state.matches('reviewing.rejected');
|
||||
}
|
||||
|
||||
export function selectDisconnected(state: State) {
|
||||
return state.matches('disconnected');
|
||||
}
|
||||
|
||||
export function selectWaitingForConnection(state: State) {
|
||||
return state.matches('waitingForConnection');
|
||||
}
|
||||
|
||||
export function selectBluetoothDenied(state: State) {
|
||||
return state.matches('bluetoothDenied');
|
||||
}
|
||||
122
machines/safetynet.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// TODO: wip; replace library since this breaks the build
|
||||
// import RNSafetyNetClient from '@bitwala/react-native-safetynet';
|
||||
import { getDeviceId } from 'react-native-device-info';
|
||||
import { ContextFrom } from 'xstate';
|
||||
import { createModel } from 'xstate/lib/model';
|
||||
|
||||
// TODO: move to env
|
||||
const ATTESTATION_API_KEY = 'YOUR API KEY';
|
||||
const ATTESTATION_ENDPOINT = '';
|
||||
const NONCE_ENDPOINT = '';
|
||||
|
||||
const model = createModel(
|
||||
{
|
||||
nonce: '',
|
||||
jws: '',
|
||||
error: '',
|
||||
},
|
||||
{
|
||||
events: {
|
||||
NONCE_RECEIVED: (nonce: string) => ({ nonce }),
|
||||
ATTESTATION_RECEIVED: (jws: string) => ({ jws }),
|
||||
ERROR: (error: string) => ({ error }),
|
||||
VERIFIED: () => ({}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type Context = ContextFrom<typeof model>;
|
||||
|
||||
export const safetynetMachine = model.createMachine(
|
||||
{
|
||||
id: 'safetynet',
|
||||
context: model.initialContext,
|
||||
initial: 'requestingAttestation', // 'requestingNonce',
|
||||
states: {
|
||||
requestingNonce: {
|
||||
invoke: {
|
||||
src: 'requestNonce',
|
||||
},
|
||||
},
|
||||
requestingAttestation: {
|
||||
invoke: {
|
||||
src: 'requestAttestation',
|
||||
onDone: '',
|
||||
},
|
||||
},
|
||||
verifyingAttestation: {
|
||||
invoke: {
|
||||
src: 'verifyAttestation',
|
||||
},
|
||||
},
|
||||
verified: {
|
||||
type: 'final',
|
||||
data: {
|
||||
jws: (context: Context) => context.jws,
|
||||
},
|
||||
},
|
||||
failed: {
|
||||
type: 'final',
|
||||
data: {
|
||||
error: (context: Context) => context.error,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {},
|
||||
|
||||
services: {
|
||||
requestNonce: () => async (callback) => {
|
||||
const nonceResult = await RNSafetyNetClient.requestNonce({
|
||||
endPointUrl: NONCE_ENDPOINT,
|
||||
additionalData: getDeviceId(),
|
||||
});
|
||||
|
||||
if (!nonceResult.nonce || nonceResult.error) {
|
||||
callback(
|
||||
model.events.ERROR(nonceResult.error || 'Nonce request failed.')
|
||||
);
|
||||
} else {
|
||||
callback(model.events.NONCE_RECEIVED(nonceResult.nonce));
|
||||
}
|
||||
},
|
||||
|
||||
requestAttestation: (context) => async (callback) => {
|
||||
const attestationResult =
|
||||
await RNSafetyNetClient.sendAttestationRequest(
|
||||
context.nonce,
|
||||
ATTESTATION_API_KEY
|
||||
);
|
||||
|
||||
if (!attestationResult.jws || attestationResult.error) {
|
||||
callback(
|
||||
model.events.ERROR(
|
||||
attestationResult.error || 'Attestation request failed.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
callback(model.events.ATTESTATION_RECEIVED(attestationResult.jws));
|
||||
}
|
||||
},
|
||||
|
||||
verifyAttestation: (context) => async (callback) => {
|
||||
const verification = (await RNSafetyNetClient.verifyAttestationResult({
|
||||
endPointUrl: ATTESTATION_ENDPOINT,
|
||||
attestationJws: context.jws,
|
||||
})) as VerificationResult;
|
||||
|
||||
// TODO: handle depending on response data from our server
|
||||
if (!verification.success) {
|
||||
callback(model.events.ERROR('Verfication failed.'));
|
||||
} else {
|
||||
callback(model.events.VERIFIED());
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface VerificationResult {
|
||||
success: boolean;
|
||||
}
|
||||