-
Notifications
You must be signed in to change notification settings - Fork 111
feat: add verify-mobile reminder and gate wallet buy flow #6079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds a mobile verification reminder feature and gates the wallet buy flow to ensure users have verified their mobile number. The feature prompts users to verify their mobile number if it hasn't been verified in the last 60 days or if there's no verification record.
Changes:
- Added mobile verification reminder dialog with localized strings and background drawable
- Gated wallet buy flow in both Privacy and Classic wallet fragments to check mobile verification status
- Added verification flow integration through LandingActivity with new FROM_VERIFY_MOBILE_REMINDER constant
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/res/values/strings.xml | Added English strings for verify mobile reminder dialog |
| app/src/main/res/values-zh-rCN/strings.xml | Added Chinese translations for verify mobile reminder dialog |
| app/src/main/res/drawable/bg_reminder_verify_mobile.xml | Added vector drawable background for verify mobile reminder |
| app/src/main/java/one/mixin/android/ui/wallet/PrivacyWalletFragment.kt | Added mobile verification check before wallet buy flow |
| app/src/main/java/one/mixin/android/ui/wallet/ClassicWalletFragment.kt | Added mobile verification check before wallet buy flow |
| app/src/main/java/one/mixin/android/ui/landing/MobileFragment.kt | Added FROM_VERIFY_MOBILE_REMINDER constant and verification purpose handling |
| app/src/main/java/one/mixin/android/ui/landing/LandingActivity.kt | Added showVerifyMobile and showChangePhone methods for navigation |
| app/src/main/java/one/mixin/android/ui/home/reminder/verify_mobile_reminder_bottom_sheet_dialog_fragment.kt | New dialog fragment for showing mobile verification reminder |
| app/src/main/java/one/mixin/android/ui/home/ConversationListFragment.kt | Added logic to show verify mobile reminder with priority over other reminders |
| app/src/main/java/one/mixin/android/ui/common/PinCodeFragment.kt | Added logic to return to MainActivity after mobile verification |
| app/src/main/java/one/mixin/android/api/request/VerificationRequest.kt | Added NONE verification purpose enum value |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import one.mixin.android.ui.search.SearchMessageFragment | ||
| import one.mixin.android.ui.search.SearchSingleFragment | ||
| import one.mixin.android.ui.wallet.AllTransactionsFragment | ||
| import one.mixin.android.ui.wallet.WalletActivity |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The imports for SearchMessageFragment, SearchSingleFragment, AllTransactionsFragment, and WalletActivity are added but not used anywhere in this file. These should be removed to keep the imports clean.
| import one.mixin.android.ui.search.SearchMessageFragment | |
| import one.mixin.android.ui.search.SearchSingleFragment | |
| import one.mixin.android.ui.wallet.AllTransactionsFragment | |
| import one.mixin.android.ui.wallet.WalletActivity |
| import one.mixin.android.extension.tickVibrate | ||
| import one.mixin.android.session.Session | ||
| import one.mixin.android.session.decryptPinToken | ||
| import one.mixin.android.ui.home.MainActivity |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import for MainActivity is added but not used in this file. The class is only referenced in a variable name. This import should be removed.
| import one.mixin.android.ui.home.MainActivity |
| package one.mixin.android.ui.home.reminder | ||
|
|
||
| import android.annotation.SuppressLint | ||
| import android.app.Dialog | ||
| import android.content.Context | ||
| import android.view.Gravity | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.compose.runtime.Composable | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import one.mixin.android.Constants | ||
| import one.mixin.android.R | ||
| import one.mixin.android.compose.theme.MixinAppTheme | ||
| import one.mixin.android.extension.booleanFromAttribute | ||
| import one.mixin.android.extension.defaultSharedPreferences | ||
| import one.mixin.android.extension.getSafeAreaInsetsTop | ||
| import one.mixin.android.extension.isNightMode | ||
| import one.mixin.android.extension.putLong | ||
| import one.mixin.android.extension.screenHeight | ||
| import one.mixin.android.session.Session | ||
| import one.mixin.android.ui.common.MixinComposeBottomSheetDialogFragment | ||
| import one.mixin.android.ui.landing.LandingActivity | ||
| import one.mixin.android.util.SystemUIManager | ||
| import java.time.Instant | ||
|
|
||
| @AndroidEntryPoint | ||
| class VerifyMobileReminderBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment() { |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filename uses snake_case (verify_mobile_reminder_bottom_sheet_dialog_fragment.kt) while the class name uses PascalCase (VerifyMobileReminderBottomSheetDialogFragment). Kotlin convention is to use PascalCase for filenames that match the class name. The file should be renamed to VerifyMobileReminderBottomSheetDialogFragment.kt to follow standard Kotlin naming conventions.
| lifecycleScope.launch { | ||
| if (Session.isAnonymous() && !Session.hasPhone()) { | ||
| navTo(AddPhoneBeforeFragment.newInstance(), AddPhoneBeforeFragment.TAG) | ||
| return@launch | ||
| } | ||
| val phoneVerifiedAt: String? = Session.getAccount()?.phoneVerifiedAt | ||
| val shouldVerifyMobile: Boolean = phoneVerifiedAt.isNullOrBlank() || runCatching { | ||
| val verifiedAtMillis: Long = Instant.parse(phoneVerifiedAt).toEpochMilli() | ||
| val sixtyDaysMillis: Long = 60L * 24L * 60L * 60L * 1000L | ||
| System.currentTimeMillis() - verifiedAtMillis > sixtyDaysMillis | ||
| }.getOrDefault(true) | ||
| if (shouldVerifyMobile) { | ||
| LandingActivity.showVerifyMobile(requireContext()) | ||
| return@launch | ||
| } | ||
| WalletActivity.showBuy(requireActivity(), false, null, null) | ||
| defaultSharedPreferences.putBoolean(PREF_HAS_USED_BUY, false) | ||
| RxBus.publish(BadgeEvent(PREF_HAS_USED_BUY)) | ||
| sendReceiveView.buyBadge.isVisible = false | ||
| } |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mobile verification check logic is duplicated in ClassicWalletFragment. Consider extracting this logic into a shared utility function or extension method to improve maintainability and reduce code duplication. This would make it easier to update the verification logic in one place if requirements change.
| companion object { | ||
| const val TAG: String = "VerifyMobileReminderBottomSheetDialogFragment" | ||
| private const val PREF_VERIFY_MOBILE_REMINDER_SNOOZE: String = "pref_verify_mobile_reminder_snooze" | ||
| private const val VERIFY_MOBILE_INTERVAL_MILLIS: Long = 60L * 24L * 60L * 60L * 1000L |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constant VERIFY_MOBILE_INTERVAL_MILLIS is defined as 60 days but the calculation is incorrect. The formula used is 60L * 24L * 60L * 60L * 1000L which equals 60 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds = 60 days. However, this should be 60 days * 24 hours * 3600 seconds * 1000 milliseconds OR 60L * 24L * 60L * 60L * 1000L. The current calculation appears correct but the naming suggests it should be clearer. The same calculation is duplicated in PrivacyWalletFragment (line 143) and ClassicWalletFragment (line 179). This magic number should be extracted to a shared constant.
| val phoneVerifiedAt: String? = Session.getAccount()?.phoneVerifiedAt | ||
| val shouldVerifyMobile: Boolean = phoneVerifiedAt.isNullOrBlank() || runCatching { | ||
| val verifiedAtMillis: Long = Instant.parse(phoneVerifiedAt).toEpochMilli() | ||
| val sixtyDaysMillis: Long = 60L * 24L * 60L * 60L * 1000L |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number for the 60-day interval (60L * 24L * 60L * 60L * 1000L) is duplicated here and in verify_mobile_reminder_bottom_sheet_dialog_fragment.kt. This should be extracted to a shared constant to ensure consistency and make maintenance easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val phoneVerifiedAt: String? = Session.getAccount()?.phoneVerifiedAt | ||
| val shouldVerifyMobile: Boolean = phoneVerifiedAt.isNullOrBlank() || runCatching { | ||
| val verifiedAtMillis: Long = Instant.parse(phoneVerifiedAt).toEpochMilli() | ||
| System.currentTimeMillis() - verifiedAtMillis > Constants.INTERVAL_60_DAYS | ||
| }.getOrDefault(true) |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phone verification logic is duplicated here, identical to the logic in PrivacyWalletFragment. Consider extracting this verification check into a shared utility function or extension function on Session to improve maintainability and reduce code duplication. The logic checks if the phone is verified and if it was verified more than 60 days ago.
| fun shouldShow(context: Context): Boolean { | ||
| if (!Session.hasPhone()) return false | ||
| val account = Session.getAccount() ?: return false | ||
| val lastSnoozeTimeMillis: Long = context.defaultSharedPreferences.getLong(PREF_VERIFY_MOBILE_REMINDER_SNOOZE, 0) | ||
| if (System.currentTimeMillis() - lastSnoozeTimeMillis < Constants.INTERVAL_7_DAYS) return false | ||
| val phoneVerifiedAt: String? = account.phoneVerifiedAt | ||
| if (phoneVerifiedAt.isNullOrBlank()) return true | ||
| val verifiedAtMillis: Long = runCatching { | ||
| Instant.parse(phoneVerifiedAt).toEpochMilli() | ||
| }.getOrNull() ?: return true | ||
| return System.currentTimeMillis() - verifiedAtMillis > Constants.INTERVAL_60_DAYS | ||
| } |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phone verification date check logic is duplicated in this file (lines 40-43) and in PrivacyWalletFragment (lines 141-144) and ClassicWalletFragment (lines 177-180). This same logic for checking if the phone was verified more than 60 days ago appears in multiple places. Consider extracting this into a shared utility function or extension function on Session to avoid code duplication and improve maintainability.
| fun shouldShow(context: Context): Boolean { | |
| if (!Session.hasPhone()) return false | |
| val account = Session.getAccount() ?: return false | |
| val lastSnoozeTimeMillis: Long = context.defaultSharedPreferences.getLong(PREF_VERIFY_MOBILE_REMINDER_SNOOZE, 0) | |
| if (System.currentTimeMillis() - lastSnoozeTimeMillis < Constants.INTERVAL_7_DAYS) return false | |
| val phoneVerifiedAt: String? = account.phoneVerifiedAt | |
| if (phoneVerifiedAt.isNullOrBlank()) return true | |
| val verifiedAtMillis: Long = runCatching { | |
| Instant.parse(phoneVerifiedAt).toEpochMilli() | |
| }.getOrNull() ?: return true | |
| return System.currentTimeMillis() - verifiedAtMillis > Constants.INTERVAL_60_DAYS | |
| } | |
| private fun isPhoneVerifiedMoreThanSixtyDaysAgo(phoneVerifiedAt: String?): Boolean { | |
| if (phoneVerifiedAt.isNullOrBlank()) return true | |
| val verifiedAtMillis: Long = runCatching { | |
| Instant.parse(phoneVerifiedAt).toEpochMilli() | |
| }.getOrNull() ?: return true | |
| return System.currentTimeMillis() - verifiedAtMillis > Constants.INTERVAL_60_DAYS | |
| } | |
| fun shouldShow(context: Context): Boolean { | |
| if (!Session.hasPhone()) return false | |
| val account = Session.getAccount() ?: return false | |
| val lastSnoozeTimeMillis: Long = context.defaultSharedPreferences.getLong(PREF_VERIFY_MOBILE_REMINDER_SNOOZE, 0) | |
| if (System.currentTimeMillis() - lastSnoozeTimeMillis < Constants.INTERVAL_7_DAYS) return false | |
| return isPhoneVerifiedMoreThanSixtyDaysAgo(account.phoneVerifiedAt) | |
| } |
| fun showChangePhone(context: Context) { | ||
| val intent = | ||
| Intent(context, LandingActivity::class.java).apply { | ||
| putExtra(ARGS_FROM, FROM_CHANGE_PHONE_ACCOUNT) | ||
| } | ||
| context.startActivity(intent) | ||
| } | ||
|
|
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The showChangePhone function is added but does not appear to be used anywhere in the codebase based on the changes in this PR. If this function is intended for future use, consider adding a comment explaining its purpose. If it's not needed, it should be removed to avoid maintaining unused code.
| fun showChangePhone(context: Context) { | |
| val intent = | |
| Intent(context, LandingActivity::class.java).apply { | |
| putExtra(ARGS_FROM, FROM_CHANGE_PHONE_ACCOUNT) | |
| } | |
| context.startActivity(intent) | |
| } |
| @@ -0,0 +1,104 @@ | |||
| package one.mixin.android.ui.home.reminder | |||
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file name uses snake_case (verify_mobile_reminder_bottom_sheet_dialog_fragment.kt) instead of PascalCase which is inconsistent with Kotlin naming conventions. The file should be named VerifyMobileReminderBottomSheetDialogFragment.kt to match the class name inside and follow standard Kotlin file naming conventions.
| val phoneVerifiedAt: String? = Session.getAccount()?.phoneVerifiedAt | ||
| val shouldVerifyMobile: Boolean = phoneVerifiedAt.isNullOrBlank() || runCatching { | ||
| val verifiedAtMillis: Long = Instant.parse(phoneVerifiedAt).toEpochMilli() | ||
| System.currentTimeMillis() - verifiedAtMillis > Constants.INTERVAL_60_DAYS | ||
| }.getOrDefault(true) |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phone verification logic is duplicated between PrivacyWalletFragment and ClassicWalletFragment. Consider extracting this verification check into a shared utility function or extension function on Session to improve maintainability and reduce code duplication. The logic checks if the phone is verified and if it was verified more than 60 days ago.
No description provided.