Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,25 @@ android {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
resValue 'string', 'asset_statements', """
[{
"include": "${getProperty('oneTapHost')}/.well-known/assetlinks.json"
}]
"""
}

buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled true
buildConfigField "String", "BASE_URL", "\"https://cataas.com/api/\""
buildConfigField "String", "ONE_TAP_URL", "\"${getProperty('oneTapHost')}\""
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
buildConfigField "String", "BASE_URL", "\"https://cataas.com/api/\""
buildConfigField "String", "ONE_TAP_URL", "\"${getProperty('oneTapHost')}\""
}
}
compileOptions.coreLibraryDesugaringEnabled = true
Expand Down Expand Up @@ -72,7 +79,8 @@ ktlint {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.8")

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutine_version"

implementation "androidx.navigation:navigation-runtime-ktx:2.5.1"
implementation "androidx.navigation:navigation-compose:2.5.1"
Expand Down Expand Up @@ -161,4 +169,5 @@ dependencies {
implementation "androidx.health:health-connect-client:1.0.0-alpha03"
implementation "com.google.android.gms:play-services-wallet:19.1.0"
implementation "com.google.android.gms:play-services-pay:16.0.3"
implementation 'com.google.android.gms:play-services-auth:20.3.0'
}
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_content"
tools:targetApi="32">

<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />

<activity
android:name="com.skyyo.samples.application.activity.MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ sealed class Destination(val route: String) {
object TextSpans : Destination("textSpans")
object ImeAwareLazyColumn : Destination("imeAwareLazyColumn")
object TextGradient : Destination("textGradient")
object OneTap : Destination("oneTap")
object OneTapSignUpFinish : Destination("oneTapSignUpFinish")
object OneTapAuthorised : Destination("oneTapAuthorised")
}

sealed class ProfileGraph(val route: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ import com.skyyo.samples.features.navigateWithResult.withObject.catFeed.CatFeedS
import com.skyyo.samples.features.navigationCores.bottomBar.BottomBarScreen
import com.skyyo.samples.features.navigationCores.drawer.DrawerScreen
import com.skyyo.samples.features.noticeableScrollableRow.NoticeableScrollableRowScreen
import com.skyyo.samples.features.oneTap.CreateUserScreen
import com.skyyo.samples.features.oneTap.OneTapScreen
import com.skyyo.samples.features.oneTap.UserScreen
import com.skyyo.samples.features.otp.OtpScreen
import com.skyyo.samples.features.pagination.paging.CatsPagingScreen
import com.skyyo.samples.features.pagination.pagingWithDatabase.CatsPagingRoomScreen
Expand Down Expand Up @@ -196,4 +199,7 @@ fun PopulatedNavHost(
composable(Destination.TextSpans.route) { TextSpansScreen() }
composable(Destination.ImeAwareLazyColumn.route) { ImeAwareLazyColumnScreen() }
composable(Destination.TextGradient.route) { TextGradientScreen() }
composable(Destination.OneTap.route) { OneTapScreen() }
composable(Destination.OneTapAuthorised.route) { UserScreen() }
composable(Destination.OneTapSignUpFinish.route) { CreateUserScreen() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.skyyo.samples.application.injection

import com.skyyo.samples.BuildConfig
import com.skyyo.samples.application.network.calls.CatCalls
import com.skyyo.samples.application.network.calls.OneTapCalls
import dagger.Lazy
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -45,4 +46,8 @@ object NetworkModule {
@Singleton
@Provides
fun provideCatsCalls(retrofit: Retrofit): CatCalls = retrofit.create(CatCalls::class.java)

@Singleton
@Provides
fun provideOneTapCalls(retrofit: Retrofit): OneTapCalls = retrofit.create(OneTapCalls::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.skyyo.samples.application.models

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
class OneTapAuthoriseUserRequest(val token: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.skyyo.samples.application.models

import android.os.Parcelable
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize

@Parcelize
@JsonClass(generateAdapter = true)
data class OneTapUser(
val id: String,
val name: String,
val surname: String,
val isCompleted: Boolean,
val phone: String = ""
) : Parcelable {
companion object {
val empty = OneTapUser(id = "", name = "", surname = "", isCompleted = false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.skyyo.samples.application.network.calls

import com.skyyo.samples.BuildConfig
import com.skyyo.samples.application.models.OneTapAuthoriseUserRequest
import com.skyyo.samples.application.models.OneTapUser
import retrofit2.Response
import retrofit2.http.*

interface OneTapCalls {

@POST("${BuildConfig.ONE_TAP_URL}/auth/oneTapAuthorise")
suspend fun authorise(@Body body: OneTapAuthoriseUserRequest): Response<OneTapUser>

@PUT("${BuildConfig.ONE_TAP_URL}/auth/oneTapUpdateUser")
suspend fun updateUser(@Body user: OneTapUser): Response<Void>

@DELETE("${BuildConfig.ONE_TAP_URL}/auth/oneTapDeleteUser")
suspend fun deleteUser(@Query("userId") userId: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.skyyo.samples.features.oneTap

import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.flowWithLifecycle
import com.skyyo.samples.extensions.toast
import kotlinx.coroutines.flow.receiveAsFlow

@Composable
fun CreateUserScreen(viewModel: CreateUserViewModel = hiltViewModel()) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val events = remember {
viewModel.events.receiveAsFlow().flowWithLifecycle(lifecycleOwner.lifecycle)
}

LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is UserEvent.ShowToast -> context.toast(event.message)
}
}
}

val user by viewModel.user.collectAsState()
Column(
modifier = Modifier.systemBarsPadding(),
verticalArrangement = remember { Arrangement.spacedBy(20.dp) }
) {
LabeledTextField(label = "User name: ", text = user.name)
LabeledTextField(label = "User surname: ", text = user.surname)
LabeledTextField(
label = "User phone: ",
text = user.phone,
onTextChanged = viewModel::setPhone
)
Button(onClick = viewModel::applyUpdate) {
Text(text = "apply")
}
}
}

@Composable
private fun LabeledTextField(label: String, text: String, onTextChanged: (String) -> Unit = {}) {
val labelStyle = remember {
TextStyle(
fontSize = 12.sp,
color = Color.DarkGray,
fontWeight = FontWeight.W300
)
}
val textStyle = remember {
TextStyle(
fontSize = 16.sp,
color = Color.Black,
fontWeight = FontWeight.W600
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = label, style = labelStyle)
TextField(
modifier = Modifier.padding(start = 10.dp),
value = text,
textStyle = textStyle,
onValueChange = onTextChanged
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.skyyo.samples.features.oneTap

import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.skyyo.samples.application.CODE_200
import com.skyyo.samples.application.Destination
import com.skyyo.samples.application.models.OneTapUser
import com.skyyo.samples.application.network.calls.OneTapCalls
import com.skyyo.samples.extensions.navigateWithObject
import com.skyyo.samples.extensions.tryOrNull
import com.skyyo.samples.utils.NavigationDispatcher
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import javax.inject.Inject

const val CREATE_USER_KEY = "user"

@HiltViewModel
class CreateUserViewModel @Inject constructor(
private val handle: SavedStateHandle,
private val oneTapCalls: OneTapCalls,
private val navigationDispatcher: NavigationDispatcher
) : ViewModel() {
val events = Channel<UserEvent>(Channel.UNLIMITED)
val user = handle.getStateFlow(CREATE_USER_KEY, OneTapUser.empty)

fun setPhone(phone: String) {
handle[CREATE_USER_KEY] = user.value.copy(phone = phone)
}

fun applyUpdate() = viewModelScope.launch(Dispatchers.IO) {
val response = tryOrNull { oneTapCalls.updateUser(user.value) }
when {
response?.code() == CODE_200 -> {
navigationDispatcher.emit {
it.popBackStack()
it.navigateWithObject(
route = Destination.OneTapAuthorised.route,
arguments = bundleOf(USER_KEY to user.value)
)
}
}
else -> {
val message = when (response) {
null -> "No internet"
else -> "Update failed with code: ${response.code()}"
}
events.send(UserEvent.ShowToast(message))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.skyyo.samples.features.oneTap

sealed class OneTapEvent {
class ShowToast(val message: String) : OneTapEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.skyyo.samples.features.oneTap

import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.flowWithLifecycle
import com.google.android.gms.auth.api.identity.Identity
import com.skyyo.samples.extensions.toast
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.tasks.await

@Composable
fun OneTapScreen(viewModel: OneTapViewModel = hiltViewModel()) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val events = remember {
viewModel.events.receiveAsFlow().flowWithLifecycle(lifecycleOwner.lifecycle)
}

LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is OneTapEvent.ShowToast -> context.toast(event.message)
}
}
}

val oneTapClient = remember { Identity.getSignInClient(context) }
val oneTapLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
when (it.resultCode) {
Activity.RESULT_OK -> viewModel.oneTapAccepted(oneTapClient, it.data)
Activity.RESULT_CANCELED -> viewModel.oneTapRejected()
}
}
val isOneTapUiRejected by viewModel.isOneTapUiRejected.collectAsState()

LaunchedEffect(isOneTapUiRejected) {
if (!isOneTapUiRejected) {
try {
val signInResult = oneTapClient.beginSignIn(viewModel.oneTapRequest).await()
oneTapLauncher.launch(
IntentSenderRequest.Builder(signInResult.pendingIntent.intentSender).build()
)
} catch (e: Exception) {
// No Google Accounts found. Just continue presenting the signed-out UI.
}
}
}

Column(Modifier.fillMaxSize().systemBarsPadding()) {
Text(text = "Unauthorised content")
}
}
Loading