Skip to content

Commit 601b7a7

Browse files
committed
chore: add initial in-app update handling
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent 408874a commit 601b7a7

File tree

17 files changed

+538
-1
lines changed

17 files changed

+538
-1
lines changed

apps/flipcash/app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ dependencies {
141141
implementation(project(":apps:flipcash:core"))
142142
implementation(project(":apps:flipcash:shared:accesskey"))
143143
implementation(project(":apps:flipcash:shared:appsettings"))
144+
implementation(project(":apps:flipcash:shared:appupdates"))
144145
implementation(project(":apps:flipcash:shared:authentication"))
145146
implementation(project(":apps:flipcash:shared:bill-customization"))
146147
implementation(project(":apps:flipcash:shared:featureflags"))
@@ -170,6 +171,7 @@ dependencies {
170171
implementation(project(":apps:flipcash:features:lab"))
171172
implementation(project(":apps:flipcash:features:advanced"))
172173
implementation(project(":apps:flipcash:features:appsettings"))
174+
implementation(project(":apps:flipcash:features:appupdates"))
173175
implementation(project(":apps:flipcash:features:deposit"))
174176
implementation(project(":apps:flipcash:features:myaccount"))
175177
implementation(project(":apps:flipcash:features:backupkey"))

apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import com.flipcash.app.router.Router
3232
import com.flipcash.app.session.LocalSessionController
3333
import com.flipcash.app.shareable.LocalShareController
3434
import com.flipcash.app.shareable.ShareSheetController
35+
import com.flipcash.app.updates.AppUpdateController
36+
import com.flipcash.app.updates.LocalAppUpdater
3537
import com.flipcash.services.analytics.FlipcashAnalyticsService
3638
import com.flipcash.services.user.UserManager
3739
import com.getcode.libs.analytics.LocalAnalytics
@@ -123,6 +125,10 @@ class MainActivity : FragmentActivity() {
123125
@Inject
124126
lateinit var billPlaygroundController: BillPlaygroundController
125127

128+
@Inject
129+
lateinit var appUpdater: AppUpdateController
130+
131+
126132
override fun onCreate(savedInstanceState: Bundle?) {
127133
super.onCreate(savedInstanceState)
128134
handleUncaughtException()
@@ -149,6 +155,7 @@ class MainActivity : FragmentActivity() {
149155
LocalOnRampAmountController provides onRampAmountController,
150156
LocalPhoneUtils provides phoneUtils,
151157
LocalBillPlaygroundController provides billPlaygroundController,
158+
LocalAppUpdater provides appUpdater,
152159
) {
153160
Rinku {
154161
App(

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.flipcash.app.payments.PaymentScaffold
3939
import com.flipcash.app.router.LocalRouter
4040
import com.flipcash.app.session.LocalSessionController
4141
import com.flipcash.app.theme.FlipcashDesignSystem
42+
import com.flipcash.app.updates.UpdateRequiredBlockingView
4243
import com.flipcash.features.shareapp.R
4344
import com.flipcash.services.modals.ModalManager
4445
import com.flipcash.services.user.AuthState
@@ -263,6 +264,7 @@ internal fun App(
263264
}
264265

265266
BiometricsBlockingView(modifier = Modifier.fillMaxSize(), biometricsState)
267+
UpdateRequiredBlockingView(modifier = Modifier.fillMaxSize(), biometricsState = biometricsState)
266268
TopBarContainer(barManager.barMessages)
267269
BottomBarContainer(barManager.barMessages)
268270
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="128dp"
3+
android:height="128dp"
4+
android:viewportWidth="128"
5+
android:viewportHeight="128">
6+
<path
7+
android:pathData="M64,40V69.33M77.33,58.67L64,72L50.67,58.67M45.33,88H82.67M114.67,64C114.67,91.98 91.98,114.67 64,114.67C36.02,114.67 13.33,91.98 13.33,64C13.33,36.02 36.02,13.33 64,13.33C91.98,13.33 114.67,36.02 114.67,64Z"
8+
android:strokeWidth="5.33333"
9+
android:fillColor="#00000000"
10+
android:strokeColor="#ffffff"
11+
android:strokeLineCap="square"/>
12+
</vector>

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,7 @@
433433
<string name="error_description_CashReturnedToWallet">The cash was returned to your wallet</string>
434434

435435
<string name="label_mintDate">Minted: %1$s</string>
436+
437+
<string name="title_updateRequired">Update Required</string>
438+
<string name="subtitle_updateRequired">The latest features of Flipcash require you to update to the latest version</string>
436439
</resources>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
.gradle/
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
id(Plugins.android_library)
5+
id(Plugins.kotlin_android)
6+
id(Plugins.kotlin_ksp)
7+
id(Plugins.hilt)
8+
id(Plugins.kotlin_parcelize)
9+
id(Plugins.jetbrains_compose_compiler)
10+
}
11+
12+
android {
13+
namespace = "${Gradle.flipcashNamespace}.features.appupdates"
14+
compileSdk = Android.compileSdkVersion
15+
defaultConfig {
16+
minSdk = Android.minSdkVersion
17+
testInstrumentationRunner = Android.testInstrumentationRunner
18+
}
19+
20+
buildFeatures {
21+
buildConfig = true
22+
compose = true
23+
}
24+
}
25+
26+
kotlin {
27+
jvmToolchain {
28+
languageVersion.set(JavaLanguageVersion.of(Versions.java))
29+
}
30+
31+
compilerOptions {
32+
jvmTarget.set(JvmTarget.fromTarget(Versions.java))
33+
optIn.addAll(
34+
"kotlin.time.ExperimentalTime",
35+
"kotlin.ExperimentalUnsignedTypes",
36+
"kotlin.RequiresOptIn"
37+
)
38+
}
39+
}
40+
41+
dependencies {
42+
implementation(Libs.inject)
43+
implementation(Libs.hilt)
44+
ksp(Libs.hilt_android_compiler)
45+
ksp(Libs.hilt_compiler)
46+
47+
implementation(Libs.timber)
48+
49+
implementation(platform(Libs.compose_bom))
50+
implementation(Libs.compose_ui)
51+
implementation(Libs.compose_animation)
52+
implementation(Libs.compose_foundation)
53+
implementation(Libs.compose_material)
54+
implementation(Libs.compose_materialIconsExtended)
55+
56+
implementation(project(":apps:flipcash:core"))
57+
implementation(project(":apps:flipcash:shared:appupdates"))
58+
implementation(project(":ui:analytics"))
59+
implementation(project(":ui:core"))
60+
implementation(project(":ui:biometrics"))
61+
implementation(project(":ui:components"))
62+
implementation(project(":ui:navigation"))
63+
implementation(project(":ui:resources"))
64+
implementation(project(":ui:theme"))
65+
implementation(Libs.rinku_compose)
66+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.flipcash.app.updates
2+
3+
import androidx.compose.runtime.compositionLocalOf
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
import kotlinx.coroutines.flow.StateFlow
6+
7+
val LocalAppUpdater = compositionLocalOf<AppUpdateController> { StubAppUpdateController() }
8+
9+
private class StubAppUpdateController: AppUpdateController {
10+
override val availableUpdate: StateFlow<UpdateInfo?> = MutableStateFlow(null)
11+
12+
override suspend fun checkForUpdate() = Unit
13+
14+
override suspend fun startUpdate(): Result<Unit> {
15+
return Result.failure(Throwable("This is a stub implementation"))
16+
}
17+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.flipcash.app.updates
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.fadeIn
5+
import androidx.compose.animation.fadeOut
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.navigationBarsPadding
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.material.Icon
14+
import androidx.compose.material.Surface
15+
import androidx.compose.material.Text
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.CompositionLocalProvider
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.rememberCoroutineScope
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.res.painterResource
24+
import androidx.compose.ui.res.stringResource
25+
import androidx.compose.ui.text.style.TextAlign
26+
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.lifecycle.Lifecycle
28+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
29+
import com.flipcash.app.theme.FlipcashDesignSystem
30+
import com.flipcash.features.appupdates.R
31+
import com.getcode.theme.CodeTheme
32+
import com.getcode.ui.biometrics.BiometricsState
33+
import com.getcode.ui.theme.ButtonState
34+
import com.getcode.ui.theme.CodeButton
35+
import com.getcode.ui.theme.CodeScaffold
36+
import com.getcode.ui.utils.RepeatOnLifecycle
37+
import com.google.android.play.core.install.model.InstallStatus
38+
import com.google.android.play.core.install.model.UpdateAvailability
39+
import kotlinx.coroutines.flow.MutableStateFlow
40+
import kotlinx.coroutines.launch
41+
42+
@Composable
43+
fun UpdateRequiredBlockingView(
44+
biometricsState: BiometricsState,
45+
modifier: Modifier = Modifier,
46+
) {
47+
val appUpdater = LocalAppUpdater.current
48+
val availableUpdate by appUpdater.availableUpdate.collectAsStateWithLifecycle()
49+
val composeScope = rememberCoroutineScope()
50+
51+
AnimatedVisibility(
52+
visible = availableUpdate?.isUpdateAvailable == true && biometricsState.passed,
53+
enter = fadeIn(),
54+
exit = fadeOut()
55+
) {
56+
Surface(
57+
modifier = modifier,
58+
color = CodeTheme.colors.brand.copy(alpha = 0.87f),
59+
) {
60+
CodeScaffold(
61+
bottomBar = {
62+
CodeButton(
63+
modifier = Modifier
64+
.fillMaxWidth()
65+
.padding(horizontal = CodeTheme.dimens.inset)
66+
.padding(bottom = CodeTheme.dimens.grid.x1)
67+
.navigationBarsPadding(),
68+
onClick = {
69+
composeScope.launch {
70+
appUpdater.startUpdate()
71+
}
72+
},
73+
text = stringResource(id = R.string.action_unlockCode),
74+
buttonState = ButtonState.Filled
75+
)
76+
}
77+
) { padding ->
78+
Box(
79+
modifier = Modifier
80+
.fillMaxSize()
81+
.padding(padding)
82+
) {
83+
Column(
84+
modifier = Modifier
85+
.fillMaxWidth()
86+
.padding(horizontal = CodeTheme.dimens.inset)
87+
.align(Alignment.Center),
88+
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4),
89+
horizontalAlignment = Alignment.CenterHorizontally
90+
) {
91+
Icon(
92+
painter = painterResource(R.drawable.ic_app_update_required),
93+
contentDescription = null,
94+
)
95+
96+
Column(
97+
modifier = Modifier.fillMaxWidth(),
98+
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
99+
horizontalAlignment = Alignment.CenterHorizontally,
100+
) {
101+
Text(
102+
text = stringResource(R.string.title_updateRequired),
103+
style = CodeTheme.typography.textLarge,
104+
color = CodeTheme.colors.textMain,
105+
)
106+
Text(
107+
modifier = Modifier
108+
.fillMaxWidth(0.8f),
109+
text = stringResource(R.string.subtitle_updateRequired),
110+
style = CodeTheme.typography.textMedium,
111+
color = CodeTheme.colors.textSecondary,
112+
textAlign = TextAlign.Center,
113+
)
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
121+
RepeatOnLifecycle(
122+
targetState = Lifecycle.State.RESUMED
123+
) {
124+
appUpdater.checkForUpdate()
125+
}
126+
}
127+
128+
@Composable
129+
@Preview
130+
private fun PreviewUpdateRequiredView() {
131+
FlipcashDesignSystem {
132+
val appUpdater = remember {
133+
object : AppUpdateController {
134+
override val availableUpdate = MutableStateFlow<UpdateInfo?>(
135+
UpdateInfo(
136+
updateAvailability = UpdateAvailability.UPDATE_AVAILABLE,
137+
updatePriority = 5,
138+
clientVersionStalenessDays = null,
139+
bytesDownloaded = 0,
140+
totalBytesToDownload = 100,
141+
installStatus = InstallStatus.UNKNOWN,
142+
availableVersionCode = 3000
143+
)
144+
)
145+
146+
override suspend fun checkForUpdate() = Unit
147+
148+
override suspend fun startUpdate(): Result<Unit> = Result.success(Unit)
149+
150+
}
151+
}
152+
CompositionLocalProvider(LocalAppUpdater provides appUpdater) {
153+
UpdateRequiredBlockingView(
154+
biometricsState = BiometricsState(passed = true),
155+
)
156+
}
157+
}
158+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
.gradle/

0 commit comments

Comments
 (0)