diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 2a2d7581d..a922c24db 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -65,8 +65,8 @@
LongMethod:ThreadScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ThreadScreen( state: ThreadContract.UiState, onClose: () -> Unit, onPostClick: (String) -> Unit, onPostReplyClick: (String) -> Unit, onPostQuoteClick: (String) -> Unit, onProfileClick: (String) -> Unit, onHashtagClick: (String) -> Unit, onMediaClick: (String, String) -> Unit, onGoToWallet: () -> Unit, onReplyInNoteEditor: (String, Uri?, String) -> Unit, eventPublisher: (ThreadContract.UiEvent) -> Unit, )
LongMethod:TransactionEditor.kt$@Composable private fun TransactionHeaderColumn( modifier: Modifier, uiMode: UiMode, state: CreateTransactionContract.UiState, keyboardVisible: Boolean, onAmountClick: () -> Unit, )
LongMethod:TransactionEditor.kt$@ExperimentalMaterial3Api @ExperimentalComposeUiApi @Composable fun TransactionEditor( modifier: Modifier, state: CreateTransactionContract.UiState, paddingValues: PaddingValues, eventPublisher: (CreateTransactionContract.UiEvent) -> Unit, onCancelClick: () -> Unit, )
- LongMethod:WalletActivationScreen.kt$@ExperimentalComposeUiApi @Composable private fun WalletActivationDataInput( data: WalletActivationData, working: Boolean, error: Throwable?, onErrorDismiss: () -> Unit, onDataChanged: (WalletActivationData) -> Unit, onActivationCodeRequest: (WalletActivationData) -> Unit, isKeyboardVisible: Boolean, )
LongMethod:WalletActivationScreen.kt$@ExperimentalComposeUiApi @Composable private fun WalletCodeActivationInput( working: Boolean, error: Throwable?, email: String, onCodeChanged: () -> Unit, onCodeConfirmation: (String) -> Unit, isKeyboardVisible: Boolean, )
+ LongMethod:WalletActivationScreen.kt$@Suppress("MagicNumber") @OptIn(ExperimentalMaterial3Api::class) @ExperimentalComposeUiApi @Composable private fun WalletActivationDataInput( data: WalletActivationData, working: Boolean, error: Throwable?, onErrorDismiss: () -> Unit, onDataChanged: (WalletActivationData) -> Unit, onActivationCodeRequest: (WalletActivationData) -> Unit, )
LongMethod:WalletDashboardScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun WalletDashboardScreen( state: WalletDashboardContract.UiState, onPrimaryDestinationChanged: (PrimalTopLevelDestination) -> Unit, onDrawerDestinationClick: (DrawerScreenDestination) -> Unit, onWalletActivateClick: () -> Unit, onProfileClick: (String) -> Unit, onTransactionClick: (String) -> Unit, onSendClick: () -> Unit, onScanClick: () -> Unit, onReceiveClick: () -> Unit, eventPublisher: (UiEvent) -> Unit, )
LongMethod:WalletSettingsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun WalletSettingsScreen( state: WalletSettingsContract.UiState, onClose: () -> Unit, onEditProfileClick: () -> Unit, eventPublisher: (UiEvent) -> Unit, )
LongMethod:WelcomeScreen.kt$@Composable fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit)
@@ -130,8 +130,6 @@
MagicNumber:Timestamps.kt$60
MagicNumber:Timestamps.kt$7
MagicNumber:ValidationUtils.kt$32
- MagicNumber:WalletActivationScreen.kt$0.25f
- MagicNumber:WalletActivationScreen.kt$0.75f
MagicNumber:WalletDashboardScreen.kt$0.42f
MagicNumber:WelcomeScreen.kt$0.4f
MagicNumber:WelcomeScreen.kt$0.6f
diff --git a/app/src/main/kotlin/net/primal/android/auth/welcome/WelcomeScreen.kt b/app/src/main/kotlin/net/primal/android/auth/welcome/WelcomeScreen.kt
index 3eb3fa3e9..5c718e3ed 100644
--- a/app/src/main/kotlin/net/primal/android/auth/welcome/WelcomeScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/auth/welcome/WelcomeScreen.kt
@@ -20,27 +20,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.primal.android.R
-import net.primal.android.core.compose.PrimalClickableText
+import net.primal.android.core.compose.ToSAndPrivacyPolicyText
import net.primal.android.core.compose.button.PrimalCallToActionButton
import net.primal.android.core.compose.fadingBottomEdge
-import net.primal.android.core.ext.openUriSafely
import net.primal.android.theme.AppTheme
import net.primal.android.theme.PrimalTheme
import net.primal.android.theme.domain.PrimalTheme
@Composable
fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit) {
- val localUriHandler = LocalUriHandler.current
-
Surface(
modifier = Modifier
.systemBarsPadding()
@@ -92,7 +85,10 @@ fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
PrimalCallToActionButton(
- modifier = Modifier.widthIn(0.dp, 420.dp).fillMaxWidth().padding(horizontal = 32.dp),
+ modifier = Modifier
+ .widthIn(0.dp, 420.dp)
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp),
title = stringResource(id = R.string.welcome_create_account_button_title),
subtitle = if (maxHeight > MIN_HEIGHT_FOR_SUBTITLE) {
stringResource(id = R.string.welcome_create_account_button_subtitle)
@@ -105,7 +101,10 @@ fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit) {
Spacer(modifier = Modifier.height(16.dp))
PrimalCallToActionButton(
- modifier = Modifier.widthIn(0.dp, 420.dp).fillMaxWidth().padding(horizontal = 32.dp),
+ modifier = Modifier
+ .widthIn(0.dp, 420.dp)
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp),
title = stringResource(id = R.string.welcome_sign_in_button_title),
subtitle = if (maxHeight > MIN_HEIGHT_FOR_SUBTITLE) {
stringResource(id = R.string.welcome_sign_in_button_subtitle)
@@ -117,14 +116,12 @@ fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit) {
Spacer(modifier = Modifier.height(16.dp))
- TermsAndServiceHint(
+ ToSAndPrivacyPolicyText(
modifier = Modifier
.widthIn(0.dp, 360.dp)
.fillMaxWidth()
- .padding(horizontal = 80.dp),
- onTosClick = {
- localUriHandler.openUriSafely(TOS_URL)
- },
+ .padding(horizontal = 32.dp),
+ tosPrefix = stringResource(id = R.string.welcome_tos_prefix),
)
}
}
@@ -134,44 +131,6 @@ fun WelcomeScreen(onSignInClick: () -> Unit, onCreateAccountClick: () -> Unit) {
private const val MIN_HEIGHT_FOR_SUBTITLE = 500
-private const val TOS_ANNOTATION_TAG = "TosAnnotationTag"
-private const val TOS_URL = "https://www.primal.net/terms"
-
-@Composable
-fun TermsAndServiceHint(modifier: Modifier = Modifier, onTosClick: () -> Unit) {
- val tosHint = stringResource(id = R.string.welcome_tos_hint)
- val tosLink = stringResource(id = R.string.welcome_tos_hint_highlighted_word)
- val annotatedString = buildAnnotatedString {
- append(tosHint)
-
- val startIndex = tosHint.indexOf(tosLink)
- if (startIndex >= 0) {
- val endIndex = startIndex + tosLink.length
- addStyle(
- style = SpanStyle(color = AppTheme.colorScheme.primary),
- start = startIndex,
- end = endIndex,
- )
- addStringAnnotation(
- tag = TOS_ANNOTATION_TAG,
- annotation = tosLink,
- start = startIndex,
- end = endIndex,
- )
- }
- }
-
- PrimalClickableText(
- modifier = modifier,
- text = annotatedString,
- style = AppTheme.typography.bodySmall.copy(
- color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
- textAlign = TextAlign.Center,
- ),
- onClick = { _, _ -> onTosClick() },
- )
-}
-
@Preview
@Composable
fun PreviewWelcomeScreen() {
diff --git a/app/src/main/kotlin/net/primal/android/core/compose/DatePickerModalBottomSheet.kt b/app/src/main/kotlin/net/primal/android/core/compose/DatePickerModalBottomSheet.kt
new file mode 100644
index 000000000..1933785b8
--- /dev/null
+++ b/app/src/main/kotlin/net/primal/android/core/compose/DatePickerModalBottomSheet.kt
@@ -0,0 +1,45 @@
+package net.primal.android.core.compose
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DatePicker
+import androidx.compose.material3.DatePickerColors
+import androidx.compose.material3.DatePickerDefaults
+import androidx.compose.material3.DatePickerFormatter
+import androidx.compose.material3.DatePickerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import net.primal.android.theme.AppTheme
+
+@ExperimentalMaterial3Api
+@Composable
+fun DatePickerModalBottomSheet(
+ state: DatePickerState,
+ dateFormatter: DatePickerFormatter = remember { DatePickerFormatter() },
+ dateValidator: (Long) -> Boolean = { true },
+ showModeToggle: Boolean = true,
+ colors: DatePickerColors = DatePickerDefaults.colors(
+ selectedDayContainerColor = AppTheme.colorScheme.primary,
+ ),
+ onDismissRequest: () -> Unit,
+) {
+ ModalBottomSheet(
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ containerColor = AppTheme.colorScheme.surface,
+ tonalElevation = 0.dp,
+ onDismissRequest = onDismissRequest,
+ ) {
+ DatePicker(
+ state = state,
+ modifier = Modifier.padding(bottom = 16.dp),
+ dateFormatter = dateFormatter,
+ dateValidator = dateValidator,
+ showModeToggle = showModeToggle,
+ colors = colors,
+ )
+ }
+}
diff --git a/app/src/main/kotlin/net/primal/android/core/compose/ToS.kt b/app/src/main/kotlin/net/primal/android/core/compose/ToS.kt
new file mode 100644
index 000000000..69a9aa6df
--- /dev/null
+++ b/app/src/main/kotlin/net/primal/android/core/compose/ToS.kt
@@ -0,0 +1,63 @@
+package net.primal.android.core.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import net.primal.android.R
+import net.primal.android.core.ext.openUriSafely
+import net.primal.android.theme.AppTheme
+
+private const val TOS_ANNOTATION_TAG = "TosAnnotationTag"
+private const val PRIVACY_ANNOTATION_TAG = "PrivacyAnnotationTag"
+
+private const val PRIMAL_TOS_URL = "https://www.primal.net/terms"
+private const val PRIMAL_PRIVACY_POLICY_URL = "https://www.primal.net/privacy"
+
+@Composable
+fun ToSAndPrivacyPolicyText(modifier: Modifier = Modifier, tosPrefix: String) {
+ val linkSpanStyle = SpanStyle(color = AppTheme.colorScheme.primary)
+ val annotatedString = buildAnnotatedString {
+ append(tosPrefix)
+ append("\n")
+
+ pushStringAnnotation(TOS_ANNOTATION_TAG, "tos")
+ withStyle(style = linkSpanStyle) {
+ append(stringResource(id = R.string.legal_tos_hint_highlighted_word))
+ }
+ pop()
+
+ append(" and ")
+ pushStringAnnotation(PRIVACY_ANNOTATION_TAG, "privacy")
+ withStyle(style = linkSpanStyle) {
+ append(stringResource(id = R.string.legal_privacy_policy_hint_highlighted_word))
+ }
+ append(".")
+ pop()
+ }
+
+ val localUriHandler = LocalUriHandler.current
+ PrimalClickableText(
+ modifier = modifier,
+ text = annotatedString,
+ style = AppTheme.typography.bodySmall.copy(
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
+ textAlign = TextAlign.Center,
+ ),
+ onClick = { position, offset ->
+ annotatedString.getStringAnnotations(
+ start = position,
+ end = position,
+ ).firstOrNull()?.let { annotation ->
+ when (annotation.tag) {
+ TOS_ANNOTATION_TAG -> localUriHandler.openUriSafely(PRIMAL_TOS_URL)
+ PRIVACY_ANNOTATION_TAG -> localUriHandler.openUriSafely(PRIMAL_PRIVACY_POLICY_URL)
+ }
+ }
+ },
+ )
+}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationData.kt b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationData.kt
index e3443c223..fa78f9442 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationData.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationData.kt
@@ -3,8 +3,10 @@ package net.primal.android.wallet.activation
import net.primal.android.wallet.activation.regions.Region
data class WalletActivationData(
- val name: String = "",
+ val firstName: String = "",
+ val lastName: String = "",
val email: String = "",
+ val dateOfBirth: Long? = null,
val country: Region? = null,
val state: Region? = null,
)
diff --git a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationScreen.kt
index 617a0c33e..72300eb14 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationScreen.kt
@@ -26,10 +26,14 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.DatePickerFormatter
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.OutlinedTextField
@@ -38,7 +42,9 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -64,14 +70,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import java.io.IOException
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream
import net.primal.android.R
import net.primal.android.core.compose.AdjustTemporarilySystemBarColors
+import net.primal.android.core.compose.DatePickerModalBottomSheet
import net.primal.android.core.compose.PrimalDefaults
import net.primal.android.core.compose.PrimalTopAppBar
+import net.primal.android.core.compose.ToSAndPrivacyPolicyText
import net.primal.android.core.compose.button.PrimalLoadingButton
import net.primal.android.core.compose.foundation.keyboardVisibilityAsState
import net.primal.android.core.compose.icons.PrimalIcons
@@ -178,7 +190,6 @@ fun WalletActivationScreen(
data = uiState.data,
working = uiState.working,
error = uiState.error,
- isKeyboardVisible = isKeyboardVisible,
onErrorDismiss = {
eventPublisher(UiEvent.ClearErrorMessage)
onClose()
@@ -215,25 +226,47 @@ private fun StepContainerWithActionButton(
actionButtonText: String,
actionButtonEnabled: Boolean,
actionButtonLoading: Boolean = false,
+ actionButtonVisible: Boolean = true,
+ tosAndPrivacyPolicyVisible: Boolean = false,
containerContent: @Composable ColumnScope.() -> Unit,
) {
Column(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
containerContent()
- PrimalLoadingButton(
- modifier = Modifier.fillMaxWidth(fraction = 0.8f),
- enabled = actionButtonEnabled && !actionButtonLoading,
- loading = actionButtonLoading,
- onClick = onActionClick,
- text = actionButtonText,
- )
+ if (actionButtonVisible) {
+ PrimalLoadingButton(
+ modifier = Modifier.fillMaxWidth(fraction = 0.8f),
+ enabled = actionButtonEnabled && !actionButtonLoading,
+ loading = actionButtonLoading,
+ onClick = onActionClick,
+ text = actionButtonText,
+ )
+
+ if (tosAndPrivacyPolicyVisible) {
+ ToSAndPrivacyPolicyText(
+ modifier = Modifier
+ .widthIn(0.dp, 360.dp)
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp)
+ .padding(top = 8.dp),
+ tosPrefix = stringResource(id = R.string.wallet_tos_prefix),
+ )
+ }
+ }
}
}
+private const val MIN_AGE_FOR_WALLET = 18
+private const val MAX_DATE_OF_BIRTH = 1900
+
+@Suppress("MagicNumber")
+@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalComposeUiApi
@Composable
private fun WalletActivationDataInput(
@@ -243,14 +276,36 @@ private fun WalletActivationDataInput(
onErrorDismiss: () -> Unit,
onDataChanged: (WalletActivationData) -> Unit,
onActivationCodeRequest: (WalletActivationData) -> Unit,
- isKeyboardVisible: Boolean,
) {
- var name by rememberSaveable { mutableStateOf(data.name) }
+ var firstName by rememberSaveable { mutableStateOf(data.firstName) }
+ var lastName by rememberSaveable { mutableStateOf(data.lastName) }
var email by rememberSaveable { mutableStateOf(data.email) }
var country by remember { mutableStateOf(data.country) }
var state by remember { mutableStateOf(data.state) }
+
+ val maxDate = Instant.now().minus(
+ Duration.ofDays(MIN_AGE_FOR_WALLET * 365L) +
+ Duration.ofHours(MIN_AGE_FOR_WALLET / 4 * 24L),
+ )
+ val datePickerState = rememberDatePickerState(
+ initialSelectedDateMillis = data.dateOfBirth,
+ initialDisplayedMonthMillis = data.dateOfBirth ?: maxDate.toEpochMilli(),
+ yearRange = IntRange(MAX_DATE_OF_BIRTH, LocalDate.now().year - MIN_AGE_FOR_WALLET),
+ )
+ var dateOfBirth by rememberSaveable { mutableStateOf(data.dateOfBirth) }
+ LaunchedEffect(datePickerState.selectedDateMillis) {
+ dateOfBirth = datePickerState.selectedDateMillis
+ }
+
val activationDataSnapshot = {
- WalletActivationData(name = name, email = email, country = country, state = state)
+ WalletActivationData(
+ firstName = firstName,
+ lastName = lastName,
+ email = email,
+ dateOfBirth = dateOfBirth,
+ country = country,
+ state = state,
+ )
}
val countries = rememberListOfCountries()
@@ -260,9 +315,9 @@ private fun WalletActivationDataInput(
}
}
- val keyboardController = LocalSoftwareKeyboardController.current
var countrySelectionVisible by remember { mutableStateOf(false) }
var stateSelectionVisible by remember { mutableStateOf(false) }
+ var datePickerVisible by remember { mutableStateOf(false) }
if (countrySelectionVisible) {
RegionSelectionBottomSheet(
@@ -274,7 +329,9 @@ private fun WalletActivationDataInput(
},
onDismissRequest = { countrySelectionVisible = false },
)
- } else if (stateSelectionVisible) {
+ }
+
+ if (stateSelectionVisible) {
RegionSelectionBottomSheet(
regions = countries.find { it.code == country?.code }?.states ?: emptyList(),
title = stringResource(id = R.string.wallet_activation_state_picker_title),
@@ -286,6 +343,15 @@ private fun WalletActivationDataInput(
)
}
+ if (datePickerVisible) {
+ DatePickerModalBottomSheet(
+ state = datePickerState,
+ dateFormatter = remember { DatePickerFormatter() },
+ dateValidator = { it <= maxDate.toEpochMilli() },
+ onDismissRequest = { datePickerVisible = false },
+ )
+ }
+
WalletActivationErrorHandler(
error = error,
fallbackMessage = stringResource(id = R.string.app_generic_error),
@@ -296,6 +362,7 @@ private fun WalletActivationDataInput(
actionButtonText = stringResource(id = R.string.wallet_activation_next_button),
actionButtonEnabled = activationDataSnapshot().isValid(availableStates),
actionButtonLoading = working,
+ tosAndPrivacyPolicyVisible = true,
onActionClick = { onActivationCodeRequest(activationDataSnapshot()) },
) {
Column(
@@ -305,35 +372,54 @@ private fun WalletActivationDataInput(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
- AnimatedVisibility(visible = !isKeyboardVisible) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
Image(
modifier = Modifier.padding(vertical = 16.dp),
imageVector = PrimalIcons.WalletPrimalActivation,
contentDescription = null,
colorFilter = ColorFilter.tint(color = AppTheme.colorScheme.onSurface),
)
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth(fraction = 0.8f)
+ .padding(vertical = 32.dp),
+ text = stringResource(id = R.string.wallet_activation_pending_data_hint),
+ textAlign = TextAlign.Center,
+ color = AppTheme.colorScheme.onSurface,
+ style = AppTheme.typography.bodyLarge.copy(
+ fontWeight = FontWeight.SemiBold,
+ ),
+ )
}
- Text(
- modifier = Modifier
- .fillMaxWidth(fraction = 0.8f)
- .padding(vertical = 32.dp),
- text = stringResource(id = R.string.wallet_activation_pending_data_hint),
- textAlign = TextAlign.Center,
- color = AppTheme.colorScheme.onSurface,
- style = AppTheme.typography.bodyLarge.copy(
- fontWeight = FontWeight.SemiBold,
+ WalletOutlinedTextField(
+ modifier = Modifier.fillMaxWidth(fraction = 0.8f),
+ value = firstName,
+ onValueChange = {
+ firstName = it
+ onDataChanged(activationDataSnapshot())
+ },
+ placeholderText = stringResource(id = R.string.wallet_activation_first_name),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next,
),
)
+ Spacer(modifier = Modifier.height(16.dp))
+
WalletOutlinedTextField(
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
- value = name,
+ value = lastName,
onValueChange = {
- name = it
+ lastName = it
onDataChanged(activationDataSnapshot())
},
- placeholderText = stringResource(id = R.string.wallet_activation_your_name),
+ placeholderText = stringResource(id = R.string.wallet_activation_last_name),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next,
@@ -349,23 +435,26 @@ private fun WalletActivationDataInput(
email = it.trim()
onDataChanged(activationDataSnapshot())
},
- placeholderText = stringResource(id = R.string.wallet_activation_your_email_address),
+ placeholderText = stringResource(id = R.string.wallet_activation_email_address),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
- imeAction = if (activationDataSnapshot().isValid(availableStates)) ImeAction.Go else ImeAction.None,
- ),
- keyboardActions = KeyboardActions(
- onGo = {
- if (activationDataSnapshot().isValid(availableStates)) {
- keyboardController?.hide()
- onActivationCodeRequest(activationDataSnapshot())
- }
- },
+ imeAction = ImeAction.Done,
),
)
Spacer(modifier = Modifier.height(16.dp))
+ WalletOutlinedTextField(
+ modifier = Modifier.fillMaxWidth(fraction = 0.8f),
+ value = dateOfBirth.toDateFormat(),
+ onClick = { datePickerVisible = true },
+ onValueChange = { },
+ readOnly = true,
+ placeholderText = stringResource(id = R.string.wallet_activation_date_of_birth),
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
Row(
modifier = Modifier
.fillMaxWidth(fraction = 0.8f)
@@ -377,7 +466,7 @@ private fun WalletActivationDataInput(
value = country?.name ?: "",
onValueChange = {},
readOnly = true,
- placeholderText = stringResource(id = R.string.wallet_activation_your_country_of_residence),
+ placeholderText = stringResource(id = R.string.wallet_activation_country_of_residence),
)
if (!availableStates.isNullOrEmpty()) {
@@ -399,6 +488,13 @@ private fun WalletActivationDataInput(
}
}
+private fun Long?.toDateFormat(): String {
+ if (this == null) return ""
+
+ return LocalDate.ofEpochDay(this / Duration.ofDays(1).toMillis())
+ .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+}
+
@OptIn(ExperimentalSerializationApi::class)
@Composable
private fun rememberListOfCountries(): List {
@@ -413,8 +509,8 @@ private fun rememberListOfCountries(): List {
}
private fun WalletActivationData.isValid(availableStates: List?): Boolean {
- return name.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
- country != null && (availableStates.isNullOrEmpty() || state != null)
+ return firstName.isNotBlank() && lastName.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
+ dateOfBirth != null && country != null && (availableStates.isNullOrEmpty() || state != null)
}
@ExperimentalComposeUiApi
@@ -700,10 +796,9 @@ private fun PreviewWalletActivationDataInput() {
PrimalTheme(primalTheme = net.primal.android.theme.domain.PrimalTheme.Sunset) {
Surface {
WalletActivationDataInput(
- data = WalletActivationData(name = "alex", email = "alex@primal.net"),
+ data = WalletActivationData(firstName = "alex", email = "alex@primal.net"),
working = false,
error = null,
- isKeyboardVisible = false,
onErrorDismiss = { },
onDataChanged = { },
onActivationCodeRequest = { },
diff --git a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationViewModel.kt
index f9d8393ba..962ffb3d6 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationViewModel.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/activation/WalletActivationViewModel.kt
@@ -3,6 +3,9 @@ package net.primal.android.wallet.activation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import java.time.Duration
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,6 +21,8 @@ import net.primal.android.user.domain.WalletPreference
import net.primal.android.user.repository.UserRepository
import net.primal.android.wallet.activation.WalletActivationContract.UiEvent
import net.primal.android.wallet.activation.WalletActivationContract.UiState
+import net.primal.android.wallet.api.model.GetActivationCodeRequestBody
+import net.primal.android.wallet.api.model.WalletActivationDetails
import net.primal.android.wallet.repository.WalletRepository
import timber.log.Timber
@@ -58,12 +63,20 @@ class WalletActivationViewModel @Inject constructor(
setState { copy(working = true) }
try {
val userId = activeAccountStore.activeUserId()
+ checkNotNull(data.dateOfBirth)
+ checkNotNull(data.country)
walletRepository.requestActivationCodeToEmail(
userId = userId,
- name = data.name,
- email = data.email,
- country = data.country?.code,
- state = data.state?.code,
+ body = GetActivationCodeRequestBody(
+ userDetails = WalletActivationDetails(
+ firstName = data.firstName,
+ lastName = data.lastName,
+ email = data.email,
+ dateOfBirth = data.dateOfBirth.formatDateOfBirth(),
+ country = data.country.code,
+ state = data.state?.code ?: "",
+ ),
+ ),
)
setState { copy(status = WalletActivationStatus.PendingCodeConfirmation) }
} catch (error: WssException) {
@@ -74,6 +87,11 @@ class WalletActivationViewModel @Inject constructor(
}
}
+ private fun Long.formatDateOfBirth(): String {
+ return LocalDate.ofEpochDay(this / Duration.ofDays(1).toMillis())
+ .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+ }
+
private fun onActivateWallet(code: String) =
viewModelScope.launch {
setState { copy(working = true) }
diff --git a/app/src/main/kotlin/net/primal/android/wallet/api/WalletApi.kt b/app/src/main/kotlin/net/primal/android/wallet/api/WalletApi.kt
index c5c0ac6c5..32eac1165 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/api/WalletApi.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/api/WalletApi.kt
@@ -2,6 +2,7 @@ package net.primal.android.wallet.api
import net.primal.android.wallet.api.model.BalanceResponse
import net.primal.android.wallet.api.model.DepositRequestBody
+import net.primal.android.wallet.api.model.GetActivationCodeRequestBody
import net.primal.android.wallet.api.model.InAppPurchaseQuoteResponse
import net.primal.android.wallet.api.model.LightningInvoiceResponse
import net.primal.android.wallet.api.model.MiningFeeTier
@@ -19,13 +20,7 @@ interface WalletApi {
suspend fun getWalletUserInfo(userId: String): WalletUserInfoResponse
- suspend fun requestActivationCodeToEmail(
- userId: String,
- name: String,
- email: String,
- country: String?,
- state: String?,
- )
+ suspend fun requestActivationCodeToEmail(userId: String, body: GetActivationCodeRequestBody)
suspend fun activateWallet(userId: String, code: String): String
diff --git a/app/src/main/kotlin/net/primal/android/wallet/api/WalletApiImpl.kt b/app/src/main/kotlin/net/primal/android/wallet/api/WalletApiImpl.kt
index 02ea671c3..0c948ff4d 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/api/WalletApiImpl.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/api/WalletApiImpl.kt
@@ -90,20 +90,14 @@ class WalletApiImpl @Inject constructor(
.toUserWalletInfoResponseOrThrow()
}
- override suspend fun requestActivationCodeToEmail(
- userId: String,
- name: String,
- email: String,
- country: String?,
- state: String?,
- ) {
+ override suspend fun requestActivationCodeToEmail(userId: String, body: GetActivationCodeRequestBody) {
primalApiClient.query(
message = PrimalCacheFilter(
primalVerb = PrimalVerb.WALLET,
optionsJson = buildWalletOptionsJson(
userId = userId,
walletVerb = WalletOperationVerb.GET_ACTIVATION_CODE,
- requestBody = GetActivationCodeRequestBody(name, email, country, state),
+ requestBody = body,
),
),
)
diff --git a/app/src/main/kotlin/net/primal/android/wallet/api/model/GetActivationCodeRequestBody.kt b/app/src/main/kotlin/net/primal/android/wallet/api/model/GetActivationCodeRequestBody.kt
index 359d88029..6f603a20e 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/api/model/GetActivationCodeRequestBody.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/api/model/GetActivationCodeRequestBody.kt
@@ -1,11 +1,9 @@
package net.primal.android.wallet.api.model
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetActivationCodeRequestBody(
- val name: String,
- val email: String,
- val country: String?,
- val state: String?,
+ @SerialName("user_details") val userDetails: WalletActivationDetails,
) : WalletOperationRequestBody()
diff --git a/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletActivationDetails.kt b/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletActivationDetails.kt
new file mode 100644
index 000000000..e9d719bce
--- /dev/null
+++ b/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletActivationDetails.kt
@@ -0,0 +1,14 @@
+package net.primal.android.wallet.api.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class WalletActivationDetails(
+ @SerialName("first_name") val firstName: String,
+ @SerialName("last_name") val lastName: String,
+ @SerialName("email") val email: String,
+ @SerialName("date_of_birth") val dateOfBirth: String,
+ @SerialName("country") val country: String,
+ @SerialName("state") val state: String,
+)
diff --git a/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletOperationVerb.kt b/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletOperationVerb.kt
index fd9eb4bc8..55f40d230 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletOperationVerb.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/api/model/WalletOperationVerb.kt
@@ -10,7 +10,7 @@ enum class WalletOperationVerb(val identifier: String) {
USER_INFO("user_info"),
IN_APP_PURCHASE_QUOTE("in_app_purchase_quote"),
IN_APP_PURCHASE("in_app_purchase"),
- GET_ACTIVATION_CODE("get_activation_code"),
+ GET_ACTIVATION_CODE("get_activation_code_2"),
ACTIVATE("activate"),
PARSE_LNURL("parse_lnurl"),
PARSE_LNINVOICE("parse_lninvoice"),
diff --git a/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt b/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
index bb19c5ca3..375b41d5b 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
@@ -15,6 +15,7 @@ import net.primal.android.user.repository.UserRepository
import net.primal.android.wallet.api.WalletApi
import net.primal.android.wallet.api.mediator.WalletTransactionsMediator
import net.primal.android.wallet.api.model.DepositRequestBody
+import net.primal.android.wallet.api.model.GetActivationCodeRequestBody
import net.primal.android.wallet.api.model.InAppPurchaseQuoteResponse
import net.primal.android.wallet.api.model.LightningInvoiceResponse
import net.primal.android.wallet.api.model.MiningFeeTier
@@ -54,15 +55,9 @@ class WalletRepository @Inject constructor(
return walletApi.activateWallet(userId, code)
}
- suspend fun requestActivationCodeToEmail(
- userId: String,
- name: String,
- email: String,
- country: String?,
- state: String?,
- ) {
+ suspend fun requestActivationCodeToEmail(userId: String, body: GetActivationCodeRequestBody) {
withContext(dispatcherProvider.io()) {
- walletApi.requestActivationCodeToEmail(userId, name, email, country, state)
+ walletApi.requestActivationCodeToEmail(userId, body)
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1d81133d6..bfb67a5e6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,8 +21,10 @@
Already have a Nostr account? Sign with your Nostr key.
Create account
Your new Nostr account will be up and running in a minute.
- By proceeding you confirm that you accept our terms of service
- terms of service
+ By proceeding you accept our
+
+ Terms of Service
+ Privacy Policy
New Account
Profile Preview
@@ -359,6 +361,7 @@
You are running low on sats
Buy Sats Now
+ By tapping \"Next\" you agree to the
Activate Wallet
Activating your wallet is easy!\nWe just need a few details below:
Check Your Email
@@ -366,10 +369,12 @@
Incorrect code. Please try again.
Success
Your wallet has been activated. Your new Nostr lightning address is:
- Your name
- Your email address
+ First name
+ Last name
+ Email address
+ Date of birth
State
- Country of residence
+ Country of residence
Activation code
Next
Finish