Skip to content

Commit c46d817

Browse files
committed
feat(onboarding): add notification opt-in back
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 051b8b5 commit c46d817

16 files changed

Lines changed: 885 additions & 300 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import com.flipcash.app.myaccount.MyAccountScreen
4141
import com.flipcash.app.onramp.OnRampCustomAmountScreen
4242
import com.flipcash.app.onramp.OnRampFlowTracker
4343
import com.flipcash.app.onramp.OnRampProviderListScreen
44+
import com.flipcash.app.permissions.NotificationPermissionRationaleScreen
45+
import com.flipcash.app.permissions.NotificationPermissionScreen
4446
import com.flipcash.app.purchase.PurchaseAccountScreen
4547
import com.flipcash.app.scanner.ScannerScreen
4648
import com.flipcash.app.shareapp.ShareAppScreen
@@ -89,7 +91,8 @@ fun appEntryProvider(
8991
annotatedEntry<AppRoute.Onboarding.AccessKey> { AccessKeyScreen() }
9092
annotatedEntry<AppRoute.Onboarding.AccessKeySavedLocation> { PhotoAccessKeyScreen() }
9193
annotatedEntry<AppRoute.Onboarding.Purchase> { key -> PurchaseAccountScreen(key.fromLogin) }
92-
annotatedEntry<AppRoute.Onboarding.NotificationPermission> { }
94+
annotatedEntry<AppRoute.Onboarding.NotificationPermission> { key -> NotificationPermissionScreen(key.postCreate) }
95+
annotatedEntry<AppRoute.Onboarding.NotificationPermissionRationale> { key -> NotificationPermissionRationaleScreen(key.permanentlyDenied) }
9396
annotatedEntry<AppRoute.Onboarding.CameraPermission> { }
9497

9598
// Main

apps/flipcash/core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020
implementation(libs.androidx.credentials)
2121
implementation(libs.androidx.credentials.play.auth)
2222
implementation(libs.androidx.datastore)
23+
implementation(libs.compose.material3)
2324

2425
api(libs.coil3)
2526
api(libs.coil3.network)

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,47 @@ import kotlinx.serialization.Serializable
1919
sealed interface AppRoute : NavKey, Parcelable {
2020

2121
/** Initial loading/splash route shown while auth state resolves. */
22-
@Serializable @Parcelize data object Loading : AppRoute
22+
@Serializable
23+
@Parcelize
24+
data object Loading : AppRoute
2325

2426
@Serializable
2527
@Parcelize
2628
// TODO: turn into a Flow
27-
sealed interface Onboarding: AppRoute {
28-
@Serializable data class Login(val seed: String? = null, val fromDeeplink: Boolean = false) : Onboarding
29-
@Serializable data object SeedInput : Onboarding
30-
@Serializable data object AccessKey : Onboarding
31-
@Serializable data object AccessKeySavedLocation: Onboarding
32-
@Serializable data class Purchase(val fromLogin: Boolean = false) : Onboarding
33-
@Deprecated("Onboarding streamlined; permissions now requested at time of use")
34-
@Serializable data class NotificationPermission(val postCreate: Boolean = false) : Onboarding
29+
sealed interface Onboarding : AppRoute {
30+
@Serializable
31+
data class Login(val seed: String? = null, val fromDeeplink: Boolean = false) : Onboarding
32+
@Serializable
33+
data object SeedInput : Onboarding
34+
@Serializable
35+
data object AccessKey : Onboarding
36+
@Serializable
37+
data object AccessKeySavedLocation : Onboarding
38+
@Serializable
39+
data class Purchase(val fromLogin: Boolean = false) : Onboarding
40+
41+
@Serializable
42+
data class NotificationPermission(val postCreate: Boolean = false) : Onboarding
43+
@Serializable
44+
data class NotificationPermissionRationale(val permanentlyDenied: Boolean = false) : Onboarding
45+
3546
@Deprecated("Onboarding streamlined; permissions now requested at time of use")
36-
@Serializable data class CameraPermission(val postCreate: Boolean = false) : Onboarding
47+
@Serializable
48+
data class CameraPermission(val postCreate: Boolean = false) : Onboarding
3749
}
3850

3951

4052
@Serializable
4153
@Parcelize
42-
sealed interface Main: AppRoute {
43-
@Serializable data class AppRestricted(val restrictionType: RestrictionType) : Main
44-
@Serializable data object Scanner : Main
54+
sealed interface Main : AppRoute {
55+
@Serializable
56+
data class AppRestricted(val restrictionType: RestrictionType) : Main
57+
@Serializable
58+
data object Scanner : Main
4559

4660
// TODO: is there a better place for this to live?
47-
@Serializable data class RegionSelection(val kind: RegionSelectionKind) : Main
61+
@Serializable
62+
data class RegionSelection(val kind: RegionSelectionKind) : Main
4863

4964
@Serializable
5065
@Parcelize
@@ -63,32 +78,54 @@ sealed interface AppRoute : NavKey, Parcelable {
6378
val includeEmail: Boolean = true,
6479
val email: String? = null,
6580
val emailVerificationCode: String? = null,
66-
): AppRoute
81+
) : AppRoute
6782

6883
@Serializable
6984
@Parcelize
70-
sealed interface Sheets: AppRoute {
71-
@Serializable data class TokenSelection(val purpose: TokenPurpose): Sheets
72-
@Serializable data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets
73-
@Serializable data object Wallet : Sheets
74-
@Serializable data object Menu : Sheets
75-
@Serializable data object Lab: Sheets
76-
@Serializable data object ShareApp : Sheets
85+
sealed interface Sheets : AppRoute {
86+
@Serializable
87+
data class TokenSelection(val purpose: TokenPurpose) : Sheets
88+
@Serializable
89+
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets
90+
@Serializable
91+
data object Wallet : Sheets
92+
@Serializable
93+
data object Menu : Sheets
94+
@Serializable
95+
data object Lab : Sheets
96+
@Serializable
97+
data object ShareApp : Sheets
7798
}
7899

79100
@Serializable
80101
@Parcelize
81-
sealed interface Token: AppRoute {
82-
@Serializable data class Info(val mint: Mint, val forNeededFunds: Boolean = false, val fromDeeplink: Boolean = false): Token
83-
@Serializable data class Transactions(val mint: Mint): Token
84-
@Serializable data class SwapTransact(val purpose: TokenSwapPurpose, val forNeededFunds: Boolean = false): Token
85-
@Serializable data class TxProcessing(val swapId: SwapId, val awaitExternalWallet: Boolean = false): Token, NonDismissableRoute, NonDraggableRoute
86-
@Serializable data object SellReceipt: Token
102+
sealed interface Token : AppRoute {
103+
@Serializable
104+
data class Info(
105+
val mint: Mint,
106+
val forNeededFunds: Boolean = false,
107+
val fromDeeplink: Boolean = false
108+
) : Token
109+
110+
@Serializable
111+
data class Transactions(val mint: Mint) : Token
112+
@Serializable
113+
data class SwapTransact(
114+
val purpose: TokenSwapPurpose,
115+
val forNeededFunds: Boolean = false
116+
) : Token
117+
118+
@Serializable
119+
data class TxProcessing(val swapId: SwapId, val awaitExternalWallet: Boolean = false) :
120+
Token, NonDismissableRoute, NonDraggableRoute
121+
122+
@Serializable
123+
data object SellReceipt : Token
87124
}
88125

89126
@Serializable
90127
@Parcelize
91-
sealed interface OnRamp: AppRoute {
128+
sealed interface OnRamp : AppRoute {
92129
@Serializable
93130
data class ProviderList(
94131
val from: AppRoute? = null,
@@ -101,27 +138,36 @@ sealed interface AppRoute : NavKey, Parcelable {
101138

102139
@Serializable
103140
@Parcelize
104-
sealed interface Transfers: AppRoute {
141+
sealed interface Transfers : AppRoute {
105142

106143
sealed interface Withdrawal {
107-
@Serializable data class Amount(val mint: Mint) : Transfers
108-
@Serializable data object Destination : Transfers
109-
@Serializable data object Confirmation : Transfers
144+
@Serializable
145+
data class Amount(val mint: Mint) : Transfers
146+
@Serializable
147+
data object Destination : Transfers
148+
@Serializable
149+
data object Confirmation : Transfers
110150
}
111151
}
112152

113153
@Serializable
114154
@Parcelize
115-
sealed interface Menu: AppRoute {
116-
@Serializable data object MyAccount : Menu
117-
@Serializable data class Deposit(val mint: Mint) : Menu
118-
@Serializable data object BackupKey : Menu
119-
@Serializable data object AppSettings : Menu
120-
@Serializable data object AdvancedFeatures : Menu
121-
@Serializable data object Lab : Menu
155+
sealed interface Menu : AppRoute {
156+
@Serializable
157+
data object MyAccount : Menu
158+
@Serializable
159+
data class Deposit(val mint: Mint) : Menu
160+
@Serializable
161+
data object BackupKey : Menu
162+
@Serializable
163+
data object AppSettings : Menu
164+
@Serializable
165+
data object AdvancedFeatures : Menu
166+
@Serializable
167+
data object Lab : Menu
122168
}
123169

124170
@Serializable
125171
@Parcelize
126-
sealed interface Advanced: AppRoute
172+
sealed interface Advanced : AppRoute
127173
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.flipcash.app.core.ui
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.draw.clip
11+
import androidx.compose.ui.res.painterResource
12+
import androidx.compose.ui.unit.dp
13+
import com.flipcash.core.R
14+
15+
@Composable
16+
fun DeviceFrame(
17+
modifier: Modifier = Modifier,
18+
clipToFrame: Boolean = true,
19+
contentAlignment: Alignment = Alignment.TopCenter,
20+
contents: @Composable () -> Unit,
21+
) {
22+
Box(modifier = modifier, contentAlignment = Alignment.TopCenter) {
23+
Image(
24+
painter = painterResource(id = R.drawable.ic_device_frame),
25+
contentDescription = "",
26+
)
27+
// Inner viewport clipped to frame edge
28+
Box(
29+
modifier = Modifier
30+
.matchParentSize()
31+
.padding(top = 10.dp)
32+
.then(
33+
if (clipToFrame) Modifier.clip(RoundedCornerShape(42.dp)) else Modifier
34+
),
35+
contentAlignment = contentAlignment,
36+
) {
37+
contents()
38+
}
39+
}
40+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.flipcash.app.core.ui
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.IntrinsicSize
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.aspectRatio
11+
import androidx.compose.foundation.layout.fillMaxHeight
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.requiredSize
15+
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.layout.width
17+
import androidx.compose.foundation.layout.wrapContentWidth
18+
import androidx.compose.foundation.shape.CircleShape
19+
import androidx.compose.foundation.shape.RoundedCornerShape
20+
import androidx.compose.material.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.res.painterResource
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.text.font.FontFamily
28+
import androidx.compose.ui.text.font.FontWeight
29+
import androidx.compose.ui.tooling.preview.Preview
30+
import androidx.compose.ui.unit.dp
31+
import androidx.compose.ui.unit.sp
32+
import com.flipcash.app.theme.FlipcashPreview
33+
import com.flipcash.core.R
34+
import com.getcode.theme.BrandAction
35+
import com.getcode.theme.CodeTheme
36+
import com.getcode.theme.White10
37+
import kotlin.time.Clock
38+
39+
@Composable
40+
fun NotificationPreview(
41+
modifier: Modifier = Modifier,
42+
title: String,
43+
body: String,
44+
timestamp: String = "now",
45+
) {
46+
Row(
47+
modifier = modifier
48+
.height(IntrinsicSize.Min)
49+
.wrapContentWidth(unbounded = true)
50+
.background(
51+
color = Color(0xFF2C2C2C),
52+
shape = RoundedCornerShape(CodeTheme.dimens.grid.x2),
53+
)
54+
.padding(horizontal = 12.dp, vertical = 10.dp),
55+
verticalAlignment = Alignment.CenterVertically,
56+
) {
57+
Image(
58+
modifier = Modifier
59+
.fillMaxHeight()
60+
.requiredSize(32.dp)
61+
.aspectRatio(1f),
62+
painter = painterResource(R.drawable.ic_notification_preview_icon),
63+
contentDescription = null,
64+
)
65+
Spacer(modifier = Modifier.width(8.dp))
66+
Column {
67+
Row(verticalAlignment = Alignment.CenterVertically) {
68+
Text(
69+
text = title,
70+
color = Color.White,
71+
fontSize = 12.sp,
72+
fontFamily = FontFamily.Default,
73+
fontWeight = FontWeight.SemiBold
74+
)
75+
Text(
76+
text = " · ",
77+
fontSize = 12.sp,
78+
color = CodeTheme.colors.textSecondary,
79+
fontFamily = FontFamily.Default,
80+
)
81+
Text(
82+
text = timestamp,
83+
fontSize = 11.sp,
84+
color = CodeTheme.colors.textSecondary,
85+
fontFamily = FontFamily.Default,
86+
fontWeight = FontWeight.Normal,
87+
)
88+
}
89+
Text(
90+
text = body,
91+
fontSize = 11.sp,
92+
fontFamily = FontFamily.Default,
93+
fontWeight = FontWeight.Normal,
94+
color = CodeTheme.colors.textMain,
95+
)
96+
}
97+
}
98+
}
99+
100+
@Composable
101+
@Preview
102+
fun Preview_Notification() {
103+
FlipcashPreview {
104+
NotificationPreview(
105+
title = stringResource(R.string.notification_preview_title),
106+
body = stringResource(R.string.notification_preview_body),
107+
)
108+
}
109+
}

0 commit comments

Comments
 (0)