Skip to content

Commit 52aabbf

Browse files
authored
Merge pull request #118 from synonymdev/feat/pin-for-pay
feat: PIN for payments
2 parents 05f4e74 + 0dee2da commit 52aabbf

File tree

10 files changed

+446
-89
lines changed

10 files changed

+446
-89
lines changed

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ class SettingsStore @Inject constructor(
8080
val isPinOnIdleEnabled: Flow<Boolean> = store.data.map { it[IS_PIN_ON_IDLE_ENABLED] == true }
8181
suspend fun setIsPinOnIdleEnabled(value: Boolean) { store.edit { it[IS_PIN_ON_IDLE_ENABLED] = value } }
8282

83+
val isPinForPaymentsEnabled: Flow<Boolean> = store.data.map { it[IS_PIN_FOR_PAYMENTS_ENABLED] == true }
84+
suspend fun setIsPinForPaymentsEnabled(value: Boolean) { store.edit { it[IS_PIN_FOR_PAYMENTS_ENABLED] = value } }
85+
8386
suspend fun wipe() {
8487
store.edit { it.clear() }
8588
Logger.info("Deleted all user settings data.")
@@ -97,5 +100,6 @@ class SettingsStore @Inject constructor(
97100
private val IS_PIN_ON_LAUNCH_ENABLED = booleanPreferencesKey("is_pin_on_launch_enabled")
98101
private val IS_BIOMETRIC_ENABLED = booleanPreferencesKey("is_biometric_enabled")
99102
private val IS_PIN_ON_IDLE_ENABLED = booleanPreferencesKey("is_pin_on_idle_enabled")
103+
private val IS_PIN_FOR_PAYMENTS_ENABLED = booleanPreferencesKey("is_pin_for_payments_enabled")
100104
}
101105
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ fun AuthCheckScreen(
1717
val isPinOnLaunchEnabled by app.isPinOnLaunchEnabled.collectAsStateWithLifecycle()
1818
val isBiometricEnabled by app.isBiometricEnabled.collectAsStateWithLifecycle()
1919
val isPinOnIdleEnabled by app.isPinOnIdleEnabled.collectAsStateWithLifecycle()
20+
val isPinForPaymentsEnabled by app.isPinForPaymentsEnabled.collectAsStateWithLifecycle()
2021

2122
AuthCheckView(
2223
showLogoOnPin = route.showLogoOnPin,
@@ -37,6 +38,10 @@ fun AuthCheckScreen(
3738
app.setIsPinOnIdleEnabled(!isPinOnIdleEnabled)
3839
}
3940

41+
AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS -> {
42+
app.setIsPinForPaymentsEnabled(!isPinForPaymentsEnabled)
43+
}
44+
4045
AuthCheckAction.DISABLE_PIN -> {
4146
app.removePin()
4247
}
@@ -52,5 +57,6 @@ object AuthCheckAction {
5257
const val TOGGLE_PIN_ON_LAUNCH = "TOGGLE_PIN_ON_LAUNCH"
5358
const val TOGGLE_BIOMETRICS = "TOGGLE_BIOMETRICS"
5459
const val TOGGLE_PIN_ON_IDLE = "TOGGLE_PIN_ON_IDLE"
60+
const val TOGGLE_PIN_FOR_PAYMENTS = "TOGGLE_PIN_FOR_PAYMENTS"
5561
const val DISABLE_PIN = "DISABLE_PIN"
5662
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ private fun PinPad(
163163
)
164164
} else {
165165
BodyS(
166-
text = stringResource(R.string.security__pin_attempts).replace("{attemptsRemaining}", "$attemptsRemaining"),
166+
text = stringResource(R.string.security__pin_attempts)
167+
.replace("{attemptsRemaining}", "$attemptsRemaining"),
167168
color = Colors.Brand,
168169
textAlign = TextAlign.Center,
169170
modifier = Modifier.clickableAlpha { onClickForgotPin() }
@@ -175,7 +176,8 @@ private fun PinPad(
175176
if (allowBiometrics) {
176177
val biometricsName = stringResource(R.string.security__bio)
177178
PrimaryButton(
178-
text = stringResource(R.string.security__pin_use_biometrics).replace("{biometricsName}", biometricsName),
179+
text = stringResource(R.string.security__pin_use_biometrics)
180+
.replace("{biometricsName}", biometricsName),
179181
onClick = onShowBiometrics,
180182
fullWidth = false,
181183
size = ButtonSize.Small,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package to.bitkit.ui.screens.wallets.send
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.navigationBarsPadding
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.LaunchedEffect
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.mutableStateOf
15+
import androidx.compose.runtime.remember
16+
import androidx.compose.runtime.setValue
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.text.style.TextAlign
21+
import androidx.compose.ui.tooling.preview.Preview
22+
import androidx.compose.ui.unit.dp
23+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
24+
import to.bitkit.R
25+
import to.bitkit.env.Env
26+
import to.bitkit.ui.appViewModel
27+
import to.bitkit.ui.components.BodyM
28+
import to.bitkit.ui.components.BodyS
29+
import to.bitkit.ui.components.KEY_DELETE
30+
import to.bitkit.ui.components.PinDots
31+
import to.bitkit.ui.components.PinNumberPad
32+
import to.bitkit.ui.scaffold.SheetTopBar
33+
import to.bitkit.ui.shared.util.clickableAlpha
34+
import to.bitkit.ui.shared.util.gradientBackground
35+
import to.bitkit.ui.theme.AppThemeSurface
36+
import to.bitkit.ui.theme.Colors
37+
38+
const val PIN_CHECK_RESULT_KEY = "PIN_CHECK_RESULT_KEY"
39+
40+
@Composable
41+
fun PinCheckScreen(
42+
onBack: () -> Unit,
43+
onSuccess: () -> Unit,
44+
) {
45+
val app = appViewModel ?: return
46+
val attemptsRemaining by app.pinAttemptsRemaining.collectAsStateWithLifecycle()
47+
var pin by remember { mutableStateOf("") }
48+
49+
LaunchedEffect(pin) {
50+
if (pin.length == Env.PIN_LENGTH) {
51+
if (app.validatePin(pin)) {
52+
onSuccess()
53+
}
54+
pin = ""
55+
}
56+
}
57+
58+
PinCheckContent(
59+
pin = pin,
60+
attemptsRemaining = attemptsRemaining,
61+
onKeyPress = { key ->
62+
if (key == KEY_DELETE) {
63+
if (pin.isNotEmpty()) {
64+
pin = pin.dropLast(1)
65+
}
66+
} else if (pin.length < Env.PIN_LENGTH) {
67+
pin += key
68+
}
69+
},
70+
onBack = onBack,
71+
onClickForgotPin = { app.setShowForgotPin(true) },
72+
)
73+
}
74+
75+
@Composable
76+
private fun PinCheckContent(
77+
pin: String,
78+
attemptsRemaining: Int,
79+
onKeyPress: (String) -> Unit,
80+
onBack: () -> Unit,
81+
onClickForgotPin: () -> Unit,
82+
) {
83+
val isLastAttempt = attemptsRemaining == 1
84+
85+
Column(
86+
modifier = Modifier
87+
.fillMaxWidth()
88+
.gradientBackground()
89+
.navigationBarsPadding()
90+
) {
91+
SheetTopBar(
92+
titleText = stringResource(R.string.security__pin_send_title),
93+
onBack = onBack,
94+
)
95+
Spacer(Modifier.height(32.dp))
96+
97+
Column(
98+
horizontalAlignment = Alignment.CenterHorizontally,
99+
) {
100+
BodyM(
101+
text = stringResource(R.string.security__pin_send),
102+
color = Colors.White64,
103+
modifier = Modifier.padding(horizontal = 32.dp)
104+
)
105+
106+
Spacer(modifier = Modifier.height(32.dp))
107+
108+
AnimatedVisibility(visible = attemptsRemaining < Env.PIN_ATTEMPTS) {
109+
if (isLastAttempt) {
110+
BodyS(
111+
text = stringResource(R.string.security__pin_last_attempt),
112+
color = Colors.Brand,
113+
textAlign = TextAlign.Center,
114+
modifier = Modifier.padding(horizontal = 32.dp)
115+
)
116+
} else {
117+
BodyS(
118+
text = stringResource(R.string.security__pin_attempts)
119+
.replace("{attemptsRemaining}", "$attemptsRemaining"),
120+
color = Colors.Brand,
121+
textAlign = TextAlign.Center,
122+
modifier = Modifier
123+
.padding(horizontal = 32.dp)
124+
.clickableAlpha { onClickForgotPin() }
125+
)
126+
}
127+
Spacer(modifier = Modifier.height(16.dp))
128+
}
129+
130+
PinDots(
131+
pin = pin,
132+
modifier = Modifier.padding(vertical = 16.dp),
133+
)
134+
135+
Spacer(modifier = Modifier.weight(1f))
136+
137+
PinNumberPad(
138+
onPress = onKeyPress,
139+
modifier = Modifier
140+
.height(350.dp)
141+
.background(Colors.Black)
142+
)
143+
}
144+
}
145+
}
146+
147+
@Preview
148+
@Composable
149+
private fun Preview() {
150+
AppThemeSurface {
151+
PinCheckContent(
152+
pin = "123",
153+
attemptsRemaining = 8,
154+
onKeyPress = {},
155+
onBack = {},
156+
onClickForgotPin = {},
157+
)
158+
}
159+
}
160+
161+
@Preview
162+
@Composable
163+
private fun PreviewAttempts() {
164+
AppThemeSurface {
165+
PinCheckContent(
166+
pin = "123",
167+
attemptsRemaining = 3,
168+
onKeyPress = {},
169+
onBack = {},
170+
onClickForgotPin = {},
171+
)
172+
}
173+
}
174+
175+
@Preview
176+
@Composable
177+
private fun PreviewAttemptsLast() {
178+
AppThemeSurface {
179+
PinCheckContent(
180+
pin = "123",
181+
attemptsRemaining = 1,
182+
onKeyPress = {},
183+
onBack = {},
184+
onClickForgotPin = {},
185+
)
186+
}
187+
}

0 commit comments

Comments
 (0)