Merge pull request #25 from zcash/15-onboarding
This commit is contained in:
commit
361c6a2eee
|
@ -2,7 +2,6 @@ syntax: glob
|
|||
.DS_Store
|
||||
.gradle/
|
||||
.idea/caches
|
||||
.idea/codeStyles
|
||||
.idea/compiler.xml
|
||||
.idea/dictionaries/
|
||||
.idea/gradle.xml
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
|
@ -0,0 +1,53 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ui-lib:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||
<module name="zcash-android-app.ui-lib" />
|
||||
<option name="TESTING_TYPE" value="0" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="CLASS_NAME" value="" />
|
||||
<option name="PACKAGE_NAME" value="" />
|
||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||
<option name="EXTRA_OPTIONS" value="" />
|
||||
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Hybrid>
|
||||
<Java />
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||
</Profilers>
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
id("zcash.android-build-conventions")
|
||||
}
|
||||
|
||||
val packageName = "cash.z.ecc.android"
|
||||
val packageName = "cash.z.ecc"
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
|
@ -15,15 +15,6 @@ android {
|
|||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.get()
|
||||
}
|
||||
|
||||
flavorDimensions.add("network")
|
||||
|
||||
productFlavors {
|
||||
|
@ -99,6 +90,7 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.zcash)
|
||||
implementation(projects.uiLib)
|
||||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cash.z.ecc.android.sdk.demoapp">
|
||||
package="cash.z.ecc">
|
||||
|
||||
<application
|
||||
android:name="cash.z.ecc.android.app.App"
|
||||
android:name="cash.z.ecc.app.AppImpl"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
@ -18,20 +18,13 @@
|
|||
<activity-alias
|
||||
android:name=".LauncherActivity"
|
||||
android:label="@string/"
|
||||
android:targetActivity="cash.z.ecc.android.app.MainActivity"
|
||||
android:targetActivity="cash.z.ecc.ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name="cash.z.ecc.android.app.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.MyApplication"
|
||||
android:exported="false">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package cash.z.ecc.android.app
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.ecc.android.sdk.demoapp.BuildConfig
|
||||
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.ext.Twig
|
||||
|
||||
class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictModeHelper.enableStrictMode()
|
||||
}
|
||||
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package cash.z.ecc.android.app
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.app.ui.theme.MyApplicationTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
MyApplicationTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String) {
|
||||
Text(text = "Hello $name!")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
MyApplicationTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package cash.z.ecc.app
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.ecc.BuildConfig
|
||||
|
||||
@Suppress("unused")
|
||||
class AppImpl : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictModeHelper.enableStrictMode()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.app
|
||||
package cash.z.ecc.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
|
@ -75,6 +75,7 @@ fun com.android.build.gradle.BaseExtension.configureBaseExtension() {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO [#22]: This doesn't work, so there's a duplicate in build.gradle.kts
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
|
||||
|
@ -82,4 +83,23 @@ fun com.android.build.gradle.BaseExtension.configureBaseExtension() {
|
|||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"META-INF/AL2.0",
|
||||
"META-INF/ASL2.0",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LGPL2.1",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE-notice.md",
|
||||
"META-INF/LICENSE.md",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/NOTICE.txt",
|
||||
"META-INF/license.txt",
|
||||
"META-INF/notice.txt",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,15 @@ TODO This is a placeholder for describing the app architecture.
|
|||
* Versions are declared in [gradle.properties](../gradle.properties). There's still enough inconsistency in how versions are handled in Gradle, that this is as close as we can get to a universal system. A version catalog is used for dependencies and is configured in [settings.gradle.kts](../settings.gradle.kts), but other versions like Gradle Plug-ins, the NDK version, Java version, and Android SDK versions don't fit into the version catalog model and are read directly from the properties
|
||||
* Much of the Gradle configuration lives in [build-conventions](../build-conventions/) to prevent repetitive configuration as additional modules are added to the project
|
||||
* Build scripts are written in Kotlin, so that a single language is used across build and the app code bases
|
||||
* Only Gradle, Google, and JetBrains plug-ins are included in the critical path. Third party plug-ins can be used, but they're outside the critical path. For example, the Gradle Versions Plugin could be removed and wouldn't negative impact building, testing, or deploying the app
|
||||
* Only Gradle, Google, and JetBrains plug-ins are included in the critical path. Third party plug-ins can be used, but they're outside the critical path. For example, the Gradle Versions Plugin could be removed and wouldn't negative impact building, testing, or deploying the app
|
||||
|
||||
## App
|
||||
The main entrypoints of the application are:
|
||||
* [AppImpl.kt](../app/src/main/java/cash/z/ecc/app/AppImpl.kt) - The root Application object defined in the app module
|
||||
* [MainActivity.kt](../ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt) - The main Activity, defined in ui-lib. Note that the Activity is NOT exported. Instead, the app module defines an activity-alias in the AndroidManifest which is what presents the actual icon on the Android home screen.
|
||||
|
||||
## Modules
|
||||
The logical components of the app are implemented as a number of Gradle modules.
|
||||
|
||||
* app — Compiles all of the modules together into the final application. This module contains minimal actual code. Note that the Java package structure for this module is under `cash.z.ecc.app` while the Android package name is `cash.z.ecc`.
|
||||
* ui-lib — User interface that the user interacts with. This contains 99% of the UI code, along with localizations, icons, and other assets.
|
|
@ -14,7 +14,7 @@ Start by making sure the command line with Gradle works first, because **all the
|
|||
1. Java 16 is currently recommended. Java 11 is the minimum requirement for Android Studio.
|
||||
1. To simplify installation, use [Oracle's JDK](https://www.oracle.com/java/technologies/javase-jdk16-downloads.html) installer that will place the Java installation in the right place
|
||||
1. Install Android Studio and the Android SDK
|
||||
1. Download the [stable release of Android Studio](https://developer.android.com/studio#downloads)
|
||||
1. Download the [Android Studio Bumblebee Canary](https://developer.android.com/studio/preview) (we're using the Canary version, due to its improved integration with Jetpack Compose)
|
||||
1. TODO: Fill in step-by-step instructions for setting up a new environment and installing the Android SDK from within Android Studio
|
||||
1. Check out the code. _Use the command line (instead of Android Studio) to check out the code. This will ensure that your command line environment is set up correctly and avoids a few pitfalls with trying to use Android Studio directly. Android Studio's built-in git client is not as robust as standalone clients_
|
||||
1. Compile from the command line
|
||||
|
|
|
@ -54,6 +54,7 @@ ANDROIDX_APPCOMPAT_VERSION=1.3.1
|
|||
ANDROIDX_COMPOSE_VERSION=1.0.2
|
||||
ANDROIDX_CORE_VERSION=1.6.0
|
||||
ANDROIDX_ESPRESSO_VERSION=3.4.0
|
||||
ANDROIDX_LIFECYCLE_VERSION=2.3.1
|
||||
ANDROIDX_NAVIGATION_VERSION=2.3.5
|
||||
ANDROIDX_TEST_JUNIT_VERSION=1.1.3
|
||||
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.1.0-alpha1
|
||||
|
|
|
@ -35,6 +35,7 @@ dependencyResolutionManagement {
|
|||
val androidxComposeVersion = extra["ANDROIDX_COMPOSE_VERSION"].toString()
|
||||
val androidxCoreVersion = extra["ANDROIDX_CORE_VERSION"].toString()
|
||||
val androidxEspressoVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
|
||||
val androidxLifecycleVersion = extra["ANDROIDX_LIFECYCLE_VERSION"].toString()
|
||||
val androidxTestJunitVersion = extra["ANDROIDX_TEST_JUNIT_VERSION"].toString()
|
||||
val androidxTestOrchestratorVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
|
||||
val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_VERSION"].toString()
|
||||
|
@ -53,23 +54,26 @@ dependencyResolutionManagement {
|
|||
// Aliases
|
||||
alias("androidx-activity").to("androidx.activity:activity-ktx:$androidxActivityVersion")
|
||||
alias("androidx-activity-compose").to("androidx.activity:activity-compose:$androidxActivityVersion")
|
||||
alias("androidx-appcompat").to("androidx.appcompat:appcompat:$androidxAppcompatVersion")
|
||||
alias("androidx-annotation").to("androidx.annotation:annotation:$androidxAnnotationVersion")
|
||||
alias("androidx-appcompat").to("androidx.appcompat:appcompat:$androidxAppcompatVersion")
|
||||
alias("androidx-compose-foundation").to("androidx.compose.foundation:foundation:$androidxComposeVersion")
|
||||
alias("androidx-compose-material").to("androidx.compose.material:material:$androidxComposeVersion")
|
||||
alias("androidx-compose-material-icons-core").to("androidx.compose.material:material-icons-core:$androidxComposeVersion")
|
||||
alias("androidx-compose-tooling").to("androidx.compose.ui:ui-tooling-preview:$androidxComposeVersion")
|
||||
alias("androidx-compose-tooling").to("androidx.compose.ui:ui-tooling:$androidxComposeVersion")
|
||||
alias("androidx-compose-ui").to("androidx.compose.ui:ui:$androidxComposeVersion")
|
||||
alias("androidx-core").to("androidx.core:core-ktx:$androidxCoreVersion")
|
||||
alias("androidx-lifecycle-livedata").to("androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion")
|
||||
alias("androidx-viewmodel-compose").to("androidx.activity:activity-compose:$androidxActivityVersion")
|
||||
alias("google-material").to("com.google.android.material:material:$googleMaterialVersion")
|
||||
alias("kotlin").to("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||
alias("kotlinx-coroutines-android").to("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion")
|
||||
alias("kotlinx-coroutines-core").to("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
|
||||
alias("zcash").to("cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
|
||||
// Test libraries
|
||||
alias("androidx-espresso-contrib").to("androidx.test.espresso:espresso-contrib:$androidxEspressoVersion")
|
||||
alias("androidx-compose-test-junit").to("androidx.compose.ui:ui-test-junit4:$androidxComposeVersion")
|
||||
alias("androidx-compose-test-manifest").to("androidx.compose.ui:ui-test-manifest:$androidxComposeVersion")
|
||||
// Cannot use espresso-contrib, because it causes a build failure
|
||||
//alias("androidx-espresso-contrib").to("androidx.test.espresso:espresso-contrib:$androidxEspressoVersion")
|
||||
alias("androidx-espresso-core").to("androidx.test.espresso:espresso-core:$androidxEspressoVersion")
|
||||
alias("androidx-espresso-intents").to("androidx.test.espresso:espresso-intents:$androidxEspressoVersion")
|
||||
alias("androidx-junit").to("androidx.test.ext:junit:$androidxTestJunitVersion")
|
||||
|
@ -95,7 +99,6 @@ dependencyResolutionManagement {
|
|||
listOf(
|
||||
"androidx-espresso-core",
|
||||
"androidx-espresso-intents",
|
||||
"androidx-espresso-contrib",
|
||||
"androidx-junit"
|
||||
)
|
||||
)
|
||||
|
@ -108,3 +111,4 @@ rootProject.name = "zcash-android-app"
|
|||
includeBuild("build-conventions")
|
||||
|
||||
include("app")
|
||||
include("ui-lib")
|
|
@ -0,0 +1,48 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
id("androidx.navigation.safeargs")
|
||||
id("zcash.android-build-conventions")
|
||||
}
|
||||
|
||||
val packageName = "cash.z.ecc.ui"
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.get()
|
||||
}
|
||||
|
||||
// TODO [#5]: Figure out how to move this into the build-conventions
|
||||
testCoverage {
|
||||
jacocoVersion = libs.versions.jacoco.get()
|
||||
}
|
||||
|
||||
// TODO [#6]: Figure out how to move this into the build-conventions
|
||||
kotlinOptions {
|
||||
jvmTarget = libs.versions.java.get()
|
||||
allWarningsAsErrors = project.property("IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.lifecycle.livedata)
|
||||
implementation(libs.bundles.androidx.compose)
|
||||
implementation(libs.google.material)
|
||||
implementation(libs.kotlin)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.zcash)
|
||||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.androidx.compose.test.junit)
|
||||
androidTestImplementation(libs.androidx.compose.test.manifest)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Test
|
||||
|
||||
class IndexTest {
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@SmallTest
|
||||
fun out_of_bounds() {
|
||||
Index(-1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
class OnboardingStageTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getProgress_first() {
|
||||
val progress = OnboardingStage.values().first().getProgress()
|
||||
|
||||
assertEquals(0, progress.current.value)
|
||||
assertEquals(3, progress.last.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getProgress_last() {
|
||||
val progress = OnboardingStage.values().last().getProgress()
|
||||
|
||||
assertEquals(3, progress.current.value)
|
||||
assertEquals(3, progress.last.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun hasNext_boundary() {
|
||||
val last = OnboardingStage.values().last()
|
||||
|
||||
assertFalse(last.hasNext())
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun hasPrevious_boundary() {
|
||||
val last = OnboardingStage.values().first()
|
||||
|
||||
assertFalse(last.hasPrevious())
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getNext_from_first() {
|
||||
val first = OnboardingStage.values().first()
|
||||
val next = first.getNext()
|
||||
|
||||
assertNotEquals(first, next)
|
||||
assertEquals(OnboardingStage.UnifiedAddresses, next)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getNext_boundary() {
|
||||
val last = OnboardingStage.values().last()
|
||||
|
||||
assertEquals(last, last.getNext())
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getPrevious_from_last() {
|
||||
val last = OnboardingStage.values().last()
|
||||
val previous = last.getPrevious()
|
||||
|
||||
assertNotEquals(last, previous)
|
||||
assertEquals(OnboardingStage.More, previous)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun getPrevious_boundary() {
|
||||
val first = OnboardingStage.values().first()
|
||||
|
||||
assertEquals(first, first.getPrevious())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Test
|
||||
|
||||
class PercentDecimalTest {
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@SmallTest
|
||||
fun require_greater_than_zero() {
|
||||
PercentDecimal(-1.0f)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@SmallTest
|
||||
fun require_less_than_one() {
|
||||
PercentDecimal(1.5f)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Test
|
||||
|
||||
class ProgressTest {
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@SmallTest
|
||||
fun last_greater_than_zero() {
|
||||
Progress(current = Index(0), last = Index(0))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@SmallTest
|
||||
fun last_greater_or_equal_to_current() {
|
||||
Progress(current = Index(5), last = Index(4))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.test
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
|
|
@ -0,0 +1,276 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.view
|
||||
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.R
|
||||
import cash.z.ecc.ui.screen.onboarding.model.OnboardingStage
|
||||
import cash.z.ecc.ui.screen.onboarding.state.OnboardingState
|
||||
import cash.z.ecc.ui.screen.onboarding.test.getStringResource
|
||||
import cash.z.ecc.ui.theme.MyApplicationTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class OnboardingViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
// Sanity check the TestSetup
|
||||
@Test
|
||||
@MediumTest
|
||||
fun verify_test_setup_stage_1() {
|
||||
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
|
||||
|
||||
assertEquals(OnboardingStage.ShieldedByDefault, testSetup.getOnboardingStage())
|
||||
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
|
||||
assertEquals(0, testSetup.getOnCreateWalletCallbackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun verify_test_setup_stage_4() {
|
||||
val testSetup = newTestSetup(OnboardingStage.Wallet)
|
||||
|
||||
assertEquals(OnboardingStage.Wallet, testSetup.getOnboardingStage())
|
||||
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
|
||||
assertEquals(0, testSetup.getOnCreateWalletCallbackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun stage_1_layout() {
|
||||
newTestSetup(OnboardingStage.ShieldedByDefault)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_back)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_body)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun stage_2_layout() {
|
||||
newTestSetup(OnboardingStage.UnifiedAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_back)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_2_header)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_2_body)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun stage_3_layout() {
|
||||
newTestSetup(OnboardingStage.More)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_back)).also {
|
||||
it.assertExists()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_3_header)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_3_body)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun stage_4_layout() {
|
||||
newTestSetup(OnboardingStage.Wallet)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_back)).also {
|
||||
it.assertExists()
|
||||
it.assertIsEnabled()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
|
||||
it.assertExists()
|
||||
it.assertIsEnabled()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
|
||||
it.assertExists()
|
||||
it.assertIsEnabled()
|
||||
it.assertHasClickAction()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun stage_1_skip() {
|
||||
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
|
||||
|
||||
val skipButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip))
|
||||
skipButton.performClick()
|
||||
|
||||
assertEquals(OnboardingStage.Wallet, testSetup.getOnboardingStage())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun last_stage_click_create_wallet() {
|
||||
val testSetup = newTestSetup(OnboardingStage.Wallet)
|
||||
|
||||
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet))
|
||||
newWalletButton.performClick()
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateWalletCallbackCount())
|
||||
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun last_stage_click_import_wallet() {
|
||||
val testSetup = newTestSetup(OnboardingStage.Wallet)
|
||||
|
||||
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet))
|
||||
newWalletButton.performClick()
|
||||
|
||||
assertEquals(1, testSetup.getOnImportWalletCallbackCount())
|
||||
assertEquals(0, testSetup.getOnCreateWalletCallbackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun multi_stage_progression() {
|
||||
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(OnboardingStage.Wallet, testSetup.getOnboardingStage())
|
||||
}
|
||||
|
||||
private fun newTestSetup(initalStage: OnboardingStage) = TestSetup(composeTestRule, initalStage)
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule, initalStage: OnboardingStage) {
|
||||
private val onboardingState = OnboardingState(initalStage)
|
||||
|
||||
private var onCreateWalletCallbackCount = 0
|
||||
private var onImportWalletCallbackCount = 0
|
||||
|
||||
fun getOnCreateWalletCallbackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCreateWalletCallbackCount
|
||||
}
|
||||
|
||||
fun getOnImportWalletCallbackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onImportWalletCallbackCount
|
||||
}
|
||||
|
||||
fun getOnboardingStage(): OnboardingStage {
|
||||
composeTestRule.waitForIdle()
|
||||
return onboardingState.current.value
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
MyApplicationTheme {
|
||||
Onboarding(
|
||||
onboardingState,
|
||||
onCreateWallet = { onCreateWalletCallbackCount++ },
|
||||
onImportWallet = { onImportWalletCallbackCount++ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cash.z.ecc.ui">
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name="cash.z.ecc.ui.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.MyApplication"
|
||||
android:exported="false">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,27 @@
|
|||
package cash.z.ecc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
|
||||
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import cash.z.ecc.ui.theme.MyApplicationTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val onboardingViewModel by viewModels<OnboardingViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
MyApplicationTheme {
|
||||
Onboarding(
|
||||
onboardingState = onboardingViewModel.onboardingState,
|
||||
onImportWallet = { TODO("Implement wallet import") },
|
||||
onCreateWallet = { TODO("Implement wallet create") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
/**
|
||||
* Useful for accessing arrays or lists by index.
|
||||
*
|
||||
* @param value A 0-based index. Must be >= 0
|
||||
*/
|
||||
@JvmInline
|
||||
value class Index(val value: Int) {
|
||||
init {
|
||||
require(value >= 0) { "Index must be >= 0 but actually is $value" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
enum class OnboardingStage {
|
||||
// Note: the ordinal order is used to manage progression through each stage
|
||||
// so be careful if reordering these
|
||||
ShieldedByDefault,
|
||||
UnifiedAddresses,
|
||||
More,
|
||||
Wallet;
|
||||
|
||||
/**
|
||||
* @see getPrevious
|
||||
*/
|
||||
fun hasPrevious() = ordinal > 0
|
||||
|
||||
/**
|
||||
* @see getNext
|
||||
*/
|
||||
fun hasNext() = ordinal < values().size - 1
|
||||
|
||||
/**
|
||||
* @return Previous item in ordinal order. Returns the first item when it cannot go further back.
|
||||
*/
|
||||
fun getPrevious() = values()[maxOf(0, ordinal - 1)]
|
||||
|
||||
/**
|
||||
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
|
||||
*/
|
||||
fun getNext() = values()[minOf(values().size - 1, ordinal + 1)]
|
||||
|
||||
/**
|
||||
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
|
||||
*/
|
||||
fun getProgress() = Progress(Index(ordinal), Index(values().size - 1))
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
/**
|
||||
* @param decimal A percent represented as a `Double` decimal value in the range of [0, 1].
|
||||
*/
|
||||
@JvmInline
|
||||
value class PercentDecimal(val decimal: Float) {
|
||||
init {
|
||||
require(EXPECTED_RANGE.contains(decimal)) { "$decimal is outside of range $EXPECTED_RANGE" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXPECTED_RANGE = 0.0f..1.0f
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.model
|
||||
|
||||
data class Progress(val current: Index, val last: Index) {
|
||||
init {
|
||||
require(last.value > 0) { "last must be > 0 but was $last" }
|
||||
require(last.value >= current.value) { "last ($last) must be >= current ($current)" }
|
||||
}
|
||||
|
||||
fun percent() = PercentDecimal((current.value + 1).toFloat() / (last.value + 1).toFloat())
|
||||
|
||||
companion object {
|
||||
val EMPTY = Progress(Index(0), Index(1))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.state
|
||||
|
||||
import cash.z.ecc.ui.screen.onboarding.model.OnboardingStage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* @param initialState Allows restoring the state from a different starting point. This is
|
||||
* primarily useful on Android, for automated tests, and for iterative debugging with the Compose
|
||||
* layout preview. The default constructor argument is generally fine for other platforms.
|
||||
*/
|
||||
class OnboardingState(initialState: OnboardingStage = OnboardingStage.values().first()) {
|
||||
|
||||
private val mutableState = MutableStateFlow(initialState)
|
||||
|
||||
val current: StateFlow<OnboardingStage> = mutableState
|
||||
|
||||
fun hasNext() = current.value.hasNext()
|
||||
|
||||
fun hasPrevious() = current.value.hasPrevious()
|
||||
|
||||
fun goNext() {
|
||||
mutableState.value = current.value.getNext()
|
||||
}
|
||||
|
||||
fun goPrevious() {
|
||||
mutableState.value = current.value.getPrevious()
|
||||
}
|
||||
|
||||
fun goToEnd() {
|
||||
mutableState.value = OnboardingStage.values().last()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.R
|
||||
import cash.z.ecc.ui.screen.onboarding.model.OnboardingStage
|
||||
import cash.z.ecc.ui.screen.onboarding.model.Progress
|
||||
import cash.z.ecc.ui.screen.onboarding.state.OnboardingState
|
||||
import cash.z.ecc.ui.theme.MINIMAL_WEIGHT
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ComposablePreview() {
|
||||
Onboarding(
|
||||
OnboardingState(OnboardingStage.UnifiedAddresses),
|
||||
onImportWallet = {},
|
||||
onCreateWallet = {}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onImportWallet Callback when the user decides to import an existing wallet.
|
||||
* @param onCreateWallet Callback when the user decides to create a new wallet.
|
||||
*/
|
||||
@Composable
|
||||
fun Onboarding(
|
||||
onboardingState: OnboardingState,
|
||||
onImportWallet: () -> Unit,
|
||||
onCreateWallet: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
TopNavButtons(onboardingState)
|
||||
|
||||
val onboardingStage = onboardingState.current.collectAsState().value
|
||||
|
||||
when (onboardingStage) {
|
||||
OnboardingStage.ShieldedByDefault -> ShieldedByDefault()
|
||||
OnboardingStage.UnifiedAddresses -> UnifiedAddresses()
|
||||
OnboardingStage.More -> More()
|
||||
OnboardingStage.Wallet -> Wallet(
|
||||
onCreateWallet = onCreateWallet,
|
||||
onImportWallet = onImportWallet
|
||||
)
|
||||
}
|
||||
|
||||
BottomNav(onboardingStage.getProgress(), onboardingState::goNext)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopNavButtons(onboardingState: OnboardingState) {
|
||||
Row {
|
||||
if (onboardingState.hasPrevious()) {
|
||||
Button(onboardingState::goPrevious) {
|
||||
Text(stringResource(R.string.onboarding_back))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(MINIMAL_WEIGHT, true))
|
||||
|
||||
if (onboardingState.hasNext()) {
|
||||
Button(onboardingState::goToEnd) {
|
||||
Text(stringResource(R.string.onboarding_skip))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomNav(progress: Progress, onNext: () -> Unit) {
|
||||
if (progress.current != progress.last) {
|
||||
Column {
|
||||
Button(onNext, Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.onboarding_next))
|
||||
}
|
||||
|
||||
// Converts from index to human numbering
|
||||
Text((progress.current.value + 1).toString())
|
||||
|
||||
LinearProgressIndicator(progress = progress.percent().decimal, Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShieldedByDefault() {
|
||||
Column {
|
||||
Content(
|
||||
image = ColorPainter(Color.Blue),
|
||||
imageContentDescription = stringResource(R.string.onboarding_1_image_content_description),
|
||||
headline = stringResource(R.string.onboarding_1_header),
|
||||
body = stringResource(R.string.onboarding_1_body)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnifiedAddresses() {
|
||||
Column {
|
||||
Content(
|
||||
image = ColorPainter(Color.Blue),
|
||||
imageContentDescription = stringResource(R.string.onboarding_2_image_content_description),
|
||||
headline = stringResource(R.string.onboarding_2_header),
|
||||
body = stringResource(R.string.onboarding_2_body)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun More() {
|
||||
Column {
|
||||
Content(
|
||||
image = ColorPainter(Color.Blue),
|
||||
imageContentDescription = stringResource(R.string.onboarding_3_image_content_description),
|
||||
headline = stringResource(R.string.onboarding_3_header),
|
||||
body = stringResource(R.string.onboarding_3_body)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Wallet(onCreateWallet: () -> Unit, onImportWallet: () -> Unit) {
|
||||
Column {
|
||||
Button(onCreateWallet, Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.onboarding_4_create_new_wallet))
|
||||
}
|
||||
Button(onImportWallet, Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.onboarding_4_import_existing_wallet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
image: Painter,
|
||||
imageContentDescription: String?,
|
||||
headline: String,
|
||||
body: String
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
// TODO [#17]: This suppression and magic number will get replaced once we have real assets
|
||||
@Suppress("MagicNumber")
|
||||
Image(image, imageContentDescription, Modifier.fillMaxSize(0.50f))
|
||||
}
|
||||
Text(headline)
|
||||
Text(body)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package cash.z.ecc.ui.screen.onboarding.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.ui.screen.onboarding.model.OnboardingStage
|
||||
import cash.z.ecc.ui.screen.onboarding.state.OnboardingState
|
||||
|
||||
/*
|
||||
* Android-specific ViewModel. This is used to save and restore state across Activity recreations
|
||||
* outside of the Compose framework.
|
||||
*/
|
||||
class OnboardingViewModel(
|
||||
application: Application,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
val onboardingState: OnboardingState = run {
|
||||
val initialValue = if (savedStateHandle.contains(KEY_STAGE)) {
|
||||
savedStateHandle.get<OnboardingStage>(KEY_STAGE)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (null == initialValue) {
|
||||
OnboardingState()
|
||||
} else {
|
||||
OnboardingState(initialValue)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
|
||||
// update the save state as soon as a change occurs.
|
||||
onboardingState.current.collectWith(viewModelScope) {
|
||||
savedStateHandle.set(KEY_STAGE, it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_STAGE = "stage" // $NON-NLS
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.app.ui.theme
|
||||
package cash.z.ecc.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package cash.z.ecc.ui.theme
|
||||
|
||||
/**
|
||||
* A tiny weight, useful for spacers to fill an empty space.
|
||||
*/
|
||||
const val MINIMAL_WEIGHT = 0.0001f
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.app.ui.theme
|
||||
package cash.z.ecc.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.app.ui.theme
|
||||
package cash.z.ecc.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -30,7 +30,7 @@ private val LightColorPalette = lightColors(
|
|||
@Composable
|
||||
fun MyApplicationTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable() () -> Unit
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.app.ui.theme
|
||||
package cash.z.ecc.ui.theme
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,21 @@
|
|||
<resources>
|
||||
<string name="app_name">Demo App</string>
|
||||
|
||||
<string name="onboarding_back">Back</string>
|
||||
<string name="onboarding_skip">Skip</string>
|
||||
<string name="onboarding_next">Next</string>
|
||||
<string name="onboarding_1_header">Shielded by Default</string>
|
||||
<string name="onboarding_1_body">Tired of worrying about which wallet you used last? US TOO! Now you don‘t have to, as all funds will automatically be moved to your shielded wallet (and migrated for you).</string>
|
||||
<string name="onboarding_1_image_content_description"></string>
|
||||
<string name="onboarding_2_header">Unified Addresses</string>
|
||||
<string name="onboarding_2_body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eget metus euismod, hendrerit dui vel, faucibus ante. Curabitur tortor elit, varius eu leo id, fringilla auctor odio. Donec fringilla tortor purus.</string>
|
||||
<string name="onboarding_2_image_content_description"></string>
|
||||
<string name="onboarding_3_header">And so much more…</string>
|
||||
<string name="onboarding_3_body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eget metus euismod, hendrerit dui vel, faucibus ante. Curabitur tortor elit, varius eu leo id, fringilla auctor odio. Donec fringilla tortor purus.</string>
|
||||
<string name="onboarding_3_image_content_description"></string>
|
||||
<string name="onboarding_4_header">Ready for the Future</string>
|
||||
<string name="onboarding_4_body">Let‘s get you set up!</string>
|
||||
<string name="onboarding_4_image_content_description"></string>
|
||||
<string name="onboarding_4_create_new_wallet">Create New Wallet</string>
|
||||
<string name="onboarding_4_import_existing_wallet">Import an Existing Wallet</string>
|
||||
</resources>
|
|
@ -0,0 +1,25 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
<style name="Theme.MyApplication.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.MyApplication.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<style name="Theme.MyApplication.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Mainnet Demo</string>
|
||||
<string name="network_name">Mainnet</string>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Testnet Demo</string>
|
||||
<string name="network_name">Testnet</string>
|
||||
</resources>
|
Loading…
Reference in New Issue