From cda3273f1024fa296b28722ef05ef00b0c04f711 Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Wed, 8 Jul 2026 09:40:54 +0100 Subject: [PATCH 1/4] Stored emojis are working! Added pager in emoji sequence How to is done Picker in progress Built the YourEmojis route Moved the InAppReview class Created the InAppReview Service Layout the bullets Completed layout for the How to choose Added better gradients Removed the textfield Disable sheet gesture Added `deprecated` portrait setting in the Android Manifest xml Wired in the Game Hub when user doesnt have the 3 emojis Polished the layout --- android-build-logic | 2 +- app/build.gradle.kts | 11 +- app/src/main/AndroidManifest.xml | 10 + .../appreview/InAppReviewService.kt | 55 +++ .../com/brainwallet/constants/BWConstants.kt | 4 + .../navigation/MainNavigationHost.kt | 30 +- .../java/com/brainwallet/navigation/Route.kt | 16 +- .../notification/NotificationHandler.kt | 56 ++- .../presenter/activities/BreadActivity.java | 7 +- .../tools/manager/BRSharedPrefs.java | 15 +- .../tools/security/BRKeyStore.java | 15 +- .../brainwallet/tools/security/PostAuth.java | 27 ++ .../com/brainwallet/ui/BrainwalletActivity.kt | 91 +---- .../ui/screens/emojis/EmojiPagerScreen.kt | 63 +++ .../ui/screens/emojis/HowToSetEmojisScreen.kt | 378 +++++++++++++++++ .../ui/screens/emojis/PickEmojisScreen.kt | 381 ++++++++++++++++++ .../YourEmojisScreen.kt | 60 ++- .../brainwallet/ui/screens/main/MainScreen.kt | 42 +- .../ui/screens/main/MainScreenEvent.kt | 2 + .../ui/screens/main/MainViewModel.kt | 40 +- .../setpasscode/SetPasscodeViewModel.kt | 2 +- .../ui/screens/settings/SettingsEvent.kt | 2 + .../ui/screens/settings/SettingsState.kt | 1 + .../ui/screens/settings/SettingsViewModel.kt | 11 + .../settings/settingsrows/SecurityDetail.kt | 43 +- .../ui/screens/youremojis/YourEmojisEvent.kt | 7 - .../screens/youremojis/YourEmojisViewModel.kt | 30 -- .../yourseedwords/YourSeedWordsScreen.kt | 3 +- .../java/com/brainwallet/ui/theme/Gradient.kt | 28 +- .../brainwallet/wallet/BRWalletManager.java | 38 +- .../brainwallet/wallet/WalletOperations.kt | 4 + app/src/main/res/drawable/check_box_24px.xml | 10 + app/src/main/res/drawable/lock_24px.xml | 10 + .../res/drawable/sentiment_satisfied_24px.xml | 10 + app/src/main/res/values/strings.xml | 16 +- bw-gdlib | 2 +- modules/private-general-purpose | 2 +- 37 files changed, 1305 insertions(+), 219 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/appreview/InAppReviewService.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/emojis/EmojiPagerScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/emojis/HowToSetEmojisScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt rename app/src/main/java/com/brainwallet/ui/screens/{youremojis => emojis}/YourEmojisScreen.kt (63%) delete mode 100644 app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisEvent.kt delete mode 100644 app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisViewModel.kt create mode 100644 app/src/main/res/drawable/check_box_24px.xml create mode 100644 app/src/main/res/drawable/lock_24px.xml create mode 100644 app/src/main/res/drawable/sentiment_satisfied_24px.xml diff --git a/android-build-logic b/android-build-logic index 0d43669a..bae909c1 160000 --- a/android-build-logic +++ b/android-build-logic @@ -1 +1 @@ -Subproject commit 0d43669a14ffebdb5738438ea64524ce9fbc39b5 +Subproject commit bae909c1e602818c1ac32b136d463d1d5794fe54 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc7f3421..cb9ddf00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -213,7 +213,6 @@ dependencies { implementation(libs.androidx.espresso.core) implementation(libs.androidx.runtime) implementation(libs.androidx.foundation) - implementation(libs.androidx.animation) implementation(libs.play.services.games) ksp(grunt.koin.annotation.compiler) implementation(platform(libs.squareup.okhttp.bom)) @@ -229,14 +228,16 @@ dependencies { implementation(libs.razir.progressbutton) implementation(libs.appsflyer) implementation(libs.android.installreferrer) - implementation("androidx.compose.animation:animation:1.5.0") - implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") - implementation("io.coil-kt:coil-compose:2.0.0-rc01") + implementation(libs.androidx.material3) testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.turbine) testImplementation(libs.slf4j.android) testImplementation(libs.kotlinx.coroutines.tests) + implementation("androidx.compose.animation:animation:1.10.6") + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") + implementation("io.coil-kt:coil-compose:2.0.0-rc01") + implementation("androidx.emoji2:emoji2-emojipicker:1.6.0") testImplementation("io.mockk:mockk:1.13.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") testImplementation("app.cash.turbine:turbine:1.0.0") @@ -260,6 +261,8 @@ dependencies { implementation("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64") implementation("ltd.grunt.brainwallet.gdx:core:1.14.1") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") + + } tasks.withType { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7a9b9a8..3a9ff95f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,6 +84,7 @@ @@ -94,6 +95,7 @@ @@ -122,41 +124,49 @@ android:name=".presenter.activities.settings.SyncBlockchainActivity" android:exported="true" android:launchMode="singleTask" + android:screenOrientation="portrait" /> Activity?, + +) { + public fun showInAppReviewDialogIfNeeded() { + val activity = activityProvider() ?: return + if (!BRSharedPrefs.isInAppReviewDone(app) && + BRSharedPrefs.getSendTransactionCount(app) > 2 + ) { + val manager = ReviewManagerFactory.create(app) + val request = manager.requestReviewFlow() + request.addOnCompleteListener( + OnCompleteListener { task: Task? -> + AnalyticsManager.logCustomEvent(BWConstants._20241006_DRR) + if (task!!.isSuccessful()) { + val reviewInfo = task.getResult() + val flow = manager.launchReviewFlow(activity, reviewInfo) + flow.addOnCompleteListener( + OnCompleteListener { task1: Task? -> + // The flow has finished. The API does not indicate whether the user + // reviewed or not, or even whether the review dialog was shown. Thus, no + // matter the result, we continue our app flow. + Timber.i( + "timber: In-app LaunchReviewFlow completed successful (%s)", + task1!!.isSuccessful() + ) + if (task1.isSuccessful()) { + BRSharedPrefs.inAppReviewDone(app) + AnalyticsManager.logCustomEvent(BWConstants._20241006_UCR) + } + } + ) + } else { + Timber.e(task.getException(), "In-app request review flow failed") + } + } + ) + } + } +} diff --git a/app/src/main/java/com/brainwallet/constants/BWConstants.kt b/app/src/main/java/com/brainwallet/constants/BWConstants.kt index 77c4fef7..13625a48 100644 --- a/app/src/main/java/com/brainwallet/constants/BWConstants.kt +++ b/app/src/main/java/com/brainwallet/constants/BWConstants.kt @@ -46,6 +46,9 @@ object BWConstants { const val PUT_PHRASE_NEW_WALLET_REQUEST_CODE: Int = 114 const val PUT_PHRASE_RECOVERY_WALLET_REQUEST_CODE: Int = 115 const val PROVE_PHRASE_REQUEST: Int = 119 + const val SHOW_EMOJIS_REQUEST_CODE: Int = 120 + + const val PUT_EMOJIS_REQUEST_CODE: Int = 121 /** * Request codes for taking pictures @@ -71,6 +74,7 @@ object BWConstants { const val CURRENT_UNIT: String = "currencyUnit" const val POSITION: String = "position" const val PHRASE_WRITTEN: String = "phraseWritten" + const val EMOJIS_CHOSEN: String = "emojisChosen" const val ALLOW_SPEND: String = "allowSpend" const val USER_ID: String = "userId" const val GEO_PERMISSIONS_REQUESTED: String = "geoPermissionsRequested" diff --git a/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt b/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt index dd559a55..f04fc51e 100644 --- a/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt +++ b/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt @@ -1,5 +1,7 @@ package com.brainwallet.navigation +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally import androidx.compose.runtime.Composable import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController @@ -8,13 +10,15 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.brainwallet.constants.BWConstants +import com.brainwallet.ui.bentosections.buyreceivebento.receive.ReceiveDialog import com.brainwallet.ui.screens.buyreceive.BuyReceiveScreen import com.brainwallet.ui.screens.gamehub.GameHubScreen import com.brainwallet.ui.screens.home.MainScreen -import com.brainwallet.ui.bentosections.buyreceivebento.receive.ReceiveDialog import com.brainwallet.ui.screens.main.WebModalScreen -import com.brainwallet.ui.screens.restore.RestoreScreen +import com.brainwallet.ui.screens.emojis.EmojiPagerScreen +import com.brainwallet.ui.screens.emojis.YourEmojisScreen import com.brainwallet.ui.screens.ready.ReadyScreen +import com.brainwallet.ui.screens.restore.RestoreScreen import com.brainwallet.ui.screens.send.SendScreen import com.brainwallet.ui.screens.setpasscode.SetPasscodeScreen import com.brainwallet.ui.screens.topup.TopUpScreen @@ -22,6 +26,7 @@ import com.brainwallet.ui.screens.unlock.UnLockScreen import com.brainwallet.ui.screens.welcome.WelcomeScreen import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItScreen import com.brainwallet.ui.screens.yourseedwords.YourSeedWordsScreen +import kotlinx.collections.immutable.toImmutableList /** * Main Navigation Host for compose @@ -102,6 +107,15 @@ fun NavGraphBuilder.mainNavGraph( seedWords = route.seedWords, ) } + + composable { navBackStackEntry -> + val route: Route.YourEmojis = navBackStackEntry.toRoute() + YourEmojisScreen( + onNavigate = onNavigate, + emojis = route.emojis.toImmutableList(), + ) + } + composable { navBackStackEntry -> val route: Route.YourSeedProveIt = navBackStackEntry.toRoute() YourSeedProveItScreen( @@ -154,4 +168,16 @@ fun NavGraphBuilder.mainNavGraph( url = route.url ) } + + composable( + enterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(350) + ) + }, + ) { navBackStackEntry -> + val route: Route.EmojiPickerPager = navBackStackEntry.toRoute() + EmojiPagerScreen(onNavigate = onNavigate) + } } diff --git a/app/src/main/java/com/brainwallet/navigation/Route.kt b/app/src/main/java/com/brainwallet/navigation/Route.kt index 9083cb2e..c80f353f 100644 --- a/app/src/main/java/com/brainwallet/navigation/Route.kt +++ b/app/src/main/java/com/brainwallet/navigation/Route.kt @@ -1,5 +1,6 @@ package com.brainwallet.navigation +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable import java.io.Serializable as JavaSerializable @@ -37,6 +38,10 @@ sealed class Route : JavaSerializable { @Serializable data class YourSeedProveIt(val seedWords: List) : Route() + @Serializable + @Immutable + data class YourEmojis(val emojis: List) : Route() + @Serializable object Main : Route() @@ -68,10 +73,13 @@ sealed class Route : JavaSerializable { data class BitrefillWeb(val url: String) : Route() @Serializable - data class SocialsWeb( - val url: String, - val gameImageURIString: String - ) : Route() + object EmojiPickerPager : Route() + + @Serializable + object HowToSetEmojis : Route() + + @Serializable + object PickEmojis : Route() @Serializable object LinktreeWeb : Route() diff --git a/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt b/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt index 893d37a6..81d183b5 100644 --- a/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt +++ b/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt @@ -11,9 +11,6 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.brainwallet.R -import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE -import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_GENERAL -import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS import com.brainwallet.presenter.activities.BreadActivity import com.google.firebase.messaging.RemoteMessage import org.koin.core.annotation.Single @@ -39,6 +36,59 @@ class NotificationHandler( } ) { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// setupNotificationPermission() +// } +// private val requestNotificationPermissionLauncher: ActivityResultLauncher = +// registerForActivityResult( +// ActivityResultContracts.RequestPermission(), +// ActivityResultCallback { isGranted: Boolean? -> +// if (isGranted) { +// Toast.makeText( +// BreadActivity.app, +// R.string.permission_notification_granted, +// Toast.LENGTH_SHORT +// ).show() +// } +// }) +// +// private val requestNotificationPermissionLauncher = +// registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> +// if (isGranted) { +// Toast.makeText( +// this, +// R.string.permission_notification_granted, +// Toast.LENGTH_SHORT, +// ).show() +// +// Timber.d("Notification permission granted") +// } else { +// Timber.d("Notification permission denied") +// // Optionally show a message explaining reduced functionality +// } +// } +// @RequiresApi(Build.VERSION_CODES.TIRAMISU) +// // private fun setupNotificationPermission() { +// // if (hasPermission(this, Manifest.permission.POST_NOTIFICATIONS)) return +// // +// // if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { +// // showNotificationRationaleDialog() +// // } else { +// // requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS) +// // } +// // } + +// @RequiresApi(Build.VERSION_CODES.TIRAMISU) +// private fun showNotificationRationaleDialog() { +// // AlertDialog.Builder(this) +// // .setTitle(R.string.permission_info) +// // .setMessage(R.string.please_grant_notification_permission) +// // .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } +// // .setPositiveButton(R.string.ok) { _, _ -> +// // requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS) +// // } +// // .show() +// } fun handleMessageReceived(context: Context, remoteMessage: RemoteMessage): Boolean { if (remoteMessage.data.containsKey(KEY_DATA_BRAINWALLET).not()) { return false diff --git a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java index 51828632..29c94850 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -105,12 +105,7 @@ public static BreadActivity getApp() { return app; } - private final ActivityResultLauncher requestNotificationPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - Toast.makeText(app, R.string.permission_notification_granted, Toast.LENGTH_SHORT).show(); - } - }); + @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java b/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java index e918e132..6352c7cc 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java +++ b/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java @@ -11,10 +11,8 @@ import org.koin.java.KoinJavaComponent; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Currency; import java.util.Date; -import java.util.List; import java.util.Locale; import timber.log.Timber; @@ -97,7 +95,6 @@ public static String getSyncMetadata(Context context) { return syncMetadata.getString("syncMetadata", " No Sync Duration metadata"); } - public static boolean getPhraseWroteDown(Context context) { SharedPreferences prefs = context.getSharedPreferences(BWConstants.PREFS_NAME, Context.MODE_PRIVATE); return prefs.getBoolean(BWConstants.PHRASE_WRITTEN, false); @@ -110,6 +107,18 @@ public static void putPhraseWroteDown(Context context, boolean check) { editor.apply(); } + public static boolean wereEmojisChosen(Context context) { + SharedPreferences prefs = context.getSharedPreferences(BWConstants.PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(BWConstants.EMOJIS_CHOSEN, false); + } + + public static void putEmojisChosen(Context context, boolean didPickEmojis) { + SharedPreferences prefs = context.getSharedPreferences(BWConstants.PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(BWConstants.EMOJIS_CHOSEN, didPickEmojis); + editor.apply(); + } + public static int getCurrencyListPosition(Context context) { SharedPreferences settings = context.getSharedPreferences(BWConstants.PREFS_NAME, 0); return settings.getInt(BWConstants.POSITION, 0); diff --git a/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java b/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java index a82ac0c2..bc0b7af4 100644 --- a/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java +++ b/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java @@ -85,6 +85,7 @@ public class BRKeyStore { public static Map aliasObjectMap; private static final String PHRASE_IV = "ivphrase"; + private static final String EMOJI_IV = "ivemoji"; private static final String CANARY_IV = "ivcanary"; private static final String PUB_KEY_IV = "ivpubkey"; private static final String WALLET_CREATION_TIME_IV = "ivtime"; @@ -110,8 +111,8 @@ public class BRKeyStore { public static final String AUTH_KEY_ALIAS = "authKey"; public static final String TOKEN_ALIAS = "token"; public static final String PASS_TIME_ALIAS = "passTime"; - private static final String PHRASE_FILENAME = "my_phrase"; + private static final String EMOJIS_FILENAME = "my_emojis"; private static final String CANARY_FILENAME = "my_canary"; private static final String PUB_KEY_FILENAME = "my_pub_key"; private static final String WALLET_CREATION_TIME_FILENAME = "my_creation_time"; @@ -132,6 +133,7 @@ public class BRKeyStore { static { aliasObjectMap = new HashMap<>(); aliasObjectMap.put(PHRASE_ALIAS, new AliasObject(PHRASE_ALIAS, PHRASE_FILENAME, PHRASE_IV)); + aliasObjectMap.put(EMOJIS_ALIAS, new AliasObject(EMOJIS_ALIAS, EMOJIS_FILENAME, EMOJI_IV)); aliasObjectMap.put(CANARY_ALIAS, new AliasObject(CANARY_ALIAS, CANARY_FILENAME, CANARY_IV)); aliasObjectMap.put(PUB_KEY_ALIAS, new AliasObject(PUB_KEY_ALIAS, PUB_KEY_FILENAME, PUB_KEY_IV)); aliasObjectMap.put(WALLET_CREATION_TIME_ALIAS, new AliasObject(WALLET_CREATION_TIME_ALIAS, WALLET_CREATION_TIME_FILENAME, WALLET_CREATION_TIME_IV)); @@ -266,16 +268,16 @@ public synchronized static byte[] getPhrase(final Context context, int requestCo AliasObject obj = aliasObjectMap.get(PHRASE_ALIAS); return _getData(context, obj.alias, obj.datafileName, obj.ivFileName, requestCode); } - - public synchronized static boolean putEmojis(byte[] strToStore, Context context, int requestCode) throws UserNotAuthenticatedException { + public synchronized static boolean putEmojis(byte[] emojisStringToStore, Context context, int requestCode) + throws UserNotAuthenticatedException { if (PostAuth.isStuckWithAuthLoop) { showLoopBugMessage(context); throw new UserNotAuthenticatedException(); } AliasObject obj = aliasObjectMap.get(EMOJIS_ALIAS); - return !(strToStore == null || strToStore.length == 0) && _setData(context, strToStore, obj.alias, obj.datafileName, obj.ivFileName, requestCode, true); + return !(emojisStringToStore == null || emojisStringToStore.length == 0) && + _setData(context, emojisStringToStore, obj.alias, obj.datafileName, obj.ivFileName, requestCode, false); } - public synchronized static byte[] getEmojis(final Context context, int requestCode) throws UserNotAuthenticatedException { if (PostAuth.isStuckWithAuthLoop) { showLoopBugMessage(context); @@ -285,7 +287,8 @@ public synchronized static byte[] getEmojis(final Context context, int requestCo return _getData(context, obj.alias, obj.datafileName, obj.ivFileName, requestCode); } - public synchronized static boolean putCanary(String strToStore, Context context, int requestCode) throws UserNotAuthenticatedException { + public synchronized static boolean putCanary(String strToStore, Context context, int requestCode) + throws UserNotAuthenticatedException { if (PostAuth.isStuckWithAuthLoop) { showLoopBugMessage(context); throw new UserNotAuthenticatedException(); diff --git a/app/src/main/java/com/brainwallet/tools/security/PostAuth.java b/app/src/main/java/com/brainwallet/tools/security/PostAuth.java index 7a778d2b..203de32b 100644 --- a/app/src/main/java/com/brainwallet/tools/security/PostAuth.java +++ b/app/src/main/java/com/brainwallet/tools/security/PostAuth.java @@ -87,6 +87,33 @@ public void onPhraseCheckAuth(Activity app, boolean authAsked) { app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); } + public void onShowEmojis(Activity app, boolean authAsked) { + String cleanEmojis; + try { + byte[] raw = BRKeyStore.getEmojis(app, BWConstants.SHOW_EMOJIS_REQUEST_CODE); + if (raw == null) { + NullPointerException ex = new NullPointerException("onShowEmojis: getEmojis = null"); + Timber.e(ex); + throw ex; + } + cleanEmojis = new String(raw); + } catch (UserNotAuthenticatedException e) { + if (authAsked) { + Timber.d("timber: %s: WARNING!!!! LOOP", new Object() { + }.getClass().getEnclosingMethod().getName()); + isStuckWithAuthLoop = true; + } + return; + } + + String[] emojis = cleanEmojis.split(","); + LegacyNavigation.openComposeScreen( + app, + new Route.YourSeedWords(Arrays.asList(emojis)) + ); + app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); + } + public void onPhraseProveAuth(Activity app, boolean authAsked) { String cleanPhrase; try { diff --git a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt index 04c67175..b238ce97 100644 --- a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt +++ b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt @@ -1,18 +1,13 @@ package com.brainwallet.ui -import android.Manifest import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager -import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AlertDialog import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.lifecycleScope @@ -51,14 +46,8 @@ import com.brainwallet.ui.screens.settings.SettingsViewModel import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel.Companion.LEGACY_EFFECT_ON_PAPERKEY_PROVED import com.brainwallet.ui.theme.BrainwalletAppTheme import com.brainwallet.util.EventBus -import com.brainwallet.util.PermissionUtil.hasPermission -import com.brainwallet.util.PermissionUtil.requestPermission import com.brainwallet.wallet.BRPeerManager import com.brainwallet.wallet.BRWalletManager -import com.google.android.gms.tasks.OnCompleteListener -import com.google.android.gms.tasks.Task -import com.google.android.play.core.review.ReviewInfo -import com.google.android.play.core.review.ReviewManagerFactory import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -83,28 +72,12 @@ class BrainwalletActivity : private var mConnectionReceiver: InternetManager? = null var appVisible: Boolean = false - private val requestNotificationPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - Toast.makeText( - this, - R.string.permission_notification_granted, - Toast.LENGTH_SHORT, - ).show() - - Timber.d("Notification permission granted") - } else { - Timber.d("Notification permission denied") - // Optionally show a message explaining reduced functionality - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) onConnectionChanged(InternetManager.getInstance().isConnected(this)) BRSharedPrefs.getLTCViewingPreference(application) - showInAppReviewDialogIfNeeded() + val startDestination = intent.getSerializableExtra(EXTRA_START_DESTINATION) ?: Route.Welcome @@ -219,62 +192,6 @@ class BrainwalletActivity : } } - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - private fun setupNotificationPermission() { - if (hasPermission(this, Manifest.permission.POST_NOTIFICATIONS)) return - - if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { - showNotificationRationaleDialog() - } else { - requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS) - } - } - - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - private fun showNotificationRationaleDialog() { - AlertDialog.Builder(this) - .setTitle(R.string.permission_info) - .setMessage(R.string.please_grant_notification_permission) - .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } - .setPositiveButton(R.string.ok) { _, _ -> - requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS) - } - .show() - } - - private fun showInAppReviewDialogIfNeeded() { - if (!BRSharedPrefs.isInAppReviewDone(this) && BRSharedPrefs.getSendTransactionCount(this) > 2) { - val manager = ReviewManagerFactory.create(this) - val request = manager.requestReviewFlow() - request.addOnCompleteListener( - OnCompleteListener { task: Task? -> - AnalyticsManager.logCustomEvent(BWConstants._20241006_DRR) - if (task!!.isSuccessful()) { - val reviewInfo = task.getResult() - val flow = manager.launchReviewFlow(this@BrainwalletActivity, reviewInfo) - flow.addOnCompleteListener( - OnCompleteListener { task1: Task? -> - // The flow has finished. The API does not indicate whether the user - // reviewed or not, or even whether the review dialog was shown. Thus, no - // matter the result, we continue our app flow. - Timber.i( - "timber: In-app LaunchReviewFlow completed successful (%s)", - task1!!.isSuccessful() - ) - if (task1.isSuccessful()) { - BRSharedPrefs.inAppReviewDone(this@BrainwalletActivity) - AnalyticsManager.logCustomEvent(BWConstants._20241006_UCR) - } - } - ) - } else { - Timber.e(task.getException(), "In-app request review flow failed") - } - } - ) - } - } - private fun setupNetworking() { if (mConnectionReceiver == null) mConnectionReceiver = InternetManager.getInstance() val mNetworkStateFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) @@ -317,9 +234,6 @@ class BrainwalletActivity : * then should go to setpasscode */ private fun onCheckPin() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - setupNotificationPermission() - } val pin = BRKeyStore.getPinCode(this) if (pin.isEmpty() && pin.length != BW_PIN_LENGTH) { lifecycleScope.launch { @@ -341,9 +255,6 @@ class BrainwalletActivity : */ private fun onLegacyLogic() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - setupNotificationPermission() - } if (Utils.isEmulatorOrDebug(this)) Utils.printPhoneSpecs() val masterPubKey = BRKeyStore.getMasterPublicKey(this) diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/EmojiPagerScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/EmojiPagerScreen.kt new file mode 100644 index 00000000..c7e376fd --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/EmojiPagerScreen.kt @@ -0,0 +1,63 @@ +package com.brainwallet.ui.screens.emojis +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.brainwallet.navigation.OnNavigate +import com.brainwallet.ui.screens.main.MainViewModel +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmojiPagerScreen( + onNavigate: OnNavigate, + modifier: Modifier = Modifier, + viewModel: MainViewModel = koinViewModel(), + onDismissPickEmojis: () -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + val hubPageCount = 2 + val pagerState = rememberPagerState(pageCount = { hubPageCount }) + val scope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxWidth() + ) { + HorizontalPager( + state = pagerState, + userScrollEnabled = false, + pageSpacing = 8.dp, + modifier = modifier.fillMaxSize() + ) { page -> + when (page) { + 0 -> HowToSetEmojisScreen( + onNextPage = { + scope.launch { + pagerState.animateScrollToPage(page + 1) + } + } + ) + 1 -> PickEmojisScreen( + firstThreeWords = viewModel.walletManager.getSeedWords(), + onEmojisPicked = { wereEmojisPicked -> + scope.launch { + if (wereEmojisPicked) { + onDismissPickEmojis() + } else { + onDismiss() + } + } + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/HowToSetEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/HowToSetEmojisScreen.kt new file mode 100644 index 00000000..c65e6ba4 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/HowToSetEmojisScreen.kt @@ -0,0 +1,378 @@ +@file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) + +package com.brainwallet.ui.screens.emojis + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.brainwallet.R +import com.brainwallet.ui.theme.DesignTheme +import com.brainwallet.ui.theme.IBMPlexSans +import com.brainwallet.ui.theme.bentoHowToSectionGradient +import com.brainwallet.ui.theme.bentoModalDarkGradient +import com.brainwallet.ui.theme.colorMidnite + +@Composable +fun HowToSetEmojisScreen( + modifier: Modifier = Modifier, + onNextPage: () -> Unit = {} +) { + val horizontalVerticalSpacing = 8 + + val iconSizeBox = 85 + val iconSize = 30 + var resizedDescriptlineFontSize by remember { mutableStateOf(15.sp) } + + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth() + .background(brush = bentoModalDarkGradient) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(15.dp) + .background( + color = Color.Transparent, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), + ) { + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(10.dp), + text = stringResource(R.string.how_to_choose_emojis_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + fontSize = 22.sp, + color = Color.White + ), + maxLines = 2 + ) + + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(15.dp), + text = stringResource(R.string.how_to_choose_emojis_description), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center, + fontSize = 16.sp, + color = Color.White + ), + maxLines = 2 + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth() + .fillMaxHeight(0.65f) + .clip(DesignTheme.shapes.large) + .background( + brush = bentoHowToSectionGradient, + ) + .border( + width = 0.3.dp, + color = Color.White.copy(alpha = 0.1f), + shape = DesignTheme.shapes.large + ), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly + + ) { + Row( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(iconSizeBox.dp) + .padding(10.dp) + .padding(start = 10.dp) + .padding(top = 10.dp, bottom = 10.dp) + .aspectRatio(1f) + .clip(CircleShape) + .border( + 0.3.dp, + Color.White, + CircleShape + ) + .background(Color.White.copy(0.1f)) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(iconSize.dp), + tint = Color.White, + painter = painterResource(R.drawable.sentiment_satisfied_24px), + contentDescription = stringResource(id = R.string.ok) + ) + } + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 20.dp) + ) { + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 5.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_one_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + color = Color.White + ), + maxLines = 1 + ) + + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 10.dp, end = 10.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_one_description), + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow) { + resizedDescriptlineFontSize *= 0.95f + } + }, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Light, + fontSize = resizedDescriptlineFontSize, + color = Color.White + ), + maxLines = 3 + ) + } + } + HorizontalDivider( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp), + thickness = 0.5.dp, + Color.White.copy(0.2f) + ) + Row( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(iconSizeBox.dp) + .padding(10.dp) + .padding(start = 10.dp) + .padding(top = 10.dp, bottom = 10.dp) + .aspectRatio(1f) + .clip(CircleShape) + .border( + 0.3.dp, + Color.White, + CircleShape + ) + .background(Color.White.copy(0.1f)) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(iconSize.dp), + tint = Color.White, + painter = painterResource(R.drawable.check_box_24px), + contentDescription = stringResource(id = R.string.ok) + ) + } + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 20.dp) + ) { + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 5.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_two_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + color = Color.White + ), + maxLines = 1 + ) + + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 10.dp, end = 10.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_two_description), + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow) { + resizedDescriptlineFontSize *= 0.95f + } + }, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Light, + fontSize = resizedDescriptlineFontSize, + color = Color.White + ), + maxLines = 3 + ) + } + } + HorizontalDivider( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp), + thickness = 0.5.dp, + Color.White.copy(0.2f) + ) + Row( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(iconSizeBox.dp) + .padding(10.dp) + .padding(start = 10.dp) + .padding(top = 10.dp, bottom = 10.dp) + .aspectRatio(1f) + .clip(CircleShape) + .border( + 0.3.dp, + Color.White, + CircleShape + ) + .background(Color.White.copy(0.1f)) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(iconSize.dp), + tint = Color.White, + painter = painterResource(R.drawable.lock_24px), + contentDescription = stringResource(id = R.string.settings_title_lock) + ) + } + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 20.dp) + ) { + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 5.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_three_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + color = Color.White + ), + maxLines = 1 + ) + + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(start = 10.dp, top = 5.dp, bottom = 10.dp, end = 10.dp), + text = stringResource(R.string.how_to_choose_emojis_bullet_three_description), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Light, + fontSize = resizedDescriptlineFontSize, + color = Color.White + ), + maxLines = 3 + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(0.9f)) + + FilledTonalButton( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding( + start = 20.dp, + end = 20.dp, + ), + enabled = true, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = Color.White, + contentColor = colorMidnite + ), + onClick = { + onNextPage() + }, + shape = RoundedCornerShape(11.dp) + + ) { + Text( + modifier = Modifier, + text = stringResource(R.string.RecoverWallet_next), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + ) + } + Spacer(modifier = Modifier.weight(0.2f)) + } + } +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt new file mode 100644 index 00000000..e79765a4 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt @@ -0,0 +1,381 @@ +package com.brainwallet.ui.screens.emojis + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.emoji2.emojipicker.EmojiPickerView +import com.brainwallet.R +import com.brainwallet.constants.BWConstants +import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.security.BRKeyStore +import com.brainwallet.ui.theme.DesignTheme +import com.brainwallet.ui.theme.IBMPlexSans +import com.brainwallet.ui.theme.bentoEmojiSectionGradient +import com.brainwallet.ui.theme.bentoModalDarkGradient +import com.brainwallet.ui.theme.colorMidnite +import kotlinx.collections.immutable.ImmutableList +import kotlin.collections.toByteArray + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PickEmojisScreen( + firstThreeWords: ImmutableList, + modifier: Modifier = Modifier, + onEmojisPicked: (Boolean) -> Unit = {}, +) { + val context = LocalContext.current + var selectedEmojis by remember { mutableStateOf(listOf(null, null, null)) } + var activeSlot by remember { mutableStateOf(null) } + var shouldShowSeedWords by remember { mutableStateOf(false) } + + Box( + modifier = modifier + .fillMaxSize() + .background(brush = bentoModalDarkGradient) + ) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row(modifier = Modifier.height(44.dp)) { + IconButton( + onClick = { onEmojisPicked(false) }, + ) { + Icon( + painter = painterResource(id = com.brainwallet.R.drawable.btn_clear), + contentDescription = stringResource(R.string.clear), + tint = Color.White + ) + } + Spacer(modifier = Modifier.weight(1f)) + } + + Row { + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 30.dp, end = 30.dp) + .padding(bottom = 10.dp), + text = stringResource(R.string.pick_emojis_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + fontSize = 22.sp, + color = Color.White + ), + maxLines = 2 + ) + } + + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(start = 30.dp, end = 30.dp) + .padding(top = 5.dp, bottom = 5.dp), + text = stringResource(R.string.pick_emojis_description), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center, + fontSize = 16.sp, + color = Color.White + ), + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 3 emoji picker slots + val sectionSize = 120.dp + + Box( + modifier = Modifier + .fillMaxWidth() + .height(sectionSize + 32.dp) + .padding(6.dp) + .clip(DesignTheme.shapes.large) + .background(brush = bentoEmojiSectionGradient) + .border( + width = 0.3.dp, + color = Color.White.copy(alpha = 0.1f), + shape = DesignTheme.shapes.large + ), + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + selectedEmojis.forEachIndexed { index, emoji -> + EmojiSlot( + emoji = emoji, + word = firstThreeWords[index], + index = index, + shouldShowWord = shouldShowSeedWords, + isActive = activeSlot == index, + onClick = { activeSlot = index }, + modifier = Modifier + .width(sectionSize) + .height(sectionSize) + .padding(horizontal = 2.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + Icon( + modifier = Modifier.align(Alignment.CenterHorizontally) + .padding(bottom = 5.dp) + .size(30.dp) + .clickable { + shouldShowSeedWords = !shouldShowSeedWords + }, + painter = if (shouldShowSeedWords) { + painterResource(id = R.drawable.visibility_off) + } else { + painterResource(id = R.drawable.visibility_svg) + }, + contentDescription = "Toggle Seed words show", + tint = Color.White + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.6f) + .padding(start = 16.dp, end = 16.dp) + .clip(DesignTheme.shapes.large) + .background(brush = bentoEmojiSectionGradient) + .border( + width = 0.5.dp, + color = Color.White.copy(alpha = 0.2f), + shape = DesignTheme.shapes.large + ) + ) { + Column(modifier = Modifier.fillMaxSize()) { + EmojiPickerSection( + modifier = Modifier.fillMaxSize(), + onEmojiSelected = { emoji -> + activeSlot?.let { slot -> + selectedEmojis = selectedEmojis + .toMutableList().apply { + this[slot] = emoji + shouldShowSeedWords = !shouldShowSeedWords + } + activeSlot = null + } + } + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + FilledTonalButton( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding( + start = 20.dp, + end = 20.dp, + ), + enabled = true, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = Color.White, + contentColor = if (selectedEmojis.all { it != null }) { + colorMidnite + } else { + colorMidnite.copy( + alpha = 0.3f + ) + } + ), + onClick = { + if (selectedEmojis.all { it != null }) { + val joined = selectedEmojis.joinToString(separator = "") + val bytes = joined.toByteArray(Charsets.UTF_8) + BRKeyStore.putEmojis( + bytes, + context, + BWConstants.PUT_EMOJIS_REQUEST_CODE + ) + BRSharedPrefs.putEmojisChosen(context, true) + onEmojisPicked(true) + AnalyticsManager + .logCustomAdHocEvent("user_set_first_set_emojis", null) + } + }, + shape = RoundedCornerShape(11.dp) + + ) { + Text( + modifier = Modifier, + text = if (selectedEmojis.all { it != null }) { + stringResource(R.string.lets_play_label) + } else { + stringResource(R.string.pick_emojis_title) + }, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + ) + } + Spacer(modifier = Modifier.weight(0.05f)) + } + } +} + +@Composable +fun EmojiPickerSection( + modifier: Modifier = Modifier, + onEmojiSelected: (String) -> Unit = {}, + emojiIndex: Int = 0 +) { + AndroidView( + factory = { context -> + EmojiPickerView(context).apply { + setOnEmojiPickedListener { emojiViewItem -> + onEmojiSelected(emojiViewItem.emoji) + } + } + }, + modifier = modifier + .fillMaxSize() + .nestedScroll(rememberNestedScrollInteropConnection()), + ) +} + +@Composable +private fun EmojiSlot( + emoji: String?, + word: String?, + index: Int, + shouldShowWord: Boolean, + isActive: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .border( + if (isActive) { + 1.dp + } else { + 0.3.dp + }, + if (isActive) { + Color.White + } else { + Color.White.copy(alpha = 0.1f) + }, + RoundedCornerShape(12.dp) + ) + .background(brush = bentoEmojiSectionGradient) + .clickable { onClick.invoke() } + .padding(vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + if (shouldShowWord) { + if (word != null) { + Box { + val number = index + 1 + Text( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 3.dp), + text = number.toString(), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Start, + fontSize = 9.sp, + color = Color.White.copy(alpha = 0.6f) + ), + ) + Text( + modifier = Modifier + .clip(RoundedCornerShape(percent = 50)) + .background(Color.White.copy(alpha = 0.08f)) + .padding(horizontal = 20.dp, vertical = 5.dp), + text = word, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center, + fontSize = 16.sp, + color = Color.White + ), + ) + } + } + } else { + if (emoji != null) { + Text( + text = emoji, + fontSize = 48.sp, + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.sentiment_satisfied_24px), + contentDescription = null, + tint = Color.White.copy(alpha = 0.6f) + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt similarity index 63% rename from app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisScreen.kt rename to app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt index f73155c8..976c1fbf 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt @@ -1,13 +1,15 @@ @file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) -package com.brainwallet.ui.screens.youremojis - +package com.brainwallet.ui.screens.emojis import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -18,43 +20,34 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.brainwallet.R import com.brainwallet.navigation.OnNavigate import com.brainwallet.navigation.UiEffect import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton +import com.brainwallet.ui.composable.SeedWordItem +import com.brainwallet.ui.composable.SeedWordsLayout import kotlinx.collections.immutable.ImmutableList -import org.koin.compose.koinInject @Composable fun YourEmojisScreen( onNavigate: OnNavigate, emojis: ImmutableList, - modifier: Modifier = Modifier, - viewModel: YourEmojisViewModel = koinInject() + modifier: Modifier = Modifier ) { - // / Layout values val columnPadding = 12 val horizontalVerticalSpacing = 8 - val spacerHeight = 36 val leadingCopyPadding = 8 val detailLineHeight = 24 - LaunchedEffect(Unit) { - viewModel.uiEffect.collect { effect -> - when (effect) { - is UiEffect.Navigate -> onNavigate.invoke(effect) - else -> Unit - } - } - } - BrainwalletScaffold( topBar = { BrainwalletTopAppBar( @@ -81,22 +74,47 @@ fun YourEmojisScreen( verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), ) { Text( - text = stringResource(R.string.your_seed_words), + text = stringResource(R.string.your_emojis_title), style = MaterialTheme.typography.headlineSmall, ) - Spacer(modifier = Modifier.weight(0.2f)) + Text( + modifier = Modifier + .padding(top = leadingCopyPadding.dp), + text = stringResource(R.string.your_emojis_description), + style = MaterialTheme.typography.bodyLarge.copy( + textAlign = TextAlign.Center, + lineHeight = detailLineHeight.sp, + color = Color.Gray + ) + ) + + Spacer(modifier = Modifier.weight(0.1f)) + + SeedWordsLayout(modifier = Modifier.weight(1f)) { + itemsIndexed(items = emojis) { index, emoji -> + SeedWordItem( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + label = "${index + 1} $emoji" + ) + } + } + + Spacer(modifier = Modifier.weight(0.1f)) LargeButton( onClick = { - viewModel.onEvent(YourEmojisEvent.OnSavedItClick(emojis)) + // viewModel.onEvent(YourSeedWordsEvent.OnSavedItClick(seedWords)) }, ) { Text( - text = stringResource(R.string.i_saved_it_on_paper), + text = stringResource(R.string.your_emojis_reset), style = MaterialTheme.typography.bodyLarge ) } + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt index 054cbf51..f0506e7f 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt @@ -89,6 +89,7 @@ import com.brainwallet.ui.screens.main.MainViewModel import com.brainwallet.ui.screens.main.SettingsButton import com.brainwallet.ui.screens.main.ThemeButton import com.brainwallet.ui.screens.main.WebModalScreen +import com.brainwallet.ui.screens.emojis.EmojiPagerScreen import com.brainwallet.ui.screens.send.SendScreen import com.brainwallet.ui.screens.settings.settingsrows.HomeSettingDrawerSheet import com.brainwallet.ui.theme.BrainwalletAppTheme @@ -122,7 +123,6 @@ fun MainScreen( val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var isSheetOpen by remember { mutableStateOf(false) } - var modalContentRoute by remember { mutableStateOf(null) } val shopBentoState by shopBentoViewModel.state.collectAsStateWithLifecycle() val appSetting by viewModel.appSetting.collectAsStateWithLifecycle() @@ -252,6 +252,13 @@ fun MainScreen( } else if (route == Route.History) { viewModel.onEvent(MainScreenEvent.OnToggleTransactionsDetail) if (showTransactionDetail) currentRoute = Route.Main + } else if (route == Route.GameHub) { + if (!BRSharedPrefs.wereEmojisChosen(context)) { + modalContentRoute = Route.EmojiPickerPager + isSheetOpen = true + } else { + viewModel.onEvent(MainScreenEvent.OnToggleGameHub) + } } else { modalContentRoute = route isSheetOpen = true @@ -409,7 +416,12 @@ fun MainScreen( .clickable( enabled = !showTransactionDetail ) { - viewModel.onEvent(MainScreenEvent.OnToggleGameHub) + if (BRSharedPrefs.wereEmojisChosen(context)) { + viewModel.onEvent(MainScreenEvent.OnToggleGameHub) + } else { + modalContentRoute = Route.EmojiPickerPager + isSheetOpen = true + } } ) { GameHubBentoScreen( @@ -462,7 +474,8 @@ fun MainScreen( } else { Color.Black.copy(0.1f) }, - shape = RoundedCornerShape(24.dp) + shape = RoundedCornerShape(24.dp), + sheetGesturesEnabled = modalContentRoute != Route.EmojiPickerPager, ) { Box( modifier = Modifier @@ -471,8 +484,9 @@ fun MainScreen( .fillMaxHeight( if (modalContentRoute == Route.TutorialSend || modalContentRoute == Route.TutorialWalkthrough || - modalContentRoute is Route.BitrefillWeb || - modalContentRoute == Route.LinktreeWeb + modalContentRoute == Route.BitrefillWeb || + modalContentRoute == Route.LinktreeWeb || + modalContentRoute == Route.EmojiPickerPager ) { 1f } else { @@ -480,7 +494,9 @@ fun MainScreen( } ) .background( - brush = if (modalContentRoute == Route.BuyReceive) { + brush = if (modalContentRoute == Route.BuyReceive || + modalContentRoute == Route.EmojiPickerPager + ) { bentoClearGradient } else if (isDarkMode && modalContentRoute == Route.Send) { bentoModalDarkGradient @@ -543,7 +559,19 @@ fun MainScreen( shopBentoViewModel.onEvent(ShopBentoEvent.InvoiceCreated) } ) - else -> {} + is Route.EmojiPickerPager -> EmojiPagerScreen( + modifier = Modifier.fillMaxSize(), + onNavigate = onNavigate, + onDismissPickEmojis = { + isSheetOpen = false + viewModel.onEvent(MainScreenEvent.OnToggleGameHub) + }, + onDismiss = { + isSheetOpen = false + } + ) + else + -> {} } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt index 67f63e02..c69d576b 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt @@ -17,6 +17,8 @@ sealed class MainScreenEvent { data class OnCopyTransactions(val transactionItem: TxItem) : MainScreenEvent() data object OnToggleGameHub : MainScreenEvent() + data object OnUserChoosesEmojis : MainScreenEvent() + data class OnPostSocial( val jsonString: String, val screenShotData: ByteArray diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt index 50469a28..f4cdac9b 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt @@ -27,6 +27,8 @@ import com.brainwallet.util.CurrencyDataGetter import com.brainwallet.util.VersionCodeProvider import com.brainwallet.wallet.BRPeerManager import com.brainwallet.wallet.BRWalletManager +import com.brainwallet.wallet.WalletOperations +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -44,10 +46,12 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.json.JSONArray import org.json.JSONObject import org.koin.android.annotation.KoinViewModel import timber.log.Timber import java.math.BigDecimal +import java.text.BreakIterator import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -62,6 +66,7 @@ class MainViewModel( private val txRepository: TxRepository, private val connectivityRepository: ConnectivityRepository, private val balanceBentoViewModel: BalanceBentoViewModel, + private val getWalletManager: () -> WalletOperations = { BRWalletManager.getInstance() }, versionCodeProvider: VersionCodeProvider, ) : BrainwalletViewModel(), BRWalletManager.OnBalanceChanged, @@ -77,6 +82,8 @@ class MainViewModel( ) val state: StateFlow = _state.asStateFlow() val appSetting: StateFlow = settingRepository.currentSettings + val walletManager = getWalletManager() + val seedWords: ImmutableList = walletManager.getSeedWords() init { @@ -180,14 +187,29 @@ class MainViewModel( val userLanguage = settingRepository.getCurrentLanguage().code val emojis = runCatching { BRKeyStore.getEmojis(app, 0) } .getOrNull()?.decodeToString() ?: "NO_EMOJIS" - val obj = - JSONObject( - """{"launchParameters":{"address":"$address", - |"language":$userLanguage, - |"timestamp":$timestamp, - |"emojis": "$emojis" }} - """.trimMargin() - ) + + val emojiArray = JSONArray().apply { + val boundary = BreakIterator.getCharacterInstance() + boundary.setText(emojis) + var start = boundary.first() + var end = boundary.next() + while (end != BreakIterator.DONE) { + put(emojis.substring(start, end)) + start = end + end = boundary.next() + } + } + + val launchParameters = JSONObject().apply { + put("address", address) + put("language", userLanguage) + put("timestamp", timestamp) + put("emojis", emojiArray) + } + + val obj = JSONObject().apply { + put("launchParameters", launchParameters) + } return obj.toString() } @@ -487,6 +509,8 @@ class MainViewModel( Timber.e(e) } } + is MainScreenEvent.OnUserChoosesEmojis -> { + } } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt index 625042c0..a739a674 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt @@ -73,7 +73,7 @@ class SetPasscodeViewModel : BrainwalletViewModel() { ) } - // if passcode confirm all filled, then goto your seed words + // if passcode confirm all filled if (it.isMatchPasscode()) { viewModelScope.launch { EventBus.emit( diff --git a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsEvent.kt index 0ed4df00..0ca3ce63 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsEvent.kt @@ -12,7 +12,9 @@ sealed class SettingsEvent { ) : SettingsEvent() object OnSecurityUpdatePinClick : SettingsEvent() object OnSecuritySeedPhraseClick : SettingsEvent() + object OnSecurityBrainwalletPhraseClick : SettingsEvent() object OnSecurityShareAnalyticsDataClick : SettingsEvent() + object OnToggleDarkMode : SettingsEvent() object OnToggleLock : SettingsEvent() object OnLanguageSelectorButtonClick : SettingsEvent() diff --git a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsState.kt b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsState.kt index 145b6f7b..919a77ad 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsState.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsState.kt @@ -19,6 +19,7 @@ data class SettingsState( "$" ), // -1 = need to fetch val languageSelectorBottomSheetVisible: Boolean = false, + val brainwalletPhraseBottomSheetVisible: Boolean = false, val fiatSelectorBottomSheetVisible: Boolean = false, val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: SyncAnalyticsRepository.SyncMetadata? = null, diff --git a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsViewModel.kt index 9f25d4d8..b47d87d7 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/settings/SettingsViewModel.kt @@ -183,6 +183,15 @@ class SettingsViewModel( ) } + SettingsEvent.OnSecurityBrainwalletPhraseClick -> viewModelScope.launch { + EventBus.emit( + EventBus.Event.Message( + LEGACY_EFFECT_ON_BRAINWALLET_PHRASE, + address = null + ) + ) + } + SettingsEvent.OnSecurityUpdatePinClick -> viewModelScope.launch { EventBus.emit( EventBus.Event.Message( @@ -216,6 +225,8 @@ class SettingsViewModel( const val LEGACY_EFFECT_ON_SYNC = "onSyncInvoked" const val LEGACY_EFFECT_ON_SEC_UPDATE_PIN = "onSecUpdatePin" const val LEGACY_EFFECT_ON_SEED_PHRASE = "onSeedPhrase" + + const val LEGACY_EFFECT_ON_BRAINWALLET_PHRASE = "onBrainwalletPhrase" const val LEGACY_EFFECT_ON_SHARE_ANALYTICS_DATA_TOGGLE = "onShareAnalyticsDataToggle" } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/SecurityDetail.kt b/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/SecurityDetail.kt index 3fc9c2df..c859312c 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/SecurityDetail.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/SecurityDetail.kt @@ -10,8 +10,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.brainwallet.R import com.brainwallet.ui.screens.settings.SettingsEvent @@ -62,18 +68,31 @@ fun SecurityDetail( } } -// Row( -// modifier = Modifier -// .height(contentHeight.dp), -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text(stringResource(R.string.security_brainwallet_phrase_title)) -// Spacer(modifier = Modifier.weight(1f)) -// Button(onClick = { -// }) { -// Text(stringResource(R.string.security_phrase_button)) -// } -// } + Row( + modifier = Modifier + .height(contentHeight.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(R.string.security_brainwallet_phrase_title)) + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { + onEvent.invoke(SettingsEvent.OnSecurityBrainwalletPhraseClick) + }) { + Text( + text = "😅", + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.5f), + offset = Offset(x = 4f, y = 4f), + blurRadius = 4f + ) + ), + maxLines = 1 + ) + } + } Row( modifier = Modifier .height(contentHeight.dp), diff --git a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisEvent.kt deleted file mode 100644 index 01863b49..00000000 --- a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.brainwallet.ui.screens.youremojis - -import kotlinx.collections.immutable.ImmutableList - -sealed class YourEmojisEvent { - data class OnSavedItClick(val emojis: ImmutableList) : YourEmojisEvent() -} diff --git a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisViewModel.kt deleted file mode 100644 index 40dab89e..00000000 --- a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.brainwallet.ui.screens.youremojis - -import android.app.Application -import androidx.lifecycle.viewModelScope -import com.brainwallet.tools.manager.BRSharedPrefs -import com.brainwallet.tools.security.BRKeyStore -import com.brainwallet.ui.BrainwalletViewModel -import kotlinx.coroutines.launch -import org.json.JSONObject -import org.koin.android.annotation.KoinViewModel - -@KoinViewModel -class YourEmojisViewModel( - private val app: Application, -) : BrainwalletViewModel() { - private val address = BRSharedPrefs.getReceiveAddress(app) - private val timestamp = java.util.Date().time - private val emojis = runCatching { BRKeyStore.getEmojis(app, 0) } - .getOrNull()?.decodeToString() ?: "NO_EMOJIS" - private val launchParams = JSONObject( - """{"launchParameters":{"address":"$address", "timestamp":$timestamp,"emojis": "$emojis" }}""" - ) - - override fun onEvent(event: YourEmojisEvent) { - when (event) { - is YourEmojisEvent.OnSavedItClick -> viewModelScope.launch { - } - } - } -} diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt index 1de5bab8..23911d28 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt @@ -44,10 +44,9 @@ fun YourSeedWordsScreen( seedWords: List, viewModel: YourSeedWordsViewModel = koinInject() ) { - // / Layout values + // Layout values val columnPadding = 12 val horizontalVerticalSpacing = 8 - val spacerHeight = 36 val leadingCopyPadding = 8 val detailLineHeight = 24 diff --git a/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt b/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt index 490fed01..c363a226 100644 --- a/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt +++ b/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt @@ -189,8 +189,8 @@ val bentoModalDarkGradient: Brush Color(0xFF09082B).copy(alpha = 1f), Color(0xFF280589).copy(alpha = 0.7f) ), - start = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), - end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), + start = Offset(Float.MIN_VALUE, Float.MIN_VALUE), + end = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), ) fun giftCardGradient1(size: androidx.compose.ui.geometry.Size): Brush { @@ -228,3 +228,27 @@ fun giftCardGradient3(size: androidx.compose.ui.geometry.Size): Brush { tileMode = TileMode.Clamp ) } + +val bentoEmojiSectionGradient: Brush + + @Composable + get() = Brush.linearGradient( + colors = listOf( + Color(0xFF14134c).copy(alpha = 0.8f), + Color(0xFF5827e2).copy(alpha = 0.1f) + ), + start = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), + end = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), + ) + +val bentoHowToSectionGradient: Brush + + @Composable + get() = Brush.linearGradient( + colors = listOf( + Color(0xFF14134c).copy(alpha = 0.9f), + Color(0xFF5827e2).copy(alpha = 0.25f) + ), + start = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), + end = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), + ) diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 4d0bc438..ca98d165 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -17,9 +17,6 @@ import android.view.View; import android.widget.EditText; import android.widget.Toast; - -import androidx.fragment.app.FragmentActivity; - import com.brainwallet.BrainwalletApp; import com.brainwallet.R; import com.brainwallet.presenter.activities.BreadActivity; @@ -30,7 +27,6 @@ import com.brainwallet.presenter.entities.BWDatabaseTransactionEntity; import com.brainwallet.presenter.entities.ImportPrivKeyEntity; import com.brainwallet.presenter.entities.TxItem; -import com.brainwallet.presenter.interfaces.BROnSignalCompletion; import com.brainwallet.tools.animation.BRAnimator; import com.brainwallet.tools.animation.SpringAnimator; import com.brainwallet.tools.manager.AnalyticsManager; @@ -51,13 +47,19 @@ import com.brainwallet.tools.util.Utils; import com.platform.entities.WalletInfo; +import org.jetbrains.annotations.NotNull; + import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import kotlin.Suppress; +import kotlinx.collections.immutable.ImmutableList; +import kotlinx.collections.immutable.adapters.ImmutableListAdapter; import timber.log.Timber; public class BRWalletManager implements WalletOperations, @@ -163,14 +165,29 @@ public synchronized boolean generateRandomSeed(final Context ctx) { BRKeyStore.putWalletCreationTime(walletCreationTime, ctx); final WalletInfo info = new WalletInfo(); info.creationDate = walletCreationTime; - byte[] strBytes = TypesConverter.getNullTerminatedPhrase(strPhrase); byte[] pubKey = BRWalletManager.getInstance().getMasterPubKey(strBytes); BRKeyStore.putMasterPublicKey(pubKey, ctx); - return true; } - + public List getSeedWords(Context context) { + byte[] phrase = null; + try { + phrase = BRKeyStore.getPhrase(context, 0); + if (phrase == null) { + return Collections.emptyList(); + } + String[] words = new String(phrase, StandardCharsets.UTF_8).split(" "); + return Arrays.asList(words); + } catch (UserNotAuthenticatedException e) { + throw new RuntimeException( + "Failed to retrieve the phrase even though at this point the system auth was asked for sure.", e); + } finally { + if (phrase != null) { + Arrays.fill(phrase, (byte) 0); + } + } + } public boolean wipeKeyStore(Context context) { Timber.d("timber: wipeKeyStore"); return BRKeyStore.resetWalletKeyStore(context); @@ -549,6 +566,13 @@ public String getSeedPhrase(Context context) { return new String(phraseBytes); } + @Override + public @NotNull ImmutableList getSeedWords() { + Context context = BrainwalletApp.getBreadContext(); + List words = getSeedWords(context); + return new ImmutableListAdapter<>(words); + } + public interface OnBalanceChanged { void onBalanceChanged(long balance); } diff --git a/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt b/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt index 1279a3d3..76e10a0a 100644 --- a/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt +++ b/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt @@ -1,7 +1,11 @@ package com.brainwallet.wallet +import kotlinx.collections.immutable.ImmutableList + interface WalletOperations { fun tryTransactionWithOps(sendAddress: String, sendAmount: Long, opsAddress: String, opsFeeAmount: Long): ByteArray? fun getMinOutputAmount(): Long fun getMinOutputAmountRequested(): Long + + fun getSeedWords(): ImmutableList } diff --git a/app/src/main/res/drawable/check_box_24px.xml b/app/src/main/res/drawable/check_box_24px.xml new file mode 100644 index 00000000..0249f815 --- /dev/null +++ b/app/src/main/res/drawable/check_box_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/lock_24px.xml b/app/src/main/res/drawable/lock_24px.xml new file mode 100644 index 00000000..67e91830 --- /dev/null +++ b/app/src/main/res/drawable/lock_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/sentiment_satisfied_24px.xml b/app/src/main/res/drawable/sentiment_satisfied_24px.xml new file mode 100644 index 00000000..302b96f6 --- /dev/null +++ b/app/src/main/res/drawable/sentiment_satisfied_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d094fac8..50a2c635 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,6 +402,7 @@ PIN Seed phrase Share analytics data + Brainwallet phrase Update PIN Show Buy LTC @@ -492,5 +493,18 @@ Scan or paste the destination address. Double check! Change your local fiat to figure out how much you can send. Add a memo as a handy reminder. Tap Continue. Shop Buy Gift Cards with LTC! - + Pick Emojis + Tap a box. Show or hide words to choose your emojis. Tap an emoji. + Choose your 3 emojis for the game + Here is where you build your real brainwallet. Memorize your seed phrase through gaming. + Pick your first 3 emojis + There are 12 seed words in your phrase. Use the first 3 words to choose. + Emojis that have meaning + Use matches to the word like lemon. Or, yellow. Or, not. It\'s up to you! + Seed to your memory + See the emoji, you\'ll remember the seed word. Memorize your emoji...get POINTS! + Emojis set. Let\'s play! + Your Brainwallet Emojis + You choose these emojis for the game and help you remember your seed phrase + Clear emojis diff --git a/bw-gdlib b/bw-gdlib index d58b9e71..c8ca278c 160000 --- a/bw-gdlib +++ b/bw-gdlib @@ -1 +1 @@ -Subproject commit d58b9e7150163cdca07da7b1d654bb85afc43eec +Subproject commit c8ca278c2142c63322ec176835a4dd321bb8930a diff --git a/modules/private-general-purpose b/modules/private-general-purpose index ba0531d4..2fe93eb1 160000 --- a/modules/private-general-purpose +++ b/modules/private-general-purpose @@ -1 +1 @@ -Subproject commit ba0531d44be6a796fe86867912962a26676d4afe +Subproject commit 2fe93eb1b79fb16d1d7975aedbe66d33d5c8fb35 From b6ce0945af0ad358943212498997294820157cba Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Mon, 13 Jul 2026 19:15:20 +0100 Subject: [PATCH 2/4] Cleaned up item layout in 3 emojis Added logic to show the emojis WIP: Figuring out the nav for setting emojis Relayout the Pick Emojis x close --- .../com/brainwallet/constants/BWConstants.kt | 2 + .../tools/security/BRKeyStore.java | 2 +- .../brainwallet/tools/security/PostAuth.java | 31 +++++-- .../com/brainwallet/ui/BrainwalletActivity.kt | 3 + .../ui/screens/emojis/PickEmojisScreen.kt | 74 +++++++++------- .../ui/screens/emojis/YourEmojisScreen.kt | 85 +++++++++++++++++-- .../ui/screens/main/MainScreenEvent.kt | 2 + .../ui/screens/main/MainViewModel.kt | 12 ++- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 165 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/brainwallet/constants/BWConstants.kt b/app/src/main/java/com/brainwallet/constants/BWConstants.kt index 13625a48..fe413c28 100644 --- a/app/src/main/java/com/brainwallet/constants/BWConstants.kt +++ b/app/src/main/java/com/brainwallet/constants/BWConstants.kt @@ -50,6 +50,8 @@ object BWConstants { const val PUT_EMOJIS_REQUEST_CODE: Int = 121 + const val CLEAR_EMOJIS_REQUEST_CODE: Int = 122 + /** * Request codes for taking pictures */ diff --git a/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java b/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java index bc0b7af4..029a11e9 100644 --- a/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java +++ b/app/src/main/java/com/brainwallet/tools/security/BRKeyStore.java @@ -275,7 +275,7 @@ public synchronized static boolean putEmojis(byte[] emojisStringToStore, Context throw new UserNotAuthenticatedException(); } AliasObject obj = aliasObjectMap.get(EMOJIS_ALIAS); - return !(emojisStringToStore == null || emojisStringToStore.length == 0) && + return !(emojisStringToStore == null) && _setData(context, emojisStringToStore, obj.alias, obj.datafileName, obj.ivFileName, requestCode, false); } public synchronized static byte[] getEmojis(final Context context, int requestCode) throws UserNotAuthenticatedException { diff --git a/app/src/main/java/com/brainwallet/tools/security/PostAuth.java b/app/src/main/java/com/brainwallet/tools/security/PostAuth.java index 203de32b..1619ca45 100644 --- a/app/src/main/java/com/brainwallet/tools/security/PostAuth.java +++ b/app/src/main/java/com/brainwallet/tools/security/PostAuth.java @@ -25,7 +25,10 @@ import com.platform.entities.TxMetaData; import com.platform.tools.KVStoreManager; +import java.text.BreakIterator; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import timber.log.Timber; @@ -92,9 +95,12 @@ public void onShowEmojis(Activity app, boolean authAsked) { try { byte[] raw = BRKeyStore.getEmojis(app, BWConstants.SHOW_EMOJIS_REQUEST_CODE); if (raw == null) { - NullPointerException ex = new NullPointerException("onShowEmojis: getEmojis = null"); - Timber.e(ex); - throw ex; + LegacyNavigation.openComposeScreen( + app, + Route.EmojiPickerPager.INSTANCE + ); + app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); + return; } cleanEmojis = new String(raw); } catch (UserNotAuthenticatedException e) { @@ -106,14 +112,29 @@ public void onShowEmojis(Activity app, boolean authAsked) { return; } - String[] emojis = cleanEmojis.split(","); + List emojiList = splitIntoGraphemeClusters(cleanEmojis); + LegacyNavigation.openComposeScreen( app, - new Route.YourSeedWords(Arrays.asList(emojis)) + new Route.YourEmojis(emojiList) ); app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); } + private static List splitIntoGraphemeClusters(String text) { + List clusters = new ArrayList<>(); + BreakIterator boundary = BreakIterator.getCharacterInstance(); + boundary.setText(text); + int start = boundary.first(); + int end = boundary.next(); + while (end != BreakIterator.DONE) { + clusters.add(text.substring(start, end)); + start = end; + end = boundary.next(); + } + return clusters; + } + public void onPhraseProveAuth(Activity app, boolean authAsked) { String cleanPhrase; try { diff --git a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt index b238ce97..815f337f 100644 --- a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt +++ b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt @@ -135,6 +135,9 @@ class BrainwalletActivity : SettingsViewModel.LEGACY_EFFECT_ON_SEED_PHRASE -> { PostAuth.getInstance().onPhraseCheckAuth(this@BrainwalletActivity, true) } + SettingsViewModel.LEGACY_EFFECT_ON_BRAINWALLET_PHRASE -> { + PostAuth.getInstance().onShowEmojis(this@BrainwalletActivity, true) + } SettingsViewModel.LEGACY_EFFECT_ON_SHARE_ANALYTICS_DATA_TOGGLE -> { val current = BRSharedPrefs.getShareData(this) BRSharedPrefs.putShareData(this, !current) diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt index e79765a4..607a974f 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -67,6 +68,10 @@ fun PickEmojisScreen( var activeSlot by remember { mutableStateOf(null) } var shouldShowSeedWords by remember { mutableStateOf(false) } + fun updateEmojiState(): Boolean { + return selectedEmojis.toSet().size == selectedEmojis.size + } + Box( modifier = modifier .fillMaxSize() @@ -77,42 +82,46 @@ fun PickEmojisScreen( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - Row(modifier = Modifier.height(44.dp)) { - IconButton( - onClick = { onEmojisPicked(false) }, - ) { - Icon( - painter = painterResource(id = com.brainwallet.R.drawable.btn_clear), - contentDescription = stringResource(R.string.clear), - tint = Color.White - ) + Box( + modifier = Modifier + .padding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + Row(modifier = Modifier) { + IconButton( + onClick = { onEmojisPicked(false) }, + ) { + Icon( + painter = painterResource(id = com.brainwallet.R.drawable.btn_clear), + contentDescription = stringResource(R.string.clear), + tint = Color.White + ) + } + Spacer(modifier = Modifier.weight(1f)) } - Spacer(modifier = Modifier.weight(1f)) - } - Row { - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 30.dp, end = 30.dp) - .padding(bottom = 10.dp), - text = stringResource(R.string.pick_emojis_title), - style = TextStyle( - fontFamily = IBMPlexSans, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Center, - fontSize = 22.sp, - color = Color.White - ), - maxLines = 2 - ) + Row { + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 30.dp, end = 30.dp), + text = stringResource(R.string.pick_emojis_title), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + fontSize = 22.sp, + color = Color.White + ), + ) + } } Text( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(start = 30.dp, end = 30.dp) - .padding(top = 5.dp, bottom = 5.dp), + .padding(top = 2.dp, bottom = 2.dp), text = stringResource(R.string.pick_emojis_description), style = TextStyle( fontFamily = IBMPlexSans, @@ -121,7 +130,7 @@ fun PickEmojisScreen( fontSize = 16.sp, color = Color.White ), - maxLines = 2 + maxLines = 4 ) Spacer(modifier = Modifier.height(8.dp)) @@ -187,7 +196,7 @@ fun PickEmojisScreen( Box( modifier = Modifier .fillMaxWidth() - .weight(0.6f) + .fillMaxHeight(0.7f) .padding(start = 16.dp, end = 16.dp) .clip(DesignTheme.shapes.large) .background(brush = bentoEmojiSectionGradient) @@ -227,7 +236,7 @@ fun PickEmojisScreen( enabled = true, colors = ButtonDefaults.filledTonalButtonColors( containerColor = Color.White, - contentColor = if (selectedEmojis.all { it != null }) { + contentColor = if (updateEmojiState()) { colorMidnite } else { colorMidnite.copy( @@ -236,7 +245,7 @@ fun PickEmojisScreen( } ), onClick = { - if (selectedEmojis.all { it != null }) { + if (updateEmojiState()) { val joined = selectedEmojis.joinToString(separator = "") val bytes = joined.toByteArray(Charsets.UTF_8) BRKeyStore.putEmojis( @@ -376,6 +385,5 @@ private fun EmojiSlot( } } } - Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt index 976c1fbf..f028a891 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt @@ -1,14 +1,21 @@ @file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) package com.brainwallet.ui.screens.emojis +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -24,6 +31,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -33,15 +42,19 @@ import com.brainwallet.navigation.UiEffect import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton -import com.brainwallet.ui.composable.SeedWordItem -import com.brainwallet.ui.composable.SeedWordsLayout +import com.brainwallet.ui.screens.main.MainScreenEvent +import com.brainwallet.ui.screens.main.MainViewModel +import com.brainwallet.ui.theme.DesignTheme +import com.brainwallet.ui.theme.IBMPlexSans import kotlinx.collections.immutable.ImmutableList +import org.koin.compose.koinInject @Composable fun YourEmojisScreen( onNavigate: OnNavigate, emojis: ImmutableList, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: MainViewModel = koinInject() ) { val columnPadding = 12 val horizontalVerticalSpacing = 8 @@ -91,13 +104,14 @@ fun YourEmojisScreen( Spacer(modifier = Modifier.weight(0.1f)) - SeedWordsLayout(modifier = Modifier.weight(1f)) { + EmojisLayout(modifier = Modifier.weight(1f)) { itemsIndexed(items = emojis) { index, emoji -> - SeedWordItem( + EmojiItem( modifier = Modifier .fillMaxWidth() .weight(1f), - label = "${index + 1} $emoji" + indexLabel = "${index + 1}", + emoji = emoji ) } } @@ -106,7 +120,7 @@ fun YourEmojisScreen( LargeButton( onClick = { - // viewModel.onEvent(YourSeedWordsEvent.OnSavedItClick(seedWords)) + viewModel.onEvent(MainScreenEvent.OnUserClearsEmojis) }, ) { Text( @@ -118,3 +132,60 @@ fun YourEmojisScreen( } } } + +@Composable +fun EmojisLayout( + modifier: Modifier = Modifier, + content: LazyGridScope.() -> Unit +) { + LazyVerticalGrid( + modifier = modifier + .height(220.dp), + columns = GridCells.Fixed(3), // fixed 3 + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content + ) +} + +@Composable +fun EmojiItem( + indexLabel: String, + emoji: String, + modifier: Modifier = Modifier + +) { + Box( + modifier = Modifier + .background( + color = DesignTheme.colors.background.copy(alpha = 0.3f), + shape = MaterialTheme.shapes.extraLarge + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + contentAlignment = Alignment.Center + + ) { + Text( + text = indexLabel, + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = 4.dp, y = (-2).dp), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + fontSize = 11.sp + ), + color = DesignTheme.colors.content, + ) + + Text( + text = emoji, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + fontSize = 26.sp + ), + ) + } +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt index c69d576b..759803c5 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreenEvent.kt @@ -17,6 +17,8 @@ sealed class MainScreenEvent { data class OnCopyTransactions(val transactionItem: TxItem) : MainScreenEvent() data object OnToggleGameHub : MainScreenEvent() + data object OnUserClearsEmojis : MainScreenEvent() + data object OnUserChoosesEmojis : MainScreenEvent() data class OnPostSocial( diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt index f4cdac9b..f8ffa304 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt @@ -185,7 +185,7 @@ class MainViewModel( val address = BRSharedPrefs.getReceiveAddress(app) val timestamp = java.util.Date().time val userLanguage = settingRepository.getCurrentLanguage().code - val emojis = runCatching { BRKeyStore.getEmojis(app, 0) } + val emojis = runCatching { BRKeyStore.getEmojis(app, BWConstants.SHOW_EMOJIS_REQUEST_CODE) } .getOrNull()?.decodeToString() ?: "NO_EMOJIS" val emojiArray = JSONArray().apply { @@ -511,6 +511,16 @@ class MainViewModel( } is MainScreenEvent.OnUserChoosesEmojis -> { } + is MainScreenEvent.OnUserClearsEmojis -> { + BRSharedPrefs.putEmojisChosen(this.app, false) + val emptyBytes = "".toByteArray(Charsets.UTF_8) + + BRKeyStore.putEmojis( + emptyBytes, + this.app, + BWConstants.PUT_EMOJIS_REQUEST_CODE + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 50a2c635..4d1a4766 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,7 +494,7 @@ Shop Buy Gift Cards with LTC! Pick Emojis - Tap a box. Show or hide words to choose your emojis. Tap an emoji. + Tap a box. Show or hide words to choose your emojis. Tap an emoji. Each emoji must be different. Choose your 3 emojis for the game Here is where you build your real brainwallet. Memorize your seed phrase through gaming. Pick your first 3 emojis From 7ad8255fa9cd64b9c39dda1dcec00d9e9e538bdb Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Tue, 14 Jul 2026 18:57:05 +0100 Subject: [PATCH 3/4] Add label in button --- .../java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt | 2 +- app/src/main/res/values/strings.xml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt index 607a974f..207337a0 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt @@ -267,7 +267,7 @@ fun PickEmojisScreen( text = if (selectedEmojis.all { it != null }) { stringResource(R.string.lets_play_label) } else { - stringResource(R.string.pick_emojis_title) + stringResource(R.string.pick_emojis_button_title) }, style = TextStyle( fontFamily = IBMPlexSans, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d1a4766..5f5ede36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -493,7 +493,8 @@ Scan or paste the destination address. Double check! Change your local fiat to figure out how much you can send. Add a memo as a handy reminder. Tap Continue. Shop Buy Gift Cards with LTC! - Pick Emojis + Pick 3 Emojis + Pick 3 Unique Emojis Tap a box. Show or hide words to choose your emojis. Tap an emoji. Each emoji must be different. Choose your 3 emojis for the game Here is where you build your real brainwallet. Memorize your seed phrase through gaming. From 386f4f900641aa7a88e1a569a54b9221e92de5a7 Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Thu, 16 Jul 2026 11:34:29 +0100 Subject: [PATCH 4/4] Version bump --- app/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb9ddf00..779f1af1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,8 +31,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 35 - versionCode = 202506340 - versionName = "v4.10.2" + versionCode = 202506341 + versionName = "v4.10.3" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -241,6 +241,7 @@ dependencies { testImplementation("io.mockk:mockk:1.13.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") testImplementation("app.cash.turbine:turbine:1.0.0") + implementation("androidx.core:core-splashscreen:1.0.1") androidTestImplementation(platform(grunt.androidx.compose.bom)) androidTestImplementation("androidx.test:core-ktx:1.5.0") androidTestImplementation(grunt.bundles.androidx.compose.ui.test)