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..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" @@ -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,17 +228,20 @@ 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") + 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) @@ -260,6 +262,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..fe413c28 100644 --- a/app/src/main/java/com/brainwallet/constants/BWConstants.kt +++ b/app/src/main/java/com/brainwallet/constants/BWConstants.kt @@ -46,6 +46,11 @@ 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 + + const val CLEAR_EMOJIS_REQUEST_CODE: Int = 122 /** * Request codes for taking pictures @@ -71,6 +76,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..029a11e9 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) && + _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..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; @@ -87,6 +90,51 @@ 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) { + 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) { + if (authAsked) { + Timber.d("timber: %s: WARNING!!!! LOOP", new Object() { + }.getClass().getEnclosingMethod().getName()); + isStuckWithAuthLoop = true; + } + return; + } + + List emojiList = splitIntoGraphemeClusters(cleanEmojis); + + LegacyNavigation.openComposeScreen( + app, + 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 04c67175..815f337f 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 @@ -162,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) @@ -219,62 +195,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 +237,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 +258,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..207337a0 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/PickEmojisScreen.kt @@ -0,0 +1,389 @@ +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.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.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) } + + fun updateEmojiState(): Boolean { + return selectedEmojis.toSet().size == selectedEmojis.size + } + + Box( + modifier = modifier + .fillMaxSize() + .background(brush = bentoModalDarkGradient) + ) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + 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)) + } + + 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 = 2.dp, bottom = 2.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 = 4 + ) + + 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() + .fillMaxHeight(0.7f) + .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 (updateEmojiState()) { + colorMidnite + } else { + colorMidnite.copy( + alpha = 0.3f + ) + } + ), + onClick = { + if (updateEmojiState()) { + 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_button_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) + ) + } + } + } + } +} 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 new file mode 100644 index 00000000..f028a891 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/emojis/YourEmojisScreen.kt @@ -0,0 +1,191 @@ +@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 +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +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.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, + viewModel: MainViewModel = koinInject() +) { + val columnPadding = 12 + val horizontalVerticalSpacing = 8 + val leadingCopyPadding = 8 + val detailLineHeight = 24 + + BrainwalletScaffold( + topBar = { + BrainwalletTopAppBar( + navigationIcon = { + IconButton( + onClick = { onNavigate.invoke(UiEffect.Navigate.Back()) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(columnPadding.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), + ) { + Text( + text = stringResource(R.string.your_emojis_title), + style = MaterialTheme.typography.headlineSmall, + ) + + 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)) + + EmojisLayout(modifier = Modifier.weight(1f)) { + itemsIndexed(items = emojis) { index, emoji -> + EmojiItem( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + indexLabel = "${index + 1}", + emoji = emoji + ) + } + } + + Spacer(modifier = Modifier.weight(0.1f)) + + LargeButton( + onClick = { + viewModel.onEvent(MainScreenEvent.OnUserClearsEmojis) + }, + ) { + Text( + text = stringResource(R.string.your_emojis_reset), + style = MaterialTheme.typography.bodyLarge + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@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/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..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,10 @@ 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( 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..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 @@ -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 { @@ -178,16 +185,31 @@ 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 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,18 @@ class MainViewModel( Timber.e(e) } } + 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/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/YourEmojisScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisScreen.kt deleted file mode 100644 index f73155c8..00000000 --- a/app/src/main/java/com/brainwallet/ui/screens/youremojis/YourEmojisScreen.kt +++ /dev/null @@ -1,102 +0,0 @@ -@file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) - -package com.brainwallet.ui.screens.youremojis - -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.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -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.res.stringResource -import androidx.compose.ui.unit.dp -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 kotlinx.collections.immutable.ImmutableList -import org.koin.compose.koinInject - -@Composable -fun YourEmojisScreen( - onNavigate: OnNavigate, - emojis: ImmutableList, - modifier: Modifier = Modifier, - viewModel: YourEmojisViewModel = koinInject() -) { - // / 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( - navigationIcon = { - IconButton( - onClick = { onNavigate.invoke(UiEffect.Navigate.Back()) }, - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back), - ) - } - } - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(columnPadding.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - ) { - Text( - text = stringResource(R.string.your_seed_words), - style = MaterialTheme.typography.headlineSmall, - ) - - Spacer(modifier = Modifier.weight(0.2f)) - - LargeButton( - onClick = { - viewModel.onEvent(YourEmojisEvent.OnSavedItClick(emojis)) - }, - ) { - Text( - text = stringResource(R.string.i_saved_it_on_paper), - style = MaterialTheme.typography.bodyLarge - ) - } - } - } -} 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..5f5ede36 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,19 @@ 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 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. + 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