version 0.2.0

This commit is contained in:
Paolo Miguel de Leon
2022-02-15 11:17:17 +08:00
committed by Ken Lewerentz
parent 5115af1ae7
commit b5be632225
166 changed files with 58836 additions and 70 deletions

4
.expo-shared/assets.json Normal file
View File

@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

29
.github/workflows/android.yml vendored Normal file
View 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
View File

@@ -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
View 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
View 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>
);
}

View File

@@ -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
View 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>

View 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
View 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
View 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"

View 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

Binary file not shown.

View 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
View 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:

View 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>

View File

@@ -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());
}
}
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View 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);
}
}
);
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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();
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View 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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources/>

View 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>

View 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>

View 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
View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/idpass-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/mosip-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

6
babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

View 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;
}

View 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;
}

View 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>
);
};

View 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
View 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;
}

View 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;
}

View 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
View 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
View 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;
}

View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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>;
}

View 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
View File

@@ -0,0 +1,3 @@
export { Text } from './Text';
export { Button } from './Button';
export { Row, Column, Centered } from './Layout';

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

Some files were not shown because too many files have changed in this diff Show More