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