Skip to content

Commit

Permalink
Feature/account onboarding (#20)
Browse files Browse the repository at this point in the history
# *account onboarding*

## ⚙️ Release Notes 

**Onboarding**:

- Consent
- Invitation
- Onboarding
- SequentialOnborading

**Account:**

- Login
- Register
- Credential Manager

## 📚 Documentation

tbd

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Signed-off-by: Basler182 <[email protected]>
Co-authored-by: Paul Schmiedmayer <[email protected]>
Co-authored-by: eldcn <[email protected]>
  • Loading branch information
3 people authored Jun 10, 2024
1 parent c03f2c8 commit d9bb114
Show file tree
Hide file tree
Showing 118 changed files with 5,166 additions and 135 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build-test-analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ jobs:
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $GOOGLE_SERVICES_JSON | base64 --decode >./app/google-services.json
# TODO: Add step to inject secrets.xml at ./modules/account/src/main/res/values/secrets.xml
- name: Build and test
run: ./gradlew build
- name: Perform CodeQL Analysis # CodeQL analysis needs project build!
uses: github/codeql-action/analyze@v3
with:
category: "/language:java-kotlin"
# TODO: remove threshold once changes have been unit tested
- name: Upload JaCoCo report to Codecov
uses: codecov/codecov-action@v4
with:
files: '**/build/reports/jacoco/jacocoCoverageReport/jacocoCoverageReport.xml'
flags: unittests
name: codecov-coverage
token: ${{ secrets.CODECOV_TOKEN }}
threshold: 20
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
*.class
*.dex

# Kotlin compiler
.kotlin

# Log file
*.log

Expand Down Expand Up @@ -53,3 +56,7 @@ build/
# built application files
*.apk
*.ap_

# services and secrets
app/google-services.json
modules/account/src/main/res/values/secrets.xml
3 changes: 3 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Codecov](https://codecov.io/gh/StanfordSpezi/SpeziKt/branch/main/graph/badge.svg)](https://app.codecov.io/gh/StanfordSpezi/SpeziKt)

# Spezi

Kotlin &amp; Android Version of the Stanford Spezi Framework

### An Ecosystem of Modules
Expand All @@ -13,3 +14,7 @@ Spezi is a collection of modules that can be used to build Android applications

- **Design System**: Provides a cohesive user interface and user experience
components. [Read More](./core/design/README.md)
- **Account**: Provides Account management components. [Read More](./modules/account/README.md)
- **Onboarding**: Provides Onboarding screens for the
application. [Read More](./modules/onboarding/README.md)
- **Contact**: Provides Contact screens. [Read More](./modules/contact/README.md)
24 changes: 22 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ plugins {
alias(libs.plugins.spezi.application)
alias(libs.plugins.spezi.compose)
alias(libs.plugins.spezi.hilt)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.google.gms.google.services)
}

android {
namespace = "edu.stanford.spezi.app"

defaultConfig {
applicationId = "edu.stanford.spezi.app"
applicationId = "edu.stanford.bdh.engagehf"
versionCode = 1
versionName = "1.0"

Expand All @@ -24,16 +27,33 @@ android {
buildTypes {
release {
isMinifyEnabled = false
buildConfigField("boolean", "USE_FIREBASE_EMULATOR", "false")
}
debug {
buildConfigField("boolean", "USE_FIREBASE_EMULATOR", "true")
}
}
}

dependencies {
implementation(project(":core:bluetooth"))
implementation(project(":core:coroutines"))
implementation(project(":core:navigation"))
implementation(project(":modules:account"))
implementation(project(":modules:onboarding"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.view.model.ktx)

implementation(libs.hilt.navigation.compose)
implementation(libs.navigation.compose)
implementation(libs.kotlinx.serialization.json)

implementation(project(":core:bluetooth"))
implementation(libs.firebase.functions.ktx)
implementation(libs.firebase.auth.ktx)
implementation(libs.firebase.firestore.ktx)
implementation(libs.firebase.storage.ktx)

androidTestImplementation(project(":core:testing"))
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Spezi">
Expand Down
126 changes: 125 additions & 1 deletion app/src/main/kotlin/edu/stanford/spezi/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,141 @@ package edu.stanford.spezi.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import dagger.hilt.android.AndroidEntryPoint
import edu.stanford.spezi.app.bluetooth.screen.BluetoothScreen
import edu.stanford.spezi.app.navigation.AppNavigationEvent
import edu.stanford.spezi.app.navigation.RegisterParams
import edu.stanford.spezi.app.navigation.Routes
import edu.stanford.spezi.app.navigation.serializableType
import edu.stanford.spezi.core.coroutines.di.Dispatching
import edu.stanford.spezi.core.design.theme.SpeziTheme
import edu.stanford.spezi.module.account.AccountNavigationEvent
import edu.stanford.spezi.module.account.login.LoginScreen
import edu.stanford.spezi.module.account.register.RegisterScreen
import edu.stanford.spezi.module.onboarding.OnboardingNavigationEvent
import edu.stanford.spezi.module.onboarding.consent.ConsentScreen
import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeScreen
import edu.stanford.spezi.module.onboarding.onboarding.OnboardingScreen
import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingScreen
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.reflect.typeOf

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val viewModel by viewModels<MainActivityViewModel>()

@Inject
@Dispatching.Main
lateinit var mainDispatcher: CoroutineDispatcher

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navHostController = rememberNavController()
SpeziTheme {
BluetoothScreen()
Navigator(navHostController = navHostController)
AppContent(navHostController = navHostController)
}
}
}

@Composable
private fun AppContent(navHostController: NavHostController) {
NavHost(
navController = navHostController,
startDestination = Routes.OnboardingScreen,
) {
registerAppGraph()
}
}

private fun NavGraphBuilder.registerAppGraph() {
composable<Routes.RegisterScreen>(
typeMap = mapOf(
typeOf<RegisterParams>() to serializableType<RegisterParams>()
)
) {
val args = it.toRoute<Routes.RegisterScreen>()
RegisterScreen(
args.registerParams.isGoogleSignUp,
args.registerParams.email,
args.registerParams.password
)
}

composable<Routes.LoginScreen> {
val args = it.toRoute<Routes.LoginScreen>()
LoginScreen(isAlreadyRegistered = args.isAlreadyRegistered)
}

composable<Routes.BluetoothScreen> {
BluetoothScreen()
}

composable<Routes.InvitationCodeScreen> {
InvitationCodeScreen()
}

composable<Routes.OnboardingScreen> {
OnboardingScreen()
}

composable<Routes.SequentialOnboardingScreen> {
SequentialOnboardingScreen()
}

composable<Routes.ConsentScreen> {
ConsentScreen()
}
}

@Composable
private fun Navigator(navHostController: NavHostController) {
LaunchedEffect(key1 = Unit) {
launch(mainDispatcher) {
viewModel.getNavigationEvents().collect { event ->
when (event) {
is AccountNavigationEvent.RegisterScreen -> navHostController.navigate(
Routes.RegisterScreen(
registerParams = RegisterParams(
isGoogleSignUp = event.isGoogleSignUp,
email = event.email,
password = event.password
),
)
)

is AccountNavigationEvent.LoginScreen -> navHostController.navigate(
Routes.LoginScreen(
isAlreadyRegistered = event.isAlreadyRegistered
)
)

is AppNavigationEvent.BluetoothScreen -> navHostController.navigate(Routes.BluetoothScreen)
is OnboardingNavigationEvent.InvitationCodeScreen -> navHostController.navigate(
Routes.InvitationCodeScreen
)

is OnboardingNavigationEvent.OnboardingScreen -> navHostController.navigate(Routes.OnboardingScreen)
is OnboardingNavigationEvent.SequentialOnboardingScreen -> navHostController.navigate(
Routes.SequentialOnboardingScreen
)

is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate(Routes.ConsentScreen)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package edu.stanford.spezi.app

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import edu.stanford.spezi.app.navigation.AppNavigationEvent
import edu.stanford.spezi.core.logging.speziLogger
import edu.stanford.spezi.core.navigation.NavigationEvent
import edu.stanford.spezi.core.navigation.Navigator
import edu.stanford.spezi.module.account.AccountEvents
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val accountEvents: AccountEvents,
private val navigator: Navigator,
) : ViewModel() {
private val logger by speziLogger()

init {
viewModelScope.launch {
accountEvents.events.collect { event ->
when (event) {
is AccountEvents.Event.SignUpSuccess, AccountEvents.Event.SignInSuccess -> {
navigator.navigateTo(AppNavigationEvent.BluetoothScreen)
}
else -> {
logger.i { "Ignoring registration event: $event" }
}
}
}
}
}

fun getNavigationEvents(): Flow<NavigationEvent> = navigator.events
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.stanford.spezi.app.navigation

import edu.stanford.spezi.core.navigation.NavigationEvent

sealed interface AppNavigationEvent : NavigationEvent {
data object BluetoothScreen : AppNavigationEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package edu.stanford.spezi.app.navigation

import android.os.Bundle
import androidx.navigation.NavType
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

internal inline fun <reified T : Any> serializableType(
isNullableAllowed: Boolean = false,
json: Json = Json,
) = object : NavType<T>(isNullableAllowed = isNullableAllowed) {
override fun get(bundle: Bundle, key: String) =
bundle.getString(key)?.let<String, T>(json::decodeFromString)

override fun parseValue(value: String): T = json.decodeFromString(value)

override fun serializeAsValue(value: T): String = json.encodeToString(value)

override fun put(bundle: Bundle, key: String, value: T) {
bundle.putString(key, json.encodeToString(value))
}
}
33 changes: 33 additions & 0 deletions app/src/main/kotlin/edu/stanford/spezi/app/navigation/Routes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package edu.stanford.spezi.app.navigation

import kotlinx.serialization.Serializable

@Serializable
sealed class Routes {

@Serializable
data class RegisterScreen(
val registerParams: @Serializable RegisterParams,
) : Routes()

@Serializable
data class LoginScreen(val isAlreadyRegistered: @Serializable Boolean = true) : Routes()

@Serializable
data object SequentialOnboardingScreen : Routes()

@Serializable
data object InvitationCodeScreen : Routes()

@Serializable
data object OnboardingScreen : Routes()

@Serializable
data object ConsentScreen : Routes()

@Serializable
data object BluetoothScreen : Routes()
}

@Serializable
data class RegisterParams(val isGoogleSignUp: Boolean, val email: String, val password: String)
Loading

0 comments on commit d9bb114

Please sign in to comment.