Skip to content

Commit 867f43e

Browse files
authored
Merge pull request #406 from synonymdev/feat/version-check
Optional and critical updates
2 parents 05d12bb + 1027ee5 commit 867f43e

File tree

8 files changed

+343
-7
lines changed

8 files changed

+343
-7
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package to.bitkit.data.dto
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
/**
7+
* Represents the root of the JSON object.
8+
*/
9+
@Serializable
10+
data class ReleaseInfoDTO(
11+
@SerialName("platforms")
12+
val platforms: Platforms,
13+
)
14+
15+
@Serializable
16+
data class Platforms(
17+
@SerialName("android")
18+
val android: PlatformDetails,
19+
@SerialName("ios")
20+
val ios: PlatformDetails?,
21+
)
22+
23+
/**
24+
* Holds the specific version information for a single platform.
25+
*/
26+
@Serializable
27+
data class PlatformDetails(
28+
@SerialName("version")
29+
val version: String,
30+
31+
@SerialName("buildNumber")
32+
val buildNumber: Int,
33+
34+
@SerialName("notes")
35+
val notes: String,
36+
37+
@SerialName("pub_date")
38+
val pubDate: String,
39+
40+
@SerialName("url")
41+
val url: String,
42+
43+
@SerialName("critical")
44+
val isCritical: Boolean,
45+
)

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ internal object Env {
172172
const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.fileprovider"
173173
const val APP_STORE_URL = "https://apps.apple.com/app/bitkit-wallet/id6502440655"
174174
const val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=to.bitkit"
175+
176+
const val RELEASE_URL = "https://github.com/synonymdev/bitkit-android/releases/download/updater/release.json"
175177
const val EXCHANGES_URL = "https://bitcoin.org/en/exchanges#international"
176178
const val BIT_REFILL_URL = "https://embed.bitrefill.com"
177179
const val BTC_MAP_URL = "https://btcmap.org/map"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package to.bitkit.services
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.request.get
5+
import io.ktor.client.statement.HttpResponse
6+
import io.ktor.client.statement.bodyAsText
7+
import io.ktor.http.isSuccess
8+
import kotlinx.serialization.json.Json
9+
import to.bitkit.data.dto.ReleaseInfoDTO
10+
import to.bitkit.env.Env
11+
import to.bitkit.utils.AppError
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class AppUpdaterService @Inject constructor(
17+
private val client: HttpClient,
18+
) {
19+
20+
suspend fun getReleaseInfo(): ReleaseInfoDTO {
21+
val response: HttpResponse = client.get(Env.RELEASE_URL)
22+
return when (response.status.isSuccess()) {
23+
true -> {
24+
val responseBody = runCatching {
25+
Json.decodeFromString<ReleaseInfoDTO>(response.bodyAsText())
26+
}.getOrElse {
27+
throw AppUpdaterError.InvalidResponse(it.message.orEmpty())
28+
}
29+
responseBody
30+
}
31+
32+
else -> throw AppUpdaterError.InvalidResponse(
33+
"Failed to fetch release info: ${response.status.description}"
34+
)
35+
}
36+
}
37+
}
38+
39+
sealed class AppUpdaterError(message: String) : AppError(message) {
40+
data class InvalidResponse(override val message: String) : AppUpdaterError(message)
41+
}

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import to.bitkit.ui.components.SheetHost
3939
import to.bitkit.ui.onboarding.InitializingWalletView
4040
import to.bitkit.ui.onboarding.WalletRestoreErrorView
4141
import to.bitkit.ui.onboarding.WalletRestoreSuccessView
42+
import to.bitkit.ui.screens.CriticalUpdateScreen
4243
import to.bitkit.ui.screens.profile.CreateProfileScreen
4344
import to.bitkit.ui.screens.profile.ProfileIntroScreen
4445
import to.bitkit.ui.screens.scanner.QrScanningScreen
@@ -136,6 +137,7 @@ import to.bitkit.ui.sheets.BackupSheet
136137
import to.bitkit.ui.sheets.LnurlAuthSheet
137138
import to.bitkit.ui.sheets.PinSheet
138139
import to.bitkit.ui.sheets.SendSheet
140+
import to.bitkit.ui.sheets.UpdateSheet
139141
import to.bitkit.ui.theme.TRANSITION_SHEET_MS
140142
import to.bitkit.ui.utils.AutoReadClipboardHandler
141143
import to.bitkit.ui.utils.Transitions
@@ -209,7 +211,7 @@ fun ContentView(
209211
LaunchedEffect(appViewModel) {
210212
appViewModel.mainScreenEffect.collect {
211213
when (it) {
212-
is MainScreenEffect.Navigate -> navController.navigate(it.route)
214+
is MainScreenEffect.Navigate -> navController.navigate(it.route, navOptions = it.navOptions)
213215
is MainScreenEffect.ProcessClipboardAutoRead -> {
214216
val isOnHome = navController.currentDestination?.hasRoute<Routes.Home>() == true
215217
if (!isOnHome) {
@@ -320,6 +322,7 @@ fun ContentView(
320322
onDismiss = { appViewModel.hideSheet() },
321323
sheets = {
322324
when (val sheet = currentSheet) {
325+
null -> Unit
323326
is Sheet.Send -> {
324327
SendSheet(
325328
appViewModel = appViewModel,
@@ -344,7 +347,7 @@ fun ContentView(
344347
is Sheet.Pin -> PinSheet(sheet, appViewModel)
345348
is Sheet.Backup -> BackupSheet(sheet, appViewModel)
346349
is Sheet.LnurlAuth -> LnurlAuthSheet(sheet, appViewModel)
347-
null -> Unit
350+
Sheet.Update -> UpdateSheet(onCancel = { appViewModel.hideSheet() })
348351
}
349352
}
350353
) {
@@ -404,6 +407,7 @@ private fun RootNavHost(
404407
suggestions(navController)
405408
support(navController)
406409
widgets(navController, settingsViewModel, currencyViewModel)
410+
update()
407411

408412
// TODO extract transferNavigation
409413
navigationWithDefaultTransitions<Routes.TransferRoot>(
@@ -1004,6 +1008,12 @@ private fun NavGraphBuilder.suggestions(
10041008
}
10051009
}
10061010

1011+
private fun NavGraphBuilder.update() {
1012+
composableWithDefaultTransitions<Routes.CriticalUpdate> {
1013+
CriticalUpdateScreen()
1014+
}
1015+
}
1016+
10071017
private fun NavGraphBuilder.support(
10081018
navController: NavHostController,
10091019
) {
@@ -1657,4 +1667,7 @@ sealed interface Routes {
16571667

16581668
@Serializable
16591669
data object AppStatus : Routes
1670+
1671+
@Serializable
1672+
data object CriticalUpdate : Routes
16601673
}

app/src/main/java/to/bitkit/ui/components/SheetHost.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ sealed interface Sheet {
3838
data object ActivityDateRangeSelector : Sheet
3939
data object ActivityTagSelector : Sheet
4040
data class LnurlAuth(val domain: String, val lnurl: String, val k1: String) : Sheet
41+
data object Update : Sheet
4142
}
4243

4344
@OptIn(ExperimentalMaterial3Api::class)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package to.bitkit.ui.screens
2+
3+
import android.content.Intent
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.aspectRatio
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.systemBarsPadding
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.platform.LocalContext
14+
import androidx.compose.ui.res.painterResource
15+
import androidx.compose.ui.res.stringResource
16+
import androidx.compose.ui.tooling.preview.Preview
17+
import androidx.compose.ui.unit.dp
18+
import androidx.core.net.toUri
19+
import to.bitkit.R
20+
import to.bitkit.env.Env
21+
import to.bitkit.ui.components.BodyM
22+
import to.bitkit.ui.components.Display
23+
import to.bitkit.ui.components.PrimaryButton
24+
import to.bitkit.ui.components.VerticalSpacer
25+
import to.bitkit.ui.scaffold.AppTopBar
26+
import to.bitkit.ui.theme.AppThemeSurface
27+
import to.bitkit.ui.theme.Colors
28+
import to.bitkit.ui.utils.withAccent
29+
30+
@Composable
31+
fun CriticalUpdateScreen() {
32+
val context = LocalContext.current
33+
34+
Column(
35+
modifier = Modifier
36+
.fillMaxSize()
37+
.padding(horizontal = 32.dp)
38+
.systemBarsPadding()
39+
) {
40+
AppTopBar(
41+
titleText = stringResource(R.string.other__update_critical_nav_title),
42+
onBackClick = null
43+
)
44+
45+
Image(
46+
painter = painterResource(R.drawable.exclamation_mark),
47+
contentDescription = null,
48+
modifier = Modifier
49+
.fillMaxWidth()
50+
.aspectRatio(1.0f)
51+
.weight(1f)
52+
)
53+
54+
Display(
55+
text = stringResource(R.string.other__update_critical_title)
56+
.withAccent(accentColor = Colors.Brand),
57+
color = Colors.White,
58+
)
59+
60+
BodyM(
61+
text = stringResource(R.string.other__update_critical_text),
62+
color = Colors.White64,
63+
)
64+
65+
VerticalSpacer(32.dp)
66+
67+
PrimaryButton(
68+
text = stringResource(R.string.other__update_critical_button),
69+
fullWidth = true,
70+
onClick = {
71+
context.startActivity(Intent(Intent.ACTION_VIEW, Env.PLAY_STORE_URL.toUri()))
72+
},
73+
)
74+
75+
VerticalSpacer(16.dp)
76+
}
77+
}
78+
79+
@Preview(showSystemUi = true)
80+
@Composable
81+
private fun Preview() {
82+
AppThemeSurface {
83+
CriticalUpdateScreen()
84+
}
85+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package to.bitkit.ui.sheets
2+
3+
import android.content.Intent
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.aspectRatio
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.navigationBarsPadding
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.platform.LocalContext
15+
import androidx.compose.ui.platform.testTag
16+
import androidx.compose.ui.res.painterResource
17+
import androidx.compose.ui.res.stringResource
18+
import androidx.compose.ui.tooling.preview.Preview
19+
import androidx.compose.ui.unit.dp
20+
import androidx.core.net.toUri
21+
import to.bitkit.R
22+
import to.bitkit.env.Env
23+
import to.bitkit.ui.components.BodyM
24+
import to.bitkit.ui.components.BottomSheetPreview
25+
import to.bitkit.ui.components.Display
26+
import to.bitkit.ui.components.PrimaryButton
27+
import to.bitkit.ui.components.SecondaryButton
28+
import to.bitkit.ui.components.SheetSize
29+
import to.bitkit.ui.components.VerticalSpacer
30+
import to.bitkit.ui.scaffold.SheetTopBar
31+
import to.bitkit.ui.shared.modifiers.sheetHeight
32+
import to.bitkit.ui.shared.util.gradientBackground
33+
import to.bitkit.ui.theme.AppThemeSurface
34+
import to.bitkit.ui.theme.Colors
35+
import to.bitkit.ui.utils.withAccent
36+
37+
@Composable
38+
fun UpdateSheet(
39+
onCancel: () -> Unit,
40+
) {
41+
val context = LocalContext.current
42+
43+
Column(
44+
modifier = Modifier
45+
.sheetHeight(SheetSize.LARGE)
46+
.gradientBackground()
47+
.navigationBarsPadding()
48+
.padding(horizontal = 32.dp)
49+
) {
50+
SheetTopBar(titleText = stringResource(R.string.other__update_nav_title))
51+
VerticalSpacer(16.dp)
52+
53+
Image(
54+
painter = painterResource(R.drawable.wand),
55+
contentDescription = null,
56+
modifier = Modifier
57+
.fillMaxWidth()
58+
.padding(horizontal = 28.dp)
59+
.aspectRatio(1.0f)
60+
.weight(1f)
61+
)
62+
63+
Display(
64+
text = stringResource(R.string.other__update_title)
65+
.withAccent(accentColor = Colors.Brand),
66+
color = Colors.White,
67+
)
68+
69+
BodyM(
70+
text = stringResource(R.string.other__update_text),
71+
color = Colors.White64,
72+
)
73+
74+
VerticalSpacer(32.dp)
75+
Row(
76+
modifier = Modifier
77+
.fillMaxWidth()
78+
.testTag("buttons_row"),
79+
horizontalArrangement = Arrangement.spacedBy(16.dp)
80+
) {
81+
SecondaryButton(
82+
text = stringResource(R.string.common__cancel),
83+
fullWidth = false,
84+
onClick = onCancel,
85+
modifier = Modifier
86+
.weight(1f),
87+
)
88+
89+
PrimaryButton(
90+
text = stringResource(R.string.other__update_button),
91+
fullWidth = false,
92+
onClick = {
93+
context.startActivity(Intent(Intent.ACTION_VIEW, Env.PLAY_STORE_URL.toUri()))
94+
},
95+
modifier = Modifier
96+
.weight(1f),
97+
)
98+
}
99+
VerticalSpacer(16.dp)
100+
}
101+
}
102+
103+
@Preview(showSystemUi = true)
104+
@Composable
105+
private fun Preview() {
106+
AppThemeSurface {
107+
BottomSheetPreview {
108+
UpdateSheet(
109+
onCancel = {},
110+
)
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)