ci(release): publish latest release

parent 3defbbd8
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC` - CIDv0: `QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx`
- CIDv1: `bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu` - CIDv1: `bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,15 +10,68 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,15 +10,68 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu.ipfs.dweb.link/ - https://bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu.ipfs.dweb.link/
- https://bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu.ipfs.cf-ipfs.com/ - https://bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu.ipfs.cf-ipfs.com/
- [ipfs://QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC/](ipfs://QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC/) - [ipfs://QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx/](ipfs://QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx/)
## 5.33.0 (2024-06-10) ## 5.34.0 (2024-06-12)
### Features ### Features
* **web:** add quoteId to Xv2 rfq POST (#8908) 6fd2f1e * **web:** [multichain] 4070 add chain approval to swap execution flow (#8284) f645d37
* **web:** [multichain] 4101 update chain id in swap and limit context (#8363) 83cf952
* **web:** [multichain] 4121 add switch chain to send execution flow (#8060) a40162b
* **web:** [multichain] 4127 fallback to gql (token balances) when chain is not synced (#8059) 9848242
* **web:** 4194 use correct native currency in send (#8446) dbff9f3
* **web:** 4194 use correct native currency in swap (#8448) 27a34cd
* **web:** 4250 - add UniverseChainInfo and UniverseChainId types (#8632) 5c6208a
* **web:** 4250 - create UNIVERSE_CHAIN_INFO and mainnet property (#8633) db60dc7
* **web:** 4250 - move RPCType and DEFAULT_NATIVE_ADDRESS to uniswap pkg (#8634) f578d39
* **web:** 4250 - pull all chain info from universe (#8643) d76fbf2
* **web:** 4250 - use mainnet from UniverseChainInfo on wallet (#8636) 2161df6
* **web:** 4250 - use mainnet from UniverseChainInfo on web (#8635) 2864084
* **web:** Add base to supported moonpay chains (#8805) d06d5cb
* **web:** add infringing LV nft collections (#8719) 095e89c
* **web:** Add Moonpay deeplink (#8806) 81e6971
* **web:** add quoteId to Xv2 rfq POST (#8909) 54a936b
* **web:** add unauthenticated FOR transaction fetcher to uniswap package (#8587) 3a6ccfe
* **web:** adding zora (#8711) e99544c
* **web:** FOR - share token picker button (#8741) 3048ad2
* **web:** migrate off direct console logging (#8334) e5f9873
* **web:** move basic sharable ForAggregator API calls to uniswap package (#8553) fa54d49
* **web:** move Pill component to shared uniswap package (#8701) 4806ede
* **web:** Refreshed nav "Get Started" modal (#8642) 6a0e15d
* **web:** Refreshed nav better app layout (#8625) 4b23b54
* **web:** Refreshed nav chain selector dropdown (#8627) b5d092c
* **web:** Refreshed nav company menu dropdown (#8641) b7b2244
* **web:** Refreshed nav preferences menu and theme toggle (#8628) 9d43d7d
* **web:** Refreshed nav prep work, organize files (#8622) 9c8b5f7
* **web:** Replace direct thegraph queries with new BE queries (#8626) 5081b54
* **web:** share country picker component for FOR (#8702) 3800c04
### Bug Fixes
* **web:** [multichain] token selector in TDP should use connected chain (#8963) 6ef7dbc
* **web:** AccountDrawer still showing in in-app browser on scroll when closed (#8623) ae322cb
* **web:** can't switch to mainnet if first pageload has chain query param (#8684) d2e3bfb
* **web:** connector may be undefined on landing page (#8822) 9323bd4
* **web:** dialog button should no longer be disabled (#8849) 56285db
* **web:** don't disable swap settings for unconnected chains (#8651) 141b089
* **web:** fix missing translation, despite matching the library doesnt work with fee.bestForStable (#8629) 9470c59
* **web:** liquidity translations (#8821) 4744be8
* **web:** Pass account to send in transferInfo - staging (#8902) 786dead
* **web:** patch wagmi to fix MM bug (#8841) 834a26c
* **web:** Remove duplicate app promo banners (#8646) b35c3c1
* **web:** removing zora from network selector when feature flag is off (#8960) e1eb12d
* **web:** update useOnClickOutside to handle tooltips (#8704) 3697c7e
* **web:** use address, chainId, from useAccount instead of web3-react (#8513) 26bc9fb
* **web:** use orders text in CancelLimitDialog (#8706) 3fb4151
### Continuous Integration
* **web:** update sitemaps f972a73
web/5.33.0 web/5.34.0
\ No newline at end of file \ No newline at end of file
...@@ -131,17 +131,17 @@ android { ...@@ -131,17 +131,17 @@ android {
dev { dev {
isDefault(true) isDefault(true)
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionName "1.28" versionName "1.29"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.28" versionName "1.29"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.28" versionName "1.29"
} }
} }
......
...@@ -20,6 +20,15 @@ ...@@ -20,6 +20,15 @@
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<meta-data
android:name="com.onesignal.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_onesignal_default" />
<meta-data
android:name="com.onesignal.NotificationAccentColor.DEFAULT"
android:value="@string/notification_accent_color" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
......
...@@ -7,6 +7,10 @@ import com.facebook.react.bridge.WritableArray ...@@ -7,6 +7,10 @@ import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.module.annotations.ReactModule import com.facebook.react.module.annotations.ReactModule
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
/** /**
* Bridge between the React Native JavaScript code and the native Android code. * Bridge between the React Native JavaScript code and the native Android code.
...@@ -18,6 +22,7 @@ import com.facebook.soloader.SoLoader ...@@ -18,6 +22,7 @@ import com.facebook.soloader.SoLoader
class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val ethersRs: RnEthersRs = RnEthersRs(reactContext.applicationContext) private val ethersRs: RnEthersRs = RnEthersRs(reactContext.applicationContext)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// Needs to be initialized form a static context // Needs to be initialized form a static context
companion object { companion object {
...@@ -40,6 +45,12 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase ...@@ -40,6 +45,12 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase
promise.resolve(ethersRs.importMnemonic(mnemonic)) promise.resolve(ethersRs.importMnemonic(mnemonic))
} }
@ReactMethod fun removeMnemonic(mnemonicId: String, promise: Promise) {
scope.launch(Dispatchers.IO) {
val result = ethersRs.removeMnemonic(mnemonicId)
promise.resolve(result)
}
}
@ReactMethod fun generateAndStoreMnemonic(promise: Promise) { @ReactMethod fun generateAndStoreMnemonic(promise: Promise) {
promise.resolve(ethersRs.generateAndStoreMnemonic()) promise.resolve(ethersRs.generateAndStoreMnemonic())
...@@ -65,6 +76,13 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase ...@@ -65,6 +76,13 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase
promise.resolve(ethersRs.generateAndStorePrivateKey(mnemonicId, derivationIndex)) promise.resolve(ethersRs.generateAndStorePrivateKey(mnemonicId, derivationIndex))
} }
@ReactMethod fun removePrivateKey(address: String, promise: Promise) {
scope.launch(Dispatchers.IO) {
val result = ethersRs.removePrivateKey(address)
promise.resolve(result)
}
}
@ReactMethod fun signTransactionHashForAddress(address: String, hash: String, chainId: Int, promise: Promise) { @ReactMethod fun signTransactionHashForAddress(address: String, hash: String, chainId: Int, promise: Promise) {
promise.resolve(ethersRs.signTransactionHashForAddress(address, hash, chainId.toLong())) promise.resolve(ethersRs.signTransactionHashForAddress(address, hash, chainId.toLong()))
} }
......
...@@ -10,6 +10,11 @@ import com.uniswap.EthersRs.signHashWithWallet ...@@ -10,6 +10,11 @@ import com.uniswap.EthersRs.signHashWithWallet
import com.uniswap.EthersRs.signMessageWithWallet import com.uniswap.EthersRs.signMessageWithWallet
import com.uniswap.EthersRs.signTxWithWallet import com.uniswap.EthersRs.signTxWithWallet
import com.uniswap.EthersRs.walletFromPrivateKey import com.uniswap.EthersRs.walletFromPrivateKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class RnEthersRs(applicationContext: Context) { class RnEthersRs(applicationContext: Context) {
...@@ -82,6 +87,11 @@ class RnEthersRs(applicationContext: Context) { ...@@ -82,6 +87,11 @@ class RnEthersRs(applicationContext: Context) {
return keychain.getString(keychainKeyForMnemonicId(mnemonicId), null) return keychain.getString(keychainKeyForMnemonicId(mnemonicId), null)
} }
suspend fun removeMnemonic(mnemonicId: String): Boolean {
keychain.edit().remove(keychainKeyForMnemonicId(mnemonicId)).apply()
return true
}
val addressesForStoredPrivateKeys: List<String> val addressesForStoredPrivateKeys: List<String>
get() = keychain.all.keys get() = keychain.all.keys
.filter { key -> key.contains(PRIVATE_KEY_PREFIX) } .filter { key -> key.contains(PRIVATE_KEY_PREFIX) }
...@@ -118,6 +128,11 @@ class RnEthersRs(applicationContext: Context) { ...@@ -118,6 +128,11 @@ class RnEthersRs(applicationContext: Context) {
return address return address
} }
suspend fun removePrivateKey(address: String): Boolean {
keychain.edit().remove(keychainKeyForPrivateKey(address)).apply()
return true
}
/** /**
* Signs a transaction for a given address. * Signs a transaction for a given address.
* @param address The address to sign the transaction for. * @param address The address to sign the transaction for.
......
...@@ -6,8 +6,8 @@ import com.facebook.react.bridge.NativeModule ...@@ -6,8 +6,8 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManager
import com.uniswap.onboarding.backup.MnemonicDisplayViewManager
import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager
import com.uniswap.onboarding.backup.MnemonicDisplayViewManager
import com.uniswap.onboarding.import.SeedPhraseInputViewManager import com.uniswap.onboarding.import.SeedPhraseInputViewManager
class UniswapPackage : ReactPackage { class UniswapPackage : ReactPackage {
...@@ -24,6 +24,7 @@ class UniswapPackage : ReactPackage { ...@@ -24,6 +24,7 @@ class UniswapPackage : ReactPackage {
): List<NativeModule> = listOf( ): List<NativeModule> = listOf(
AndroidDeviceModule(reactContext), AndroidDeviceModule(reactContext),
RNEthersRSModule(reactContext), RNEthersRSModule(reactContext),
ThemeModule(reactContext), ThemeModule(reactContext),
) )
} }
...@@ -28,19 +28,30 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() { ...@@ -28,19 +28,30 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() {
override fun getName(): String = REACT_CLASS override fun getName(): String = REACT_CLASS
private val mnemonicIdFlow = MutableStateFlow("") private val mnemonicIdFlow = MutableStateFlow("")
private val shouldShowSmallTextFlow = MutableStateFlow(false)
private val selectedWordPlaceholderFlow = MutableStateFlow("")
override fun createViewInstance(reactContext: ThemedReactContext): ComposeView { override fun createViewInstance(reactContext: ThemedReactContext): ComposeView {
val ethersRs = RnEthersRs(reactContext) val ethersRs = RnEthersRs(reactContext)
val viewModel = MnemonicConfirmationViewModel(ethersRs) val viewModel = MnemonicConfirmationViewModel(ethersRs)
return ComposeView(reactContext).apply { return ComposeView(reactContext).apply {
id = R.id.mnemonic_confirmation_compose_id // Needed for RN event emitter id = R.id.mnemonic_confirmation_compose_id // Needed for RN event emitter
setContent { setContent {
val mnemonicId by mnemonicIdFlow.collectAsState() val mnemonicId by mnemonicIdFlow.collectAsState()
val shouldShowSmallText by shouldShowSmallTextFlow.collectAsState()
val selectedWordPlaceholder by selectedWordPlaceholderFlow.collectAsState()
viewModel.updatePlaceholder(selectedWordPlaceholder)
UniswapComponent { UniswapComponent {
MnemonicConfirmation(mnemonicId = mnemonicId, viewModel = viewModel) { MnemonicConfirmation(
val reactContext = context as ReactContext mnemonicId = mnemonicId,
viewModel = viewModel,
shouldShowSmallText = shouldShowSmallText,
) {
context as ReactContext
reactContext reactContext
.getJSModule(RCTEventEmitter::class.java) .getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, EVENT_COMPLETED, null) // Sends event to RN bridge .receiveEvent(id, EVENT_COMPLETED, null) // Sends event to RN bridge
...@@ -70,6 +81,16 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() { ...@@ -70,6 +81,16 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() {
mnemonicIdFlow.update { mnemonicId } mnemonicIdFlow.update { mnemonicId }
} }
@ReactProp(name = "shouldShowSmallText")
fun setShouldShowSmallText(view: View, shouldShowSmallText: Boolean) {
shouldShowSmallTextFlow.update { shouldShowSmallText }
}
@ReactProp(name = "selectedWordPlaceholder")
fun setSelectedWordPlaceholder(view: View, selectedWordPlaceholder: String) {
selectedWordPlaceholderFlow.update { selectedWordPlaceholder }
}
companion object { companion object {
private const val REACT_CLASS = "MnemonicConfirmation" private const val REACT_CLASS = "MnemonicConfirmation"
private const val EVENT_COMPLETED = "onConfirmComplete" private const val EVENT_COMPLETED = "onConfirmComplete"
......
...@@ -4,9 +4,13 @@ import android.view.View ...@@ -4,9 +4,13 @@ import android.view.View
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter
import com.uniswap.RnEthersRs import com.uniswap.RnEthersRs
import com.uniswap.onboarding.backup.ui.MnemonicDisplay import com.uniswap.onboarding.backup.ui.MnemonicDisplay
import com.uniswap.onboarding.backup.ui.MnemonicDisplayViewModel import com.uniswap.onboarding.backup.ui.MnemonicDisplayViewModel
...@@ -22,29 +26,83 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() { ...@@ -22,29 +26,83 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() {
override fun getName(): String = REACT_CLASS override fun getName(): String = REACT_CLASS
private lateinit var context: ThemedReactContext
private val mnemonicIdFlow = MutableStateFlow("") private val mnemonicIdFlow = MutableStateFlow("")
private val copyTextFlow = MutableStateFlow("")
private val copiedTextFlow = MutableStateFlow("")
override fun createViewInstance(reactContext: ThemedReactContext): ComposeView { override fun createViewInstance(reactContext: ThemedReactContext): ComposeView {
context = reactContext
val ethersRs = RnEthersRs(reactContext) val ethersRs = RnEthersRs(reactContext)
val viewModel = MnemonicDisplayViewModel(ethersRs) val viewModel = MnemonicDisplayViewModel(ethersRs)
return ComposeView(reactContext).apply { return ComposeView(reactContext).apply {
setContent { setContent {
val mnemonicId by mnemonicIdFlow.collectAsState() val mnemonicId by mnemonicIdFlow.collectAsState()
val copyText by copyTextFlow.collectAsState()
val copiedText by copiedTextFlow.collectAsState()
UniswapComponent { UniswapComponent {
MnemonicDisplay(mnemonicId = mnemonicId, viewModel = viewModel) MnemonicDisplay(
mnemonicId = mnemonicId,
viewModel = viewModel,
copyText = copyText,
copiedText = copiedText,
onHeightMeasured = {
val bundle = Arguments.createMap().apply {
putDouble(FIELD_HEIGHT, it.toDouble())
}
sendEvent(id, EVENT_HEIGHT_MEASURED, bundle)
}
)
} }
} }
} }
} }
/**
* Maps local event name to expected RN prop. See RN [ViewManager] docs on github for schema.
* Using bubbling instead of direct events because overriding
* getExportedCustomDirectEventTypeConstants leads to a component not found error for some reason.
* Direct events will try find callback prop on native component, and bubbled events will bubble
* up until it finds component with the right callback prop.
*/
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf(
EVENT_HEIGHT_MEASURED to mapOf(
"phasedRegistrationNames" to mapOf(
"bubbled" to EVENT_HEIGHT_MEASURED,
"captured" to EVENT_HEIGHT_MEASURED
)
)
)
}
private fun sendEvent(id: Int, eventName: String, bundle: WritableMap? = null) {
context
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, eventName, bundle)
}
@ReactProp(name = "mnemonicId") @ReactProp(name = "mnemonicId")
fun setMnemonicId(view: View, mnemonicId: String) { fun setMnemonicId(view: View, mnemonicId: String) {
mnemonicIdFlow.update { mnemonicId } mnemonicIdFlow.update { mnemonicId }
} }
@ReactProp(name = "copyText")
fun setCopyText(view: View, copyText: String) {
copyTextFlow.update { copyText }
}
@ReactProp(name = "copiedText")
fun setCopiedText(view: View, copiedText: String) {
copiedTextFlow.update { copiedText }
}
companion object { companion object {
private const val REACT_CLASS = "MnemonicDisplay" private const val REACT_CLASS = "MnemonicDisplay"
private const val EVENT_HEIGHT_MEASURED = "onHeightMeasured"
private const val FIELD_HEIGHT = "height"
} }
} }
...@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column ...@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
...@@ -13,7 +12,6 @@ import androidx.compose.runtime.LaunchedEffect ...@@ -13,7 +12,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.uniswap.theme.UniswapTheme import com.uniswap.theme.UniswapTheme
/** /**
...@@ -24,10 +22,11 @@ import com.uniswap.theme.UniswapTheme ...@@ -24,10 +22,11 @@ import com.uniswap.theme.UniswapTheme
fun MnemonicConfirmation( fun MnemonicConfirmation(
viewModel: MnemonicConfirmationViewModel, viewModel: MnemonicConfirmationViewModel,
mnemonicId: String, mnemonicId: String,
shouldShowSmallText: Boolean,
onCompleted: () -> Unit, onCompleted: () -> Unit,
) { ) {
val displayedWords by viewModel.displayWords.collectAsState() val displayedWords by viewModel.selectedWords.collectAsState()
val wordBankList by viewModel.wordBankList.collectAsState() val wordBankList by viewModel.wordBankList.collectAsState()
val completed by viewModel.completed.collectAsState() val completed by viewModel.completed.collectAsState()
...@@ -41,28 +40,20 @@ fun MnemonicConfirmation( ...@@ -41,28 +40,20 @@ fun MnemonicConfirmation(
} }
} }
BoxWithConstraints {
BoxWithConstraints(
modifier = Modifier.padding(horizontal = UniswapTheme.spacing.spacing16)
) {
val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MnemonicWordsGroup(words = displayedWords, showCompact = showCompact) { MnemonicWordsGroup(
viewModel.handleWordRowClick(it) words = displayedWords,
} shouldShowSmallText = shouldShowSmallText,
)
Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing24)) Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing24))
MnemonicWordBank(words = wordBankList, showCompact = showCompact) { MnemonicWordBank(words = wordBankList, shouldShowSmallText = shouldShowSmallText) {
viewModel.handleWordBankClick(it) viewModel.handleWordBankClick(it.index)
} }
} }
} }
} }
private const val SCREEN_HEIGHT_BREAKPOINT = 500
...@@ -3,6 +3,7 @@ package com.uniswap.onboarding.backup.ui ...@@ -3,6 +3,7 @@ package com.uniswap.onboarding.backup.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.uniswap.RnEthersRs import com.uniswap.RnEthersRs
import com.uniswap.onboarding.backup.ui.model.MnemonicInputStatus
import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState
import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
...@@ -10,38 +11,36 @@ import kotlinx.coroutines.flow.SharingStarted ...@@ -10,38 +11,36 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class MnemonicConfirmationViewModel( class MnemonicConfirmationViewModel(
private val ethersRs: RnEthersRs, // Move to repository layer if app gets more complex private val ethersRs: RnEthersRs, // Move to repository layer if app gets more complex
) : ViewModel() { ) : ViewModel() {
private val defaultMnemonicsCount = 12
private var sourceWords = emptyList<String>() private var sourceWords = List(defaultMnemonicsCount) { "" }
private var shuffledWords = emptyList<String>() private var shuffledWords = emptyList<String>()
private val focusedIndex = MutableStateFlow(0) private val focusedIndex = MutableStateFlow(0)
private val selectedWords = MutableStateFlow<List<String?>>(emptyList()) private val selectedWordsIndexes =
MutableStateFlow<List<Int?>>(List(defaultMnemonicsCount) { null })
val displayWords: StateFlow<List<MnemonicWordUiState>> = private val selectedWordPlaceholderFlow = MutableStateFlow("")
combine(focusedIndex, selectedWords) { focusedIndexValue, words ->
words.mapIndexed { index, word -> val selectedWords: StateFlow<List<MnemonicWordUiState>> =
MnemonicWordUiState( selectedWordsIndexes.combine(selectedWordPlaceholderFlow) { _, placeholder ->
num = index + 1, List(sourceWords.size) { index ->
text = word ?: "", getMnemonicWordUiState(index, placeholder)
focused = index == focusedIndexValue,
hasError = word != null && word != sourceWords[index],
)
} }
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val wordBankList: StateFlow<List<MnemonicWordBankCellUiState>> = val wordBankList: StateFlow<List<MnemonicWordBankCellUiState>> =
combine(focusedIndex, selectedWords) { focusedIndexValue, words -> selectedWordsIndexes.map { selectedWordsIndexes ->
val counter = words.groupingBy { it }.eachCount().toMutableMap() shuffledWords.mapIndexed { index, word ->
shuffledWords.map { shuffledWord ->
counter[shuffledWord] = counter.getOrDefault(shuffledWord, 0) - 1
MnemonicWordBankCellUiState( MnemonicWordBankCellUiState(
text = shuffledWord, index = index,
used = counter.getOrDefault(shuffledWord, -1) >= 0, text = word,
used = selectedWordsIndexes.contains(index),
) )
} }
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
...@@ -60,46 +59,74 @@ class MnemonicConfirmationViewModel( ...@@ -60,46 +59,74 @@ class MnemonicConfirmationViewModel(
val words = mnemonic.split(" ") val words = mnemonic.split(" ")
sourceWords = words sourceWords = words
shuffledWords = words.shuffled() shuffledWords = words.shuffled()
selectedWords.update { List(words.size) { null } } selectedWordsIndexes.update { List(words.size) { null } }
} }
} }
} }
private fun reset() { private fun reset() {
sourceWords = emptyList() sourceWords = List(defaultMnemonicsCount) { "" }
shuffledWords = emptyList() shuffledWords = emptyList()
focusedIndex.update { 0 } focusedIndex.update { 0 }
selectedWords.update { emptyList() } selectedWordsIndexes.update { emptyList() }
_completed.update { false } _completed.update { false }
} }
fun handleWordRowClick(word: MnemonicWordUiState) { fun updatePlaceholder(newPlaceholder: String) {
val index = displayWords.value.indexOf(word) selectedWordPlaceholderFlow.value = newPlaceholder
focusedIndex.update { index } }
fun handleWordBankClick(wordBankIndex: Int) {
selectedWordsIndexes.update { indexes ->
val updatedIndexes = indexes.toMutableList()
updatedIndexes[focusedIndex.value] = wordBankIndex
updatedIndexes
} }
fun handleWordBankClick(state: MnemonicWordBankCellUiState) { if (focusedIndex.value == sourceWords.size - 1) {
val focusedIndexValue = focusedIndex.value checkIfCompleted()
selectedWords.update { words -> } else if (sourceWords[focusedIndex.value] == shuffledWords[wordBankIndex] && focusedIndex.value < sourceWords.size - 1) {
val updated = words.mapIndexed { index, word -> focusedIndex.update { it + 1 }
if (index == focusedIndexValue) {
if (state.text == sourceWords[focusedIndexValue]) {
focusedIndex.update { focusedIndexValue + 1 }
} }
state.text
} else {
word
} }
private fun checkIfCompleted() {
if (selectedWordsIndexes.value.size != sourceWords.size) {
return
} }
checkIfCompleted(updated) for (i in selectedWordsIndexes.value.indices) {
updated val selectedWord = getSelectedWord(i)
if (sourceWords[i].isEmpty() || selectedWord != sourceWords[i]) {
return
} }
} }
private fun checkIfCompleted(words: List<String?>) {
if (sourceWords == words) {
_completed.update { true } _completed.update { true }
} }
private fun getSelectedWord(displayIndex: Int): String {
return selectedWordsIndexes.value.getOrNull(displayIndex)?.let { shuffledWords.getOrNull(it) }
?: ""
}
private fun getMnemonicWordUiState(
displayIndex: Int,
placeholderText: String
): MnemonicWordUiState {
val selectedWord = getSelectedWord(displayIndex)
var status = MnemonicInputStatus.CORRECT_INPUT
if (selectedWord.isEmpty()) {
status = MnemonicInputStatus.NO_INPUT
} else if (selectedWord != sourceWords[displayIndex]) {
status = MnemonicInputStatus.WRONG_INPUT
}
return MnemonicWordUiState(
num = displayIndex + 1,
text = selectedWord.ifEmpty { placeholderText },
status = status,
)
} }
} }
...@@ -3,50 +3,79 @@ package com.uniswap.onboarding.backup.ui ...@@ -3,50 +3,79 @@ package com.uniswap.onboarding.backup.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.uniswap.extensions.fadingEdges import com.uniswap.onboarding.shared.CopyButton
import com.uniswap.theme.UniswapTheme import com.uniswap.theme.relativeOffset
import kotlin.math.abs
@Composable @Composable
fun MnemonicDisplay( fun MnemonicDisplay(
viewModel: MnemonicDisplayViewModel, viewModel: MnemonicDisplayViewModel,
mnemonicId: String, mnemonicId: String,
copyText: String,
copiedText: String,
onHeightMeasured: (height: Float) -> Unit
) { ) {
val words by viewModel.words.collectAsState() val words by viewModel.words.collectAsState()
val textToCopy = AnnotatedString(words.joinToString(" ") { it.text })
val density = LocalDensity.current.density
var buttonOffset by remember { mutableStateOf(20.dp) }
LaunchedEffect(mnemonicId) { LaunchedEffect(mnemonicId) {
viewModel.setup(mnemonicId) viewModel.setup(mnemonicId)
} }
BoxWithConstraints { BoxWithConstraints {
val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp
val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.verticalScroll(scrollState) .wrapContentHeight()
.padding(horizontal = UniswapTheme.spacing.spacing16) .verticalScroll(rememberScrollState())
.fadingEdges(scrollState) .onSizeChanged { size ->
onHeightMeasured(size.height / density)
}
) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = buttonOffset)
.wrapContentSize(Alignment.Center)
) { ) {
MnemonicWordsGroup(words = words)
Box( Box(
modifier = Modifier modifier = Modifier
.padding(bottom = UniswapTheme.spacing.spacing16) .align(Alignment.TopCenter)
.relativeOffset(y = -0.5f) { _, offsetY ->
buttonOffset = (abs(offsetY) / density).dp
}
) { ) {
MnemonicWordsGroup(words = words, showCompact = showCompact) CopyButton(
copyButtonText = copyText,
copiedButtonText = copiedText,
textToCopy = textToCopy
)
}
} }
} }
} }
} }
private const val SCREEN_HEIGHT_BREAKPOINT = 347
...@@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.update ...@@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.update
class MnemonicDisplayViewModel( class MnemonicDisplayViewModel(
private val ethersRs: RnEthersRs // Move to repository layer if app gets more complex private val ethersRs: RnEthersRs // Move to repository layer if app gets more complex
) : ViewModel() { ) : ViewModel() {
private val defaultMnemonicsCount = 12
private val _words = MutableStateFlow<List<MnemonicWordUiState>>(emptyList()) private val _words =
MutableStateFlow(List(defaultMnemonicsCount) { MnemonicWordUiState(num = it + 1, text = "") })
val words = _words.asStateFlow() val words = _words.asStateFlow()
private var currentMnemonicId = "" private var currentMnemonicId = ""
......
package com.uniswap.onboarding.backup.ui package com.uniswap.onboarding.backup.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
...@@ -10,6 +11,8 @@ import androidx.compose.material.Text ...@@ -10,6 +11,8 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment import com.google.accompanist.flowlayout.MainAxisAlignment
import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState
...@@ -21,19 +24,19 @@ import com.uniswap.theme.UniswapTheme ...@@ -21,19 +24,19 @@ import com.uniswap.theme.UniswapTheme
@Composable @Composable
fun MnemonicWordBank( fun MnemonicWordBank(
words: List<MnemonicWordBankCellUiState>, words: List<MnemonicWordBankCellUiState>,
showCompact: Boolean = false, shouldShowSmallText: Boolean = false,
onClick: (word: MnemonicWordBankCellUiState) -> Unit onClick: (word: MnemonicWordBankCellUiState) -> Unit
) { ) {
FlowRow( FlowRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight(),
mainAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, mainAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
crossAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, crossAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
mainAxisAlignment = MainAxisAlignment.Center, mainAxisAlignment = MainAxisAlignment.Center,
) { ) {
words.forEach { words.forEach {
MnemonicWordBankCell(word = it, showCompact = showCompact) { MnemonicWordBankCell(word = it, shouldShowSmallText = shouldShowSmallText) {
onClick(it) onClick(it)
} }
} }
...@@ -43,29 +46,35 @@ fun MnemonicWordBank( ...@@ -43,29 +46,35 @@ fun MnemonicWordBank(
@Composable @Composable
private fun MnemonicWordBankCell( private fun MnemonicWordBankCell(
word: MnemonicWordBankCellUiState, word: MnemonicWordBankCellUiState,
showCompact: Boolean, shouldShowSmallText: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val textStyle = val textStyle =
if (showCompact) UniswapTheme.typography.body2 else UniswapTheme.typography.body1 if (shouldShowSmallText) UniswapTheme.typography.body3 else UniswapTheme.typography.body2
val verticalPadding = val verticalPadding =
if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8 if (shouldShowSmallText) UniswapTheme.spacing.spacing8 else 10.dp
val horizontalPadding = val horizontalPadding =
if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8 if (shouldShowSmallText) 10.dp else UniswapTheme.spacing.spacing12
Box( Box(
modifier = Modifier modifier = Modifier
.clip(UniswapTheme.shapes.xlarge) .shadow(
.background(UniswapTheme.colors.surface2) 10.dp,
.padding(vertical = verticalPadding) spotColor = UniswapTheme.colors.black.copy(alpha = 0.04f),
.padding(horizontal = horizontalPadding) shape = UniswapTheme.shapes.xlarge
.clickable { onClick() }, )
) { ) {
Box(modifier = Modifier
.clip(shape = UniswapTheme.shapes.xlarge)
.then(Modifier.border(1.dp, UniswapTheme.colors.surface3, UniswapTheme.shapes.xlarge))
.clickable { onClick() }
.background(color = UniswapTheme.colors.surface1)
.padding(vertical = verticalPadding, horizontal = horizontalPadding)) {
Text( Text(
text = word.text, text = word.text,
style = textStyle, style = textStyle,
color = UniswapTheme.colors.neutral1.copy(if (word.used) 0.6f else 1f), color = UniswapTheme.colors.neutral1.copy(if (word.used) 0.5f else 1f),
) )
} }
}
} }
package com.uniswap.onboarding.backup.ui package com.uniswap.onboarding.backup.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.backup.ui.model.MnemonicInputStatus
import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState
import com.uniswap.theme.UniswapTheme import com.uniswap.theme.UniswapTheme
...@@ -24,44 +17,31 @@ import com.uniswap.theme.UniswapTheme ...@@ -24,44 +17,31 @@ import com.uniswap.theme.UniswapTheme
*/ */
@Composable @Composable
fun MnemonicWordCell( fun MnemonicWordCell(
modifier: Modifier = Modifier,
word: MnemonicWordUiState, word: MnemonicWordUiState,
showCompact: Boolean = false, shouldShowSmallText: Boolean = false,
onClick: (() -> Unit)? = null,
) { ) {
val textStyle = val textStyle =
if (showCompact) UniswapTheme.typography.body2 else UniswapTheme.typography.body1 if (shouldShowSmallText) UniswapTheme.typography.body3 else UniswapTheme.typography.body2
val shape = UniswapTheme.shapes.large val textColor = when (word.status) {
var rowModifier = modifier MnemonicInputStatus.NO_INPUT -> UniswapTheme.colors.neutral3
.clip(shape) MnemonicInputStatus.CORRECT_INPUT -> UniswapTheme.colors.neutral1
.shadow(1.dp, shape) MnemonicInputStatus.WRONG_INPUT -> UniswapTheme.colors.statusCritical
.background(UniswapTheme.colors.surface2)
if (word.hasError) {
rowModifier = rowModifier.border(1.dp, UniswapTheme.colors.statusCritical, shape)
} else if (word.focused) {
rowModifier = rowModifier.border(1.dp, UniswapTheme.colors.accent1, shape)
}
onClick?.let {
rowModifier = rowModifier.clickable { it() }
} }
Row( Row {
modifier = rowModifier
.padding(horizontal = if (showCompact) UniswapTheme.spacing.spacing12 else UniswapTheme.spacing.spacing16)
.padding(vertical = if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12)
) {
Text( Text(
text = "${word.num}", text = "${word.num}",
color = UniswapTheme.colors.neutral3, color = UniswapTheme.colors.neutral2,
modifier = Modifier modifier = Modifier.defaultMinSize(minWidth = if (shouldShowSmallText) 14.dp else 16.dp),
.defaultMinSize(minWidth = 24.dp) style = textStyle,
.align(Alignment.CenterVertically), )
textAlign = TextAlign.Center, Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing16))
Text(
modifier = Modifier.weight(1f),
text = word.text,
style = textStyle, style = textStyle,
color = textColor
) )
Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing12))
Text(modifier = Modifier.weight(1f), text = word.text, style = textStyle)
} }
} }
...@@ -14,21 +14,17 @@ import com.uniswap.theme.UniswapTheme ...@@ -14,21 +14,17 @@ import com.uniswap.theme.UniswapTheme
fun MnemonicWordsColumn( fun MnemonicWordsColumn(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
words: List<MnemonicWordUiState>, words: List<MnemonicWordUiState>,
showCompact: Boolean = false, shouldShowSmallText: Boolean = false,
onClick: ((word: MnemonicWordUiState) -> Unit)? = null,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy( verticalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing8),
if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12
),
) { ) {
words.forEachIndexed { index, word -> words.forEach { word ->
MnemonicWordCell(
val onWordClick = onClick?.let { word = word,
{ it(word) } shouldShowSmallText = shouldShowSmallText,
} )
MnemonicWordCell(word = word, showCompact = showCompact, onClick = onWordClick)
} }
} }
} }
package com.uniswap.onboarding.backup.ui package com.uniswap.onboarding.backup.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState
import com.uniswap.theme.UniswapTheme import com.uniswap.theme.UniswapTheme
...@@ -17,15 +22,24 @@ fun MnemonicWordsGroup( ...@@ -17,15 +22,24 @@ fun MnemonicWordsGroup(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
words: List<MnemonicWordUiState>, words: List<MnemonicWordUiState>,
columnCount: Int = DEFAULT_COLUMN_COUNT, columnCount: Int = DEFAULT_COLUMN_COUNT,
showCompact: Boolean = false, shouldShowSmallText: Boolean = false,
onClick: ((word: MnemonicWordUiState) -> Unit)? = null,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight()
.background(color = UniswapTheme.colors.surface2, shape = RoundedCornerShape(20.dp))
.border(
width = 1.dp,
color = UniswapTheme.colors.surface3,
shape = RoundedCornerShape(20.dp),
)
.padding(
vertical = UniswapTheme.spacing.spacing24,
horizontal = UniswapTheme.spacing.spacing32
),
horizontalArrangement = Arrangement.spacedBy( horizontalArrangement = Arrangement.spacedBy(
if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12 if (shouldShowSmallText) UniswapTheme.spacing.spacing16 else UniswapTheme.spacing.spacing24
) )
) { ) {
val size = words.size / columnCount val size = words.size / columnCount
...@@ -35,8 +49,7 @@ fun MnemonicWordsGroup( ...@@ -35,8 +49,7 @@ fun MnemonicWordsGroup(
MnemonicWordsColumn( MnemonicWordsColumn(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
words = words.subList(starting, ending), words = words.subList(starting, ending),
showCompact = showCompact, shouldShowSmallText = shouldShowSmallText,
onClick = onClick,
) )
} }
} }
......
package com.uniswap.onboarding.backup.ui.model package com.uniswap.onboarding.backup.ui.model
data class MnemonicWordBankCellUiState( data class MnemonicWordBankCellUiState(
val index: Int,
val text: String, val text: String,
val used: Boolean = false, val used: Boolean = false,
) )
package com.uniswap.onboarding.backup.ui.model package com.uniswap.onboarding.backup.ui.model
enum class MnemonicInputStatus {
NO_INPUT,
CORRECT_INPUT,
WRONG_INPUT
}
data class MnemonicWordUiState( data class MnemonicWordUiState(
val num: Int, val num: Int,
val text: String, val text: String,
val focused: Boolean = false, val status: MnemonicInputStatus = MnemonicInputStatus.CORRECT_INPUT
val hasError: Boolean = false,
val sourceIndex: Int? = null,
) )
package com.uniswap.onboarding.import package com.uniswap.onboarding.import
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams
import androidx.annotation.IdRes
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.core.view.updateLayoutParams
import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManager
import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter import com.facebook.react.uimanager.events.RCTEventEmitter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.uniswap.R import com.uniswap.R
import com.uniswap.RnEthersRs import com.uniswap.RnEthersRs
import com.uniswap.theme.UniswapComponent import com.uniswap.theme.UniswapComponent
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.json.Json
/** /**
...@@ -86,13 +75,22 @@ class SeedPhraseInputViewManager : ViewGroupManager<ComposeView>() { ...@@ -86,13 +75,22 @@ class SeedPhraseInputViewManager : ViewGroupManager<ComposeView>() {
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> { override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf( return mapOf(
EVENT_HELP_TEXT_PRESS to mapOf( EVENT_HELP_TEXT_PRESS to mapOf(
"phasedRegistrationNames" to mapOf("bubbled" to EVENT_HELP_TEXT_PRESS, "captured" to EVENT_HELP_TEXT_PRESS) "phasedRegistrationNames" to mapOf(
"bubbled" to EVENT_HELP_TEXT_PRESS,
"captured" to EVENT_HELP_TEXT_PRESS
)
), ),
EVENT_INPUT_VALIDATED to mapOf( EVENT_INPUT_VALIDATED to mapOf(
"phasedRegistrationNames" to mapOf("bubbled" to EVENT_INPUT_VALIDATED, "captured" to EVENT_INPUT_VALIDATED) "phasedRegistrationNames" to mapOf(
"bubbled" to EVENT_INPUT_VALIDATED,
"captured" to EVENT_INPUT_VALIDATED
)
), ),
EVENT_MNEMONIC_STORED to mapOf( EVENT_MNEMONIC_STORED to mapOf(
"phasedRegistrationNames" to mapOf("bubbled" to EVENT_MNEMONIC_STORED, "captured" to EVENT_MNEMONIC_STORED) "phasedRegistrationNames" to mapOf(
"bubbled" to EVENT_MNEMONIC_STORED,
"captured" to EVENT_MNEMONIC_STORED
)
), ),
) )
} }
......
...@@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue ...@@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.toLowerCase
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.uniswap.EthersRs import com.uniswap.EthersRs
import com.uniswap.RnEthersRs import com.uniswap.RnEthersRs
...@@ -17,9 +16,9 @@ class SeedPhraseInputViewModel( ...@@ -17,9 +16,9 @@ class SeedPhraseInputViewModel(
) : ViewModel() { ) : ViewModel() {
sealed interface Status { sealed interface Status {
object None: Status object None : Status
object Valid: Status object Valid : Status
class Error(val error: MnemonicError): Status class Error(val error: MnemonicError) : Status
} }
sealed interface MnemonicError { sealed interface MnemonicError {
...@@ -27,7 +26,7 @@ class SeedPhraseInputViewModel( ...@@ -27,7 +26,7 @@ class SeedPhraseInputViewModel(
object TooManyWords : MnemonicError object TooManyWords : MnemonicError
object NotEnoughWords : MnemonicError object NotEnoughWords : MnemonicError
object WrongRecoveryPhrase : MnemonicError object WrongRecoveryPhrase : MnemonicError
object InvalidPhrase: MnemonicError object InvalidPhrase : MnemonicError
} }
data class ReactNativeStrings( data class ReactNativeStrings(
...@@ -42,7 +41,8 @@ class SeedPhraseInputViewModel( ...@@ -42,7 +41,8 @@ class SeedPhraseInputViewModel(
// Sourced externally from RN // Sourced externally from RN
var mnemonicIdForRecovery by mutableStateOf<String?>(null) var mnemonicIdForRecovery by mutableStateOf<String?>(null)
var rnStrings by mutableStateOf(ReactNativeStrings( var rnStrings by mutableStateOf(
ReactNativeStrings(
helpText = "", helpText = "",
inputPlaceholder = "", inputPlaceholder = "",
pasteButton = "", pasteButton = "",
...@@ -50,7 +50,8 @@ class SeedPhraseInputViewModel( ...@@ -50,7 +50,8 @@ class SeedPhraseInputViewModel(
errorPhraseLength = "", errorPhraseLength = "",
errorWrongPhrase = "", errorWrongPhrase = "",
errorInvalidPhrase = "", errorInvalidPhrase = "",
)) )
)
var input by mutableStateOf(TextFieldValue("")) var input by mutableStateOf(TextFieldValue(""))
private set private set
......
package com.uniswap.onboarding.shared
import androidx.annotation.DrawableRes
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.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.draw.shadow
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.uniswap.R
import com.uniswap.theme.UniswapTheme
import kotlinx.coroutines.delay
enum class ActionButtonStatus {
SUCCESS, NEUTRAL
}
/**
* Button that calls the action when clicked
*/
@Composable
fun ActionButton(
modifier: Modifier = Modifier,
action: () -> Unit,
text: String,
status: ActionButtonStatus = ActionButtonStatus.NEUTRAL,
@DrawableRes iconDrawable: Int? = null,
) {
val iconColor = when (status) {
ActionButtonStatus.SUCCESS -> UniswapTheme.colors.statusSuccess
ActionButtonStatus.NEUTRAL -> UniswapTheme.colors.neutral2
}
val textColor = when (status) {
ActionButtonStatus.SUCCESS -> UniswapTheme.colors.statusSuccess
ActionButtonStatus.NEUTRAL -> UniswapTheme.colors.neutral1
}
Box(
modifier = modifier.shadow(
10.dp,
spotColor = UniswapTheme.colors.black.copy(alpha = 0.04f),
shape = UniswapTheme.shapes.small
)
) {
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing4),
modifier = Modifier
.clip(shape = UniswapTheme.shapes.small)
.border(1.dp, UniswapTheme.colors.surface3, UniswapTheme.shapes.small)
.clickable { action() }
.background(color = UniswapTheme.colors.surface1)
.padding(
top = UniswapTheme.spacing.spacing8,
end = UniswapTheme.spacing.spacing16,
bottom = UniswapTheme.spacing.spacing8,
start = if (iconDrawable != null) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing16
)) {
iconDrawable?.let {
Icon(
painter = painterResource(id = it),
contentDescription = null,
tint = iconColor,
modifier = Modifier.size(20.dp)
)
}
Text(
text = text, color = textColor, style = UniswapTheme.typography.buttonLabel4
)
}
}
}
@Composable
fun CopyButton(
modifier: Modifier = Modifier,
copyButtonText: String,
copiedButtonText: String,
textToCopy: AnnotatedString
) {
val clipboardManager = LocalClipboardManager.current
// Time after which the button reverts to the copy state
val copyTimeout: Long = 2000
var isCopied by remember { mutableStateOf(false) }
var copyTimeoutId by remember { mutableStateOf(0) }
fun onClick() {
clipboardManager.setText(textToCopy)
copyTimeoutId++
isCopied = true
}
LaunchedEffect(copyTimeoutId) {
delay(copyTimeout)
isCopied = false
}
ActionButton(
action = { onClick() },
text = if (isCopied) copiedButtonText else copyButtonText,
iconDrawable = R.drawable.uniswap_icon_copy,
status = if (isCopied) ActionButtonStatus.SUCCESS else ActionButtonStatus.NEUTRAL,
modifier = modifier
)
}
@Composable
fun PasteButton(
modifier: Modifier = Modifier,
pasteButtonText: String,
onPaste: (text: String) -> Unit
) {
val clipboardManager = LocalClipboardManager.current
fun onClick() {
clipboardManager.getText()?.toString()?.let {
onPaste(it)
}
}
ActionButton(
action = { onClick() },
text = pasteButtonText,
iconDrawable = R.drawable.uniswap_icon_paste,
modifier = modifier
)
}
package com.uniswap.theme
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
fun Modifier.relativeOffset(
x: Float = 0f,
y: Float = 0f,
onOffsetCalculated: (Int, Int) -> Unit = { _, _ -> }
): Modifier = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val offsetX = (placeable.width * x).toInt()
val offsetY = (placeable.height * y).toInt()
onOffsetCalculated(offsetX, offsetY)
layout(placeable.width, placeable.height) {
placeable.place(offsetX, offsetY)
}
}
)
...@@ -5,6 +5,7 @@ import androidx.compose.runtime.staticCompositionLocalOf ...@@ -5,6 +5,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
data class CustomShapes( data class CustomShapes(
val small: RoundedCornerShape = RoundedCornerShape(16.dp),
val medium: RoundedCornerShape = RoundedCornerShape(20.dp), val medium: RoundedCornerShape = RoundedCornerShape(20.dp),
val large: RoundedCornerShape = RoundedCornerShape(24.dp), val large: RoundedCornerShape = RoundedCornerShape(24.dp),
val xlarge: RoundedCornerShape = RoundedCornerShape(100.dp), val xlarge: RoundedCornerShape = RoundedCornerShape(100.dp),
......
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:pathData="M17.5 8.02083V15.3125C17.5 16.7708 16.7708 17.5 15.3125 17.5H8.02083C6.5625 17.5 5.83333 16.7708 5.83333 15.3125V8.02083C5.83333 6.5625 6.5625 5.83333 8.02083 5.83333H15.3125C16.7708 5.83333 17.5 6.5625 17.5 8.02083ZM13.125 2.5C13.125 2.155 12.845 1.875 12.5 1.875H4.6875C2.87333 1.875 1.875 2.87333 1.875 4.6875V12.5C1.875 12.845 2.155 13.125 2.5 13.125C2.845 13.125 3.125 12.845 3.125 12.5V4.6875C3.125 3.5775 3.5775 3.125 4.6875 3.125H12.5C12.845 3.125 13.125 2.845 13.125 2.5Z" android:fillColor="#000000" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp" android:width="16dp"
android:height="16dp" android:height="16dp"
android:viewportWidth="16" android:viewportWidth="20"
android:viewportHeight="16"> android:viewportHeight="20">
<path <path
android:pathData="M13.3332 5.33333V12C13.3332 13.3333 12.6665 14 11.3332 14H4.6665C3.33317 14 2.6665 13.3333 2.6665 12V5.33333C2.6665 4.172 3.16777 3.518 4.17643 3.37134C4.2571 3.35934 4.33317 3.42599 4.33317 3.50732V3.66602C4.33317 4.87935 5.11984 5.66602 6.33317 5.66602H9.66651C10.8798 5.66602 11.6665 4.87935 11.6665 3.66602V3.50732C11.6665 3.42599 11.7432 3.35934 11.8232 3.37134C12.8319 3.518 13.3332 4.172 13.3332 5.33333ZM6.33317 4.66667H9.66651C10.3332 4.66667 10.6665 4.33333 10.6665 3.66667V3C10.6665 2.33333 10.3332 2 9.66651 2H6.33317C5.6665 2 5.33317 2.33333 5.33317 3V3.66667C5.33317 4.33333 5.6665 4.66667 6.33317 4.66667Z" android:pathData="M17.5 5.30833V10.70831C17.5 10.89165 17.4833 11.075 17.4416 11.25H14.0666C12.5082 11.25 11.25 12.5083 11.25 14.0667V17.4417C11.075 17.4834 10.8917 17.5 10.7084 17.5H5.31657C3.43324 17.5 2.5 16.5583 2.5 14.6833V5.30833C2.5 3.43333 3.43324 2.5 5.31657 2.5H14.6834C16.5668 2.5 17.5 3.43333 17.5 5.30833ZM12.5 14.0667V16.85C12.575 16.8 12.6333 16.7417 12.7 16.675L16.675 12.7C16.7417 12.6333 16.8 12.575 16.85 12.5H14.0666C13.1999 12.5 12.5 13.2 12.5 14.0667Z"
android:fillColor="#000000" android:fillColor="#000000"
/> />
</vector> </vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#000000"
android:pathData="M10 1.66667C5.39749 1.66667 1.66666 5.39751 1.66666 10C1.66666 14.6025 5.39749 18.3333 10 18.3333C14.6025 18.3333 18.3333 14.6025 18.3333 10C18.3333 5.39751 14.6025 1.66667 10 1.66667ZM10.0167 14.5833C9.55667 14.5833 9.17907 14.21 9.17907 13.75C9.17907 13.29 9.54833 12.9167 10.0083 12.9167H10.0167C10.4775 12.9167 10.85 13.29 10.85 13.75C10.85 14.21 10.4767 14.5833 10.0167 14.5833ZM11.3358 10.4401C10.7267 10.8484 10.6133 11.0759 10.5924 11.1359C10.5049 11.3967 10.2617 11.5617 10 11.5617C9.93416 11.5617 9.86746 11.5517 9.80163 11.5292C9.47413 11.4192 9.29838 11.065 9.40754 10.7375C9.55838 10.2875 9.94923 9.86334 10.6392 9.40084C11.4901 8.83084 11.3808 8.20589 11.345 8.00089C11.2508 7.45589 10.7916 6.99156 10.2525 6.89656C9.84247 6.82156 9.44174 6.92906 9.12841 7.19157C8.81341 7.45573 8.6324 7.84492 8.6324 8.25826C8.6324 8.60326 8.3524 8.88326 8.0074 8.88326C7.6624 8.88326 7.3824 8.60326 7.3824 8.25826C7.3824 7.47409 7.72582 6.73663 8.32498 6.23413C8.91832 5.73746 9.69913 5.53004 10.469 5.6667C11.5258 5.85337 12.3917 6.7258 12.5759 7.78747C12.7592 8.83914 12.3183 9.78173 11.3358 10.4401Z" />
</vector>
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
<string name="app_name">Uniswap</string> <string name="app_name">Uniswap</string>
<string name="expo_splash_screen_resize_mode">cover</string> <string name="expo_splash_screen_resize_mode">cover</string>
<string name="expo_splash_screen_status_bar_translucent">true</string> <string name="expo_splash_screen_status_bar_translucent">true</string>
<string name="notification_accent_color">f50db4</string>
</resources> </resources>
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct Colors { struct Colors {
static let surface1 = Color("surface1")
static let surface2 = Color("surface2") static let surface2 = Color("surface2")
static let surface3 = Color("surface3") static let surface3 = Color("surface3")
static let neutral1 = Color("neutral1") static let neutral1 = Color("neutral1")
......
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x13",
"green" : "0x13",
"red" : "0x13"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
//
// CopyIcon.swift
// Uniswap
//
// Created by Mateusz Łopaciński on 27/05/2024.
//
import SwiftUI
struct CopyIcon: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint(x: 0.875 * width, y: 0.40104167 * height))
path.addLine(to: CGPoint(x: 0.875 * width, y: 0.765625 * height))
path.addCurve(to: CGPoint(x: 0.765625 * width, y: 0.875 * height), control1: CGPoint(x: 0.875 * width, y: 0.828125 * height), control2: CGPoint(x: 0.828125 * width, y: 0.875 * height))
path.addLine(to: CGPoint(x: 0.40104167 * width, y: 0.875 * height))
path.addCurve(to: CGPoint(x: 0.29166667 * width, y: 0.765625 * height), control1: CGPoint(x: 0.33854167 * width, y: 0.875 * height), control2: CGPoint(x: 0.29166667 * width, y: 0.828125 * height))
path.addLine(to: CGPoint(x: 0.29166667 * width, y: 0.40104167 * height))
path.addCurve(to: CGPoint(x: 0.40104167 * width, y: 0.29166667 * height), control1: CGPoint(x: 0.29166667 * width, y: 0.33854167 * height), control2: CGPoint(x: 0.33854167 * width, y: 0.29166667 * height))
path.addLine(to: CGPoint(x: 0.765625 * width, y: 0.29166667 * height))
path.addCurve(to: CGPoint(x: 0.875 * width, y: 0.40104167 * height), control1: CGPoint(x: 0.828125 * width, y: 0.29166667 * height), control2: CGPoint(x: 0.875 * width, y: 0.33854167 * height))
path.closeSubpath()
path.move(to: CGPoint(x: 0.65625 * width, y: 0.125 * height))
path.addCurve(to: CGPoint(x: 0.625 * width, y: 0.09375 * height), control1: CGPoint(x: 0.65625 * width, y: 0.109375 * height), control2: CGPoint(x: 0.640625 * width, y: 0.09375 * height))
path.addLine(to: CGPoint(x: 0.234375 * width, y: 0.09375 * height))
path.addCurve(to: CGPoint(x: 0.09375 * width, y: 0.234375 * height), control1: CGPoint(x: 0.14583333 * width, y: 0.09375 * height), control2: CGPoint(x: 0.09375 * width, y: 0.14583333 * height))
path.addLine(to: CGPoint(x: 0.09375 * width, y: 0.625 * height))
path.addCurve(to: CGPoint(x: 0.125 * width, y: 0.65625 * height), control1: CGPoint(x: 0.09375 * width, y: 0.640625 * height), control2: CGPoint(x: 0.109375 * width, y: 0.65625 * height))
path.addCurve(to: CGPoint(x: 0.15625 * width, y: 0.625 * height), control1: CGPoint(x: 0.140625 * width, y: 0.65625 * height), control2: CGPoint(x: 0.15625 * width, y: 0.640625 * height))
path.addLine(to: CGPoint(x: 0.15625 * width, y: 0.234375 * height))
path.addCurve(to: CGPoint(x: 0.234375 * width, y: 0.15625 * height), control1: CGPoint(x: 0.15625 * width, y: 0.19270833 * height), control2: CGPoint(x: 0.19270833 * width, y: 0.15625 * height))
path.addLine(to: CGPoint(x: 0.625 * width, y: 0.15625 * height))
path.addCurve(to: CGPoint(x: 0.65625 * width, y: 0.125 * height), control1: CGPoint(x: 0.640625 * width, y: 0.15625 * height), control2: CGPoint(x: 0.65625 * width, y: 0.140625 * height))
path.closeSubpath()
return path
}
}
//
// CopyButton.swift
// Uniswap
//
// Created by Mateusz Łopaciński on 14/05/2024.
//
import SwiftUI
struct CopyButton: View {
// Time after which the button reverts to the copy state
private let copyTimeout: TimeInterval = 2.0
@State private var isCopied = false
@State private var copyTimer: Timer?
var action: () -> Void
var copyText: String
var copiedText: String
var body: some View {
Button(action: {
action()
isCopied = true
// Invalidate the previous timer if it exists
copyTimer?.invalidate()
// Start a new timer
copyTimer = Timer.scheduledTimer(withTimeInterval: copyTimeout, repeats: false) { _ in
isCopied = false
}
}) {
HStack(alignment: .center, spacing: 4) {
CopyIcon()
.fill(isCopied ? Colors.statusSuccess : Colors.neutral2)
.frame(width: 20, height: 20)
Text(isCopied ? copiedText : copyText)
.foregroundColor(isCopied ? Colors.statusSuccess : Colors.neutral1)
.font(Font(UIFont(name: "Basel-Book", size: 16)!))
}
}
.padding(EdgeInsets(top: 8, leading: 10, bottom: 8, trailing: 16))
.background(Colors.surface1)
.cornerRadius(50)
.overlay(
RoundedRectangle(cornerRadius: 50)
.stroke(Colors.surface3, lineWidth: 1)
)
.shadow(color: Color.black.opacity(0.04), radius: 10)
}
}
...@@ -18,6 +18,7 @@ RCT_EXPORT_MODULE() ...@@ -18,6 +18,7 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicConfirmationView); RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_PROPERTY(shouldShowSmallText, BOOL, MnemonicConfirmationView); RCT_EXPORT_SWIFTUI_PROPERTY(shouldShowSmallText, BOOL, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_CALLBACK(onConfirmComplete, RCTDirectEventBlock, MnemonicConfirmationView); RCT_EXPORT_SWIFTUI_CALLBACK(onConfirmComplete, RCTDirectEventBlock, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_PROPERTY(selectedWordPlaceholder, NSString, MnemonicConfirmationView);
- (UIView *)view - (UIView *)view
{ {
......
...@@ -18,6 +18,11 @@ import SwiftUI ...@@ -18,6 +18,11 @@ import SwiftUI
get { return vc.rootView.props.mnemonicId } get { return vc.rootView.props.mnemonicId }
} }
var selectedWordPlaceholder: String {
set { vc.rootView.props.selectedWordPlaceholder = newValue}
get { return vc.rootView.props.selectedWordPlaceholder }
}
var shouldShowSmallText: Bool { var shouldShowSmallText: Bool {
set { vc.rootView.props.shouldShowSmallText = newValue} set { vc.rootView.props.shouldShowSmallText = newValue}
get { return vc.rootView.props.shouldShowSmallText } get { return vc.rootView.props.shouldShowSmallText }
...@@ -36,11 +41,12 @@ import SwiftUI ...@@ -36,11 +41,12 @@ import SwiftUI
class MnemonicConfirmationProps : ObservableObject { class MnemonicConfirmationProps : ObservableObject {
@Published var mnemonicId: String = "" @Published var mnemonicId: String = ""
@Published var selectedWordPlaceholder: String = ""
@Published var shouldShowSmallText: Bool = false @Published var shouldShowSmallText: Bool = false
@Published var onConfirmComplete: RCTDirectEventBlock = { _ in } @Published var onConfirmComplete: RCTDirectEventBlock = { _ in }
@Published var mnemonicWords: [String] = Array(repeating: "", count: 12) @Published var mnemonicWords: [String] = Array(repeating: "", count: 12)
@Published var scrambledWords: [String] = Array(repeating: "", count: 12) @Published var scrambledWords: [String] = Array(repeating: "", count: 12)
@Published var typedWords: [String] = Array(repeating: "", count: 12) @Published var typedWordIndexes: [Int] = Array(repeating: -1, count: 12)
@Published var selectedIndex: Int = 0 @Published var selectedIndex: Int = 0
} }
...@@ -58,49 +64,70 @@ struct MnemonicConfirmation: View { ...@@ -58,49 +64,70 @@ struct MnemonicConfirmation: View {
} }
} }
func onSuggestionTapped(word: String) { func onSuggestionTapped(tappedIndex: Int) {
props.typedWords[props.selectedIndex] = word props.typedWordIndexes[props.selectedIndex] = tappedIndex
if (props.typedWords == props.mnemonicWords) { // Check if typed words match mnemonic words only if all fields are filled
if (props.selectedIndex == props.mnemonicWords.count - 1) {
if (isMnemonicMatch()) {
props.onConfirmComplete([:]) props.onConfirmComplete([:])
} else if (props.mnemonicWords[props.selectedIndex] == props.typedWords[props.selectedIndex] && props.selectedIndex < props.mnemonicWords.count - 1) { }
} else if (props.mnemonicWords[props.selectedIndex] == props.scrambledWords[tappedIndex] && props.selectedIndex < props.mnemonicWords.count - 1) {
props.selectedIndex += 1 props.selectedIndex += 1
} }
} }
func onFieldTapped(fieldNumber: Int) { func isMnemonicMatch() -> Bool {
props.selectedIndex = fieldNumber - 1 for i in 0..<props.typedWordIndexes.count {
if (getTypedWord(index: i) != props.mnemonicWords[i]) {
return false
}
} }
return true
}
func getLabelFocusState(index: Int) -> InputFocusState{ func getTypedWord(index: Int) -> String {
let isTextFieldFocused = index == props.selectedIndex guard index >= 0 && index < props.typedWordIndexes.count else {
let isTextFieldValid = props.mnemonicWords[index] == props.typedWords[index] return ""
let isTextFieldEmpty = props.typedWords[index].count == 0 }
if (isTextFieldFocused && !isTextFieldEmpty && !isTextFieldValid) { let scrambledWordIndex = props.typedWordIndexes[index]
return InputFocusState.focusedWrongInput if scrambledWordIndex == -1 {
} else if (isTextFieldFocused) { return ""
return InputFocusState.focusedNoInput
} else if (!isTextFieldEmpty && !isTextFieldValid) {
return InputFocusState.notFocusedWrongInput
} }
return InputFocusState.notFocused
return props.scrambledWords[scrambledWordIndex]
}
func getFieldText(index: Int) -> String {
let typedWord = getTypedWord(index: index)
return typedWord.isEmpty ? props.selectedWordPlaceholder : typedWord
}
func getFieldStatus(index: Int) -> MnemonicInputStatus {
let typedWord = getTypedWord(index: index)
if (typedWord.isEmpty) {
return MnemonicInputStatus.noInput
} else if (props.mnemonicWords[index] != typedWord) {
return MnemonicInputStatus.wrongInput
}
return MnemonicInputStatus.correctInput
} }
var body: some View { var body: some View {
let end = props.mnemonicWords.count - 1 let end = props.mnemonicWords.count - 1
let middle = end / 2 let middle = end / 2
VStack(alignment: HorizontalAlignment.leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(alignment: VerticalAlignment.center, spacing: 12) { HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
ForEach((0...middle), id: \.self) {index in ForEach((0...middle), id: \.self) {index in
MnemonicTextField(index: index + 1, MnemonicTextField(index: index + 1,
initialText: props.typedWords[index], word: getFieldText(index: index),
shouldShowSmallText: props.shouldShowSmallText, status: getFieldStatus(index: index),
focusState: getLabelFocusState(index: index), shouldShowSmallText: props.shouldShowSmallText
onFieldTapped: onFieldTapped
) )
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
...@@ -108,20 +135,24 @@ struct MnemonicConfirmation: View { ...@@ -108,20 +135,24 @@ struct MnemonicConfirmation: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
ForEach((middle + 1...end), id: \.self) {index in ForEach((middle + 1...end), id: \.self) {index in
MnemonicTextField(index: index + 1, MnemonicTextField(index: index + 1,
initialText: props.typedWords[index], word: getFieldText(index: index),
shouldShowSmallText: props.shouldShowSmallText, status: getFieldStatus(index: index),
focusState: getLabelFocusState(index: index), shouldShowSmallText: props.shouldShowSmallText
onFieldTapped: onFieldTapped
) )
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
.padding([.leading, .trailing], 24) .padding(EdgeInsets(top: 24, leading: 32, bottom: 24, trailing: 32))
.background(Colors.surface2)
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Colors.surface3, lineWidth: 1)
)
MnemonicConfirmationWordBankView(words: props.scrambledWords, MnemonicConfirmationWordBankView(words: props.scrambledWords,
usedWords: props.typedWords, usedWordIndexes: props.typedWordIndexes,
labelCallback: onSuggestionTapped, labelCallback: onSuggestionTapped,
shouldShowSmallText: props.shouldShowSmallText) shouldShowSmallText: props.shouldShowSmallText)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
......
...@@ -8,33 +8,29 @@ ...@@ -8,33 +8,29 @@
import SwiftUI import SwiftUI
struct BankWord: Hashable { struct BankWord: Hashable {
var index: Int
var word: String = "" var word: String = ""
var used: Bool = false var used: Bool = false
} }
struct MnemonicConfirmationWordBankView: View { struct MnemonicConfirmationWordBankView: View {
let smallFont = UIFont(name: "Basel-Book", size: 14) let smallFont = UIFont(name: "Basel-Book", size: 14)
let mediumFont = UIFont(name: "Basel-Book", size: 16) let mediumFont = UIFont(name: "Basel-Book", size: 16)
var groupedWords: [[BankWord]] = [[BankWord]]() var groupedWords: [[BankWord]] = [[BankWord]]()
let screenWidth = UIScreen.main.bounds.width // Used to calculate max number of tags per row let screenWidth = UIScreen.main.bounds.width // Used to calculate max number of tags per row
var labelCallback: ((String) -> Void)? var labelCallback: ((Int) -> Void)?
let shouldShowSmallText: Bool let shouldShowSmallText: Bool
init(words: [String], usedWords: [String], labelCallback: @escaping (String) -> Void, shouldShowSmallText: Bool) { init(words: [String], usedWordIndexes: [Int], labelCallback: @escaping (Int) -> Void, shouldShowSmallText: Bool) {
self.labelCallback = labelCallback self.labelCallback = labelCallback
self.shouldShowSmallText = shouldShowSmallText self.shouldShowSmallText = shouldShowSmallText
// Mark words as used individually to handle case of duplicate words // Ensure that proper words are displayed as used in case of duplicates
var wordStructs = words.map { word in BankWord(word: word) } var wordStructs = words.enumerated().map { index, word in BankWord(index: index, word: word) }
// Use used words to mark used usedWordIndexes.forEach{ idx in
usedWords.forEach{ usedWord in if (idx != -1) {
for idx in 0...wordStructs.count-1 {
if (usedWord == wordStructs[idx].word && !wordStructs[idx].used) {
wordStructs[idx].used = true wordStructs[idx].used = true
return
}
} }
} }
...@@ -43,13 +39,11 @@ struct MnemonicConfirmationWordBankView: View { ...@@ -43,13 +39,11 @@ struct MnemonicConfirmationWordBankView: View {
} }
private func createGroupedWords(_ items: [BankWord]) -> [[BankWord]] { private func createGroupedWords(_ items: [BankWord]) -> [[BankWord]] {
var groupedItems: [[BankWord]] = [[BankWord]]() var groupedItems: [[BankWord]] = [[BankWord]]()
var tempItems: [BankWord] = [BankWord]() var tempItems: [BankWord] = [BankWord]()
var width: CGFloat = 0 var width: CGFloat = 0
for word in items { for word in items {
let label = UILabel() let label = UILabel()
label.text = word.word label.text = word.word
label.sizeToFit() label.sizeToFit()
...@@ -65,31 +59,33 @@ struct MnemonicConfirmationWordBankView: View { ...@@ -65,31 +59,33 @@ struct MnemonicConfirmationWordBankView: View {
tempItems.removeAll() tempItems.removeAll()
tempItems.append(word) tempItems.append(word)
} }
} }
groupedItems.append(tempItems) groupedItems.append(tempItems)
return groupedItems return groupedItems
} }
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
ForEach(groupedWords, id: \.self) { subItems in ForEach(groupedWords, id: \.self) { subItems in
HStack(spacing: 8) { HStack(spacing: shouldShowSmallText ? 4 : 8) {
ForEach(subItems, id: \.self) { bankWord in ForEach(subItems, id: \.self) { bankWord in
Text(bankWord.word) Text(bankWord.word)
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) .font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.fixedSize() .fixedSize()
.padding(shouldShowSmallText ? EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12) : EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)) .padding(shouldShowSmallText ? EdgeInsets(top: 8, leading: 10, bottom: 8, trailing: 10) : EdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12))
.background(Colors.surface2) .background(Colors.surface1)
.foregroundColor(Colors.neutral1) .foregroundColor(Colors.neutral1)
.clipShape(RoundedRectangle(cornerRadius: 100, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 100, style: .continuous))
.shadow(color: Color.black.opacity(0.04), radius: 10)
.overlay(
RoundedRectangle(cornerRadius: 50)
.stroke(Colors.surface3, lineWidth: 1)
)
.onTapGesture { .onTapGesture {
labelCallback?(bankWord.word) labelCallback?(bankWord.index)
} }
.opacity(bankWord.used ? 0.60 : 1) .opacity(bankWord.used ? 0.5 : 1)
} }
} }
} }
......
...@@ -13,12 +13,14 @@ ...@@ -13,12 +13,14 @@
@end @end
@implementation MnemonicDisplayManager @implementation MnemonicDisplayManager
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE(MnemonicDisplay)
RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicDisplayView); RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_PROPERTY(copyText, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_PROPERTY(copiedText, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_CALLBACK(onHeightMeasured, RCTBubblingEventBlock, MnemonicDisplayView)
- (UIView *)view - (UIView *)view {
{
MnemonicDisplayView *proxy = [[MnemonicDisplayView alloc] init]; MnemonicDisplayView *proxy = [[MnemonicDisplayView alloc] init];
UIView *view = [proxy view]; UIView *view = [proxy view];
NSMutableDictionary *storage = [MnemonicDisplayView storage]; NSMutableDictionary *storage = [MnemonicDisplayView storage];
......
...@@ -17,19 +17,40 @@ import SwiftUI ...@@ -17,19 +17,40 @@ import SwiftUI
get { return vc.rootView.props.mnemonicId } get { return vc.rootView.props.mnemonicId }
} }
var copyText: String {
set { vc.rootView.props.copyText = newValue }
get { return vc.rootView.props.copyText }
}
var copiedText: String {
set { vc.rootView.props.copiedText = newValue }
get { return vc.rootView.props.copiedText }
}
var onHeightMeasured: RCTBubblingEventBlock? {
didSet {
vc.rootView.props.onHeightMeasured = { [weak self] height in
self?.onHeightMeasured?([ "height": height ])
}
}
}
var view: UIView { var view: UIView {
vc.view.backgroundColor = .clear vc.view.backgroundColor = .clear
return vc.view return vc.view
} }
} }
class MnemonicDisplayProps : ObservableObject { class MnemonicDisplayProps: ObservableObject {
@Published var mnemonicId: String = "" @Published var mnemonicId: String = ""
@Published var copyText: String = ""
@Published var copiedText: String = ""
@Published var mnemonicWords: [String] = Array(repeating: "", count: 12) @Published var mnemonicWords: [String] = Array(repeating: "", count: 12)
var onHeightMeasured: ((CGFloat) -> Void)?
} }
struct MnemonicDisplay: View { struct MnemonicDisplay: View {
private let buttonOffset: CGFloat = 20
@ObservedObject var props = MnemonicDisplayProps() @ObservedObject var props = MnemonicDisplayProps()
...@@ -43,6 +64,12 @@ struct MnemonicDisplay: View { ...@@ -43,6 +64,12 @@ struct MnemonicDisplay: View {
} }
} }
func copyToClipboard() {
let mnemonicString = props.mnemonicWords.joined(separator: " ")
UIPasteboard.general.string = mnemonicString
}
var body: some View { var body: some View {
if (props.mnemonicWords.count > 12) { if (props.mnemonicWords.count > 12) {
ScrollView { ScrollView {
...@@ -58,26 +85,61 @@ struct MnemonicDisplay: View { ...@@ -58,26 +85,61 @@ struct MnemonicDisplay: View {
let end = props.mnemonicWords.count - 1 let end = props.mnemonicWords.count - 1
let middle = end / 2 let middle = end / 2
VStack(alignment: HorizontalAlignment.leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(alignment: VerticalAlignment.center, spacing: 12) { ZStack {
HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
ForEach((0...middle), id: \.self) {index in ForEach((0...middle), id: \.self) { index in
MnemonicTextField(index: index + 1, MnemonicTextField(index: index + 1,
initialText: props.mnemonicWords[index] word: props.mnemonicWords[index]
) )
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
ForEach((middle + 1...end), id: \.self) {index in ForEach((middle + 1...end), id: \.self) { index in
MnemonicTextField(index: index + 1, MnemonicTextField(index: index + 1,
initialText: props.mnemonicWords[index] word: props.mnemonicWords[index]
) )
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity) }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) }
.padding(EdgeInsets(top: 0, leading: 16, bottom: 32, trailing: 16)) .frame(maxWidth: .infinity)
.padding(EdgeInsets(top: 24, leading: 32, bottom: 24, trailing: 32))
.background(Colors.surface2)
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Colors.surface3, lineWidth: 1)
)
.overlay(
HStack {
Spacer()
CopyButton(
action: copyToClipboard,
copyText: props.copyText,
copiedText: props.copiedText
)
Spacer()
}
.offset(y: -buttonOffset),
alignment: .top
)
}
.frame(maxWidth: .infinity, alignment: .top)
.padding(.top, buttonOffset)
.overlay(
GeometryReader { geometry in
Color.clear
.onAppear {
props.onHeightMeasured?(geometry.size.height)
}
.onChange(of: geometry.size.height) { newValue in
props.onHeightMeasured?(newValue)
}
}
)
} }
} }
...@@ -7,92 +7,55 @@ ...@@ -7,92 +7,55 @@
import SwiftUI import SwiftUI
enum InputFocusState { enum MnemonicInputStatus {
case notFocused case noInput
case focusedNoInput case correctInput
case focusedWrongInput case wrongInput
case notFocusedWrongInput
} }
struct MnemonicTextField: View { struct MnemonicTextField: View {
@Environment(\.colorScheme) var colorScheme
let smallFont = UIFont(name: "Basel-Book", size: 14) let smallFont = UIFont(name: "Basel-Book", size: 14)
let mediumFont = UIFont(name: "Basel-Book", size: 16) let mediumFont = UIFont(name: "Basel-Book", size: 16)
var index: Int var index: Int
var initialText = "" var word = ""
var shouldShowSmallText: Bool var shouldShowSmallText: Bool
var onFieldTapped: ((Int) -> Void)? var status: MnemonicInputStatus
var focusState: InputFocusState
init(index: Int, init(index: Int,
initialText: String, word: String,
shouldShowSmallText: Bool = false, status: MnemonicInputStatus = .correctInput,
focusState: InputFocusState = InputFocusState.notFocused, shouldShowSmallText: Bool = false
onFieldTapped: ((Int) -> Void)? = nil
) { ) {
self.index = index self.index = index
self.initialText = initialText self.word = word
self.status = status
self.shouldShowSmallText = shouldShowSmallText self.shouldShowSmallText = shouldShowSmallText
self.focusState = focusState
self.onFieldTapped = onFieldTapped
} }
func getLabelBackground(focusState: InputFocusState) -> some View { func getLabelColor() -> Color {
switch (focusState) { switch (status) {
case .focusedNoInput: case .noInput:
return AnyView(RoundedRectangle(cornerRadius: 100) return Colors.neutral3
.strokeBorder(Colors.accent1, lineWidth: 2) case .correctInput:
.background(Colors.surface2) return Colors.neutral1
.cornerRadius(100) case .wrongInput:
) return Colors.statusCritical
case .focusedWrongInput:
return AnyView(RoundedRectangle(cornerRadius: 100)
.strokeBorder(Colors.statusCritical, lineWidth: 2)
.background(Colors.surface2)
.cornerRadius(100)
)
case .notFocusedWrongInput:
return AnyView(RoundedRectangle(cornerRadius: 100)
.strokeBorder(Colors.statusCritical, lineWidth: 2)
.background(Colors.surface2)
.cornerRadius(100)
)
case .notFocused:
return AnyView(
RoundedRectangle(cornerRadius: 100, style: .continuous)
.fill(Colors.surface2)
)
} }
} }
var body: some View { var body: some View {
HStack(alignment: VerticalAlignment.center, spacing: 0) { HStack(alignment: VerticalAlignment.center, spacing: 18) {
Text(String(index))
Text(String(index)).cornerRadius(16)
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) .font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.foregroundColor(Colors.neutral3) .foregroundColor(Colors.neutral2)
.padding(shouldShowSmallText ? EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 12) : EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 12)) .frame(width: shouldShowSmallText ? 14 : 16, alignment: Alignment.leading)
.frame(alignment: Alignment.leading)
Text(initialText) Text(word)
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) .font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.multilineTextAlignment(TextAlignment.leading) .foregroundColor(getLabelColor())
.foregroundColor(Colors.neutral1) .multilineTextAlignment(.leading)
.padding(shouldShowSmallText ? EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 16) : EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 16))
.frame(maxWidth: .infinity, alignment: Alignment.leading)
} }
.background(getLabelBackground(focusState: focusState))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
onFieldTapped?(index)
}
} }
} }
//
// MnemonicWord.swift
// Uniswap
//
// Created by Spencer Yen on 5/24/22.
//
import Foundation
class MnemonicWordView: UIView {
private var index: Int?
private var word: String?
required init(index: Int, word: String) {
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
self.index = index
self.word = word
self.setupView()
}
required override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
private func setupView() {
self.layer.cornerRadius = 24
self.layer.masksToBounds = true
let indexLabel = UILabel()
indexLabel.text = String(describing: self.index!)
indexLabel.adjustsFontSizeToFitWidth = true
indexLabel.font = UIFont.init(name: "Basel-Book", size: 16)
let wordLabel = UILabel()
wordLabel.text = self.word
wordLabel.adjustsFontSizeToFitWidth = true
wordLabel.font = UIFont.init(name: "Basel-Book", size: 16)
let stackView = UIStackView(arrangedSubviews: [indexLabel, wordLabel])
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.alignment = .leading
stackView.spacing = 12.0
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.isLayoutMarginsRelativeArrangement = true
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
self.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
if traitCollection.userInterfaceStyle == .light {
self.layer.backgroundColor = UIColor.init(red: 249/255.0, green: 249/255.0, blue: 249/255.0, alpha: 1.0).cgColor
self.layer.borderColor = UIColor.init(red: 34/255.0, green: 34/255.0, blue: 34/255.0, alpha: 0.05).cgColor
indexLabel.textColor = UIColor.init(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0)
wordLabel.textColor = UIColor.black
} else {
self.layer.backgroundColor = UIColor.init(red: 27/255.0, green: 27/255.0, blue: 27/255.0, alpha: 1.0).cgColor
self.layer.borderColor = UIColor.init(red: 255/255.0, green: 255/255.0, blue: 255/255.0, alpha: 0.12).cgColor
indexLabel.textColor = UIColor.init(red: 155/255.0, green: 155/255.0, blue: 155/255.0, alpha: 1.0)
wordLabel.textColor = UIColor.white
}
}
}
...@@ -86,6 +86,16 @@ class RNEthersRS: NSObject { ...@@ -86,6 +86,16 @@ class RNEthersRS: NSObject {
return return
} }
@objc(removeMnemonic:resolve:reject:)
func removeMnemonic(
mnemonicId: String,
resolve: RCTPromiseResolveBlock,
reject: RCTPromiseRejectBlock
) {
let res = keychain.delete(keychainKeyForMnemonicId(mnemonicId: mnemonicId))
resolve(res)
}
/** /**
Generates a new mnemonic and retrieves associated public address. Stores new mnemonic in native keychain with the mnemonic ID key as the public address. Generates a new mnemonic and retrieves associated public address. Stores new mnemonic in native keychain with the mnemonic ID key as the public address.
...@@ -189,6 +199,14 @@ class RNEthersRS: NSObject { ...@@ -189,6 +199,14 @@ class RNEthersRS: NSObject {
resolve(address) resolve(address)
} }
@objc(removePrivateKey:resolve:reject:)
func removePrivateKey(
address: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock
) {
let res = keychain.delete(keychainKeyForPrivateKey(address: address))
resolve(res)
}
@objc(signTransactionHashForAddress:hash:chainId:resolve:reject:) @objc(signTransactionHashForAddress:hash:chainId:resolve:reject:)
func signTransactionForAddress( func signTransactionForAddress(
address: String, hash: String, chainId: NSNumber, resolve: RCTPromiseResolveBlock, address: String, hash: String, chainId: NSNumber, resolve: RCTPromiseResolveBlock,
......
...@@ -16,6 +16,10 @@ RCT_EXTERN_METHOD(importMnemonic: (NSString *)mnemonic ...@@ -16,6 +16,10 @@ RCT_EXTERN_METHOD(importMnemonic: (NSString *)mnemonic
resolve: (RCTPromiseResolveBlock)resolve resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject) reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeMnemonic: (NSString *)mnemonicId
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(generateAndStoreMnemonic: (RCTPromiseResolveBlock)resolve RCT_EXTERN_METHOD(generateAndStoreMnemonic: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject) reject: (RCTPromiseRejectBlock)reject)
...@@ -32,6 +36,10 @@ RCT_EXTERN_METHOD(generateAndStorePrivateKey: (NSString *)mnemonicId ...@@ -32,6 +36,10 @@ RCT_EXTERN_METHOD(generateAndStorePrivateKey: (NSString *)mnemonicId
resolve: (RCTPromiseResolveBlock)resolve resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject) reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removePrivateKey: (NSString *)address
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(signTransactionHashForAddress: (NSString *)address RCT_EXTERN_METHOD(signTransactionHashForAddress: (NSString *)address
hash: (NSString *)hash hash: (NSString *)hash
chainId: NSNumber chainId: NSNumber
......
...@@ -6,9 +6,13 @@ import 'uniswap/src/i18n/i18n' // Uses real translations for tests ...@@ -6,9 +6,13 @@ import 'uniswap/src/i18n/i18n' // Uses real translations for tests
import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js'
import { localizeMock as mockRNLocalize } from 'react-native-localize/mock' import { localizeMock as mockRNLocalize } from 'react-native-localize/mock'
import { TextDecoder, TextEncoder } from 'util'
import { AppearanceSettingType } from 'wallet/src/features/appearance/slice' import { AppearanceSettingType } from 'wallet/src/features/appearance/slice'
import { mockLocalizationContext } from 'wallet/src/test/mocks/utils' import { mockLocalizationContext } from 'wallet/src/test/mocks/utils'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
// Mock Sentry crash reporting // Mock Sentry crash reporting
jest.mock('@sentry/react-native', () => ({ jest.mock('@sentry/react-native', () => ({
init: () => jest.fn(), init: () => jest.fn(),
...@@ -65,6 +69,10 @@ jest.mock('react-native', () => { ...@@ -65,6 +69,10 @@ jest.mock('react-native', () => {
return RN return RN
}) })
jest.mock('expo-localization', () => ({
getLocales: jest.fn(() => [{ languageCode: 'en', countryCode: 'US' }]),
}))
jest.mock('react-native-safe-area-context', () => ({ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn().mockImplementation(() => ({})), useSafeAreaInsets: jest.fn().mockImplementation(() => ({})),
useSafeAreaFrame: jest.fn().mockImplementation(() => ({})), useSafeAreaFrame: jest.fn().mockImplementation(() => ({})),
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
}, },
"dependencies": { "dependencies": {
"@amplitude/analytics-react-native": "1.4.0", "@amplitude/analytics-react-native": "1.4.0",
"@apollo/client": "3.9.6", "@apollo/client": "3.10.4",
"@ethersproject/shims": "5.6.0", "@ethersproject/shims": "5.6.0",
"@formatjs/intl-datetimeformat": "4.5.1", "@formatjs/intl-datetimeformat": "4.5.1",
"@formatjs/intl-getcanonicallocales": "1.9.0", "@formatjs/intl-getcanonicallocales": "1.9.0",
...@@ -84,8 +84,8 @@ ...@@ -84,8 +84,8 @@
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.32.0", "@uniswap/analytics-events": "2.32.0",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.2.1", "@uniswap/sdk-core": "5.0.0",
"@uniswap/v3-sdk": "3.11.1", "@uniswap/v3-sdk": "3.11.2",
"@walletconnect/core": "2.11.2", "@walletconnect/core": "2.11.2",
"@walletconnect/react-native-compat": "2.11.2", "@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/utils": "2.11.2", "@walletconnect/utils": "2.11.2",
......
...@@ -45,11 +45,16 @@ import { ...@@ -45,11 +45,16 @@ import {
import { flexStyles, useIsDarkMode } from 'ui/src' import { flexStyles, useIsDarkMode } from 'ui/src'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { DUMMY_STATSIG_SDK_KEY } from 'uniswap/src/features/gating/constants' import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { Experiments } from 'uniswap/src/features/gating/experiments' import { Experiments } from 'uniswap/src/features/gating/experiments'
import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags'
import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides' import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides'
import { Statsig, StatsigProvider } from 'uniswap/src/features/gating/sdk/statsig' import {
Statsig,
StatsigOptions,
StatsigProvider,
StatsigUser,
} from 'uniswap/src/features/gating/sdk/statsig'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
...@@ -133,7 +138,12 @@ function App(): JSX.Element | null { ...@@ -133,7 +138,12 @@ function App(): JSX.Element | null {
const deviceId = useAsyncData(fetchAndSetDeviceId).data const deviceId = useAsyncData(fetchAndSetDeviceId).data
const statSigOptions = { const statSigOptions: {
user: StatsigUser
options: StatsigOptions
sdkKey: string
waitForInitialization: boolean
} = {
options: { options: {
environment: { environment: {
tier: getStatsigEnvironmentTier(), tier: getStatsigEnvironmentTier(),
...@@ -144,7 +154,12 @@ function App(): JSX.Element | null { ...@@ -144,7 +154,12 @@ function App(): JSX.Element | null {
initCompletionCallback: loadStatsigOverrides, initCompletionCallback: loadStatsigOverrides,
}, },
sdkKey: DUMMY_STATSIG_SDK_KEY, sdkKey: DUMMY_STATSIG_SDK_KEY,
user: deviceId ? { userID: deviceId } : {}, user: {
...(deviceId ? { userID: deviceId } : {}),
custom: {
app: StatsigCustomAppValue.Mobile,
},
},
waitForInitialization: true, waitForInitialization: true,
} }
......
...@@ -17,7 +17,6 @@ import { ...@@ -17,7 +17,6 @@ import {
useDeviceInsets, useDeviceInsets,
useSporeColors, useSporeColors,
} from 'ui/src' } from 'ui/src'
import { Plus } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
...@@ -25,6 +24,7 @@ import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' ...@@ -25,6 +24,7 @@ import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal' import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
...@@ -296,10 +296,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -296,10 +296,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress={onPressAccount} onPress={onPressAccount}
/> />
<TouchableArea hapticFeedback mt="$spacing16" onPress={onPressAddWallet}> <TouchableArea hapticFeedback mt="$spacing16" onPress={onPressAddWallet}>
<Flex row alignItems="center" gap="$spacing16" ml="$spacing24"> <Flex row alignItems="center" gap="$spacing8" ml="$spacing24">
<Flex borderColor="$surface3" borderRadius="$roundedFull" borderWidth={1} p="$spacing8"> <PlusCircle />
<Plus color="$neutral2" size="$icon.12" strokeWidth={2} />
</Flex>
<Text color="$neutral2" variant="buttonLabel3"> <Text color="$neutral2" variant="buttonLabel3">
{t('account.wallet.button.add')} {t('account.wallet.button.add')}
</Text> </Text>
......
...@@ -544,7 +544,7 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -544,7 +544,7 @@ exports[`AccountSwitcher renders correctly 1`] = `
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"gap": 16, "gap": 8,
"marginLeft": 24, "marginLeft": 24,
} }
} }
...@@ -552,6 +552,8 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -552,6 +552,8 @@ exports[`AccountSwitcher renders correctly 1`] = `
<View <View
style={ style={
{ {
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderBottomColor": "#2222220D", "borderBottomColor": "#2222220D",
"borderBottomLeftRadius": 999999, "borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999, "borderBottomRightRadius": 999999,
...@@ -566,17 +568,27 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -566,17 +568,27 @@ exports[`AccountSwitcher renders correctly 1`] = `
"borderTopRightRadius": 999999, "borderTopRightRadius": 999999,
"borderTopWidth": 1, "borderTopWidth": 1,
"flexDirection": "column", "flexDirection": "column",
"height": 40,
"justifyContent": "center",
"paddingBottom": 8, "paddingBottom": 8,
"paddingLeft": 8, "paddingLeft": 8,
"paddingRight": 8, "paddingRight": 8,
"paddingTop": 8, "paddingTop": 8,
"shadowColor": "rgb(34,34,34)",
"shadowOffset": {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.050980392156862744,
"shadowRadius": 10,
"width": 40,
} }
} }
> >
<RNSVGSvgView <RNSVGSvgView
align="xMidYMid" align="xMidYMid"
bbHeight="12" bbHeight="16"
bbWidth="12" bbWidth="16"
fill="none" fill="none"
focusable={false} focusable={false}
meetOrSlice={0} meetOrSlice={0}
...@@ -591,13 +603,13 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -591,13 +603,13 @@ exports[`AccountSwitcher renders correctly 1`] = `
}, },
{ {
"color": "#7D7D7D", "color": "#7D7D7D",
"height": 12, "height": 16,
"width": 12, "width": 16,
}, },
{ {
"flex": 0, "flex": 0,
"height": 12, "height": 16,
"width": 12, "width": 16,
}, },
] ]
} }
......
...@@ -19,6 +19,7 @@ import { ChooseProfilePictureScreen } from 'src/features/unitags/ChooseProfilePi ...@@ -19,6 +19,7 @@ import { ChooseProfilePictureScreen } from 'src/features/unitags/ChooseProfilePi
import { ClaimUnitagScreen } from 'src/features/unitags/ClaimUnitagScreen' import { ClaimUnitagScreen } from 'src/features/unitags/ClaimUnitagScreen'
import { EditUnitagProfileScreen } from 'src/features/unitags/EditUnitagProfileScreen' import { EditUnitagProfileScreen } from 'src/features/unitags/EditUnitagProfileScreen'
import { UnitagConfirmationScreen } from 'src/features/unitags/UnitagConfirmationScreen' import { UnitagConfirmationScreen } from 'src/features/unitags/UnitagConfirmationScreen'
import { AppLoadingScreen } from 'src/screens/AppLoadingScreen'
import { DevScreen } from 'src/screens/DevScreen' import { DevScreen } from 'src/screens/DevScreen'
import { EducationScreen } from 'src/screens/EducationScreen' import { EducationScreen } from 'src/screens/EducationScreen'
import { ExploreScreen } from 'src/screens/ExploreScreen' import { ExploreScreen } from 'src/screens/ExploreScreen'
...@@ -28,6 +29,7 @@ import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen' ...@@ -28,6 +29,7 @@ import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen'
import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders' import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders'
import { HomeScreen } from 'src/screens/HomeScreen' import { HomeScreen } from 'src/screens/HomeScreen'
import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen' import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen'
import { OnDeviceRecoveryScreen } from 'src/screens/Import/OnDeviceRecoveryScreen'
import { RestoreCloudBackupLoadingScreen } from 'src/screens/Import/RestoreCloudBackupLoadingScreen' import { RestoreCloudBackupLoadingScreen } from 'src/screens/Import/RestoreCloudBackupLoadingScreen'
import { RestoreCloudBackupPasswordScreen } from 'src/screens/Import/RestoreCloudBackupPasswordScreen' import { RestoreCloudBackupPasswordScreen } from 'src/screens/Import/RestoreCloudBackupPasswordScreen'
import { RestoreCloudBackupScreen } from 'src/screens/Import/RestoreCloudBackupScreen' import { RestoreCloudBackupScreen } from 'src/screens/Import/RestoreCloudBackupScreen'
...@@ -235,6 +237,8 @@ export function OnboardingStackNavigator(): JSX.Element { ...@@ -235,6 +237,8 @@ export function OnboardingStackNavigator(): JSX.Element {
? SeedPhraseInputScreenV2 ? SeedPhraseInputScreenV2
: SeedPhraseInputScreen : SeedPhraseInputScreen
const isOnboardingKeyringEnabled = useFeatureFlag(FeatureFlags.OnboardingKeyring)
return ( return (
<OnboardingContextProvider> <OnboardingContextProvider>
<OnboardingStack.Navigator> <OnboardingStack.Navigator>
...@@ -251,6 +255,13 @@ export function OnboardingStackNavigator(): JSX.Element { ...@@ -251,6 +255,13 @@ export function OnboardingStackNavigator(): JSX.Element {
headerRightContainerStyle: { paddingRight: spacing.spacing16 }, headerRightContainerStyle: { paddingRight: spacing.spacing16 },
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}}> }}>
{isOnboardingKeyringEnabled && (
<OnboardingStack.Screen
component={AppLoadingScreen}
name={OnboardingScreens.AppLoading}
options={navOptions.noHeader}
/>
)}
<OnboardingStack.Screen <OnboardingStack.Screen
component={LandingScreen} component={LandingScreen}
name={OnboardingScreens.Landing} name={OnboardingScreens.Landing}
...@@ -295,6 +306,11 @@ export function OnboardingStackNavigator(): JSX.Element { ...@@ -295,6 +306,11 @@ export function OnboardingStackNavigator(): JSX.Element {
component={ImportMethodScreen} component={ImportMethodScreen}
name={OnboardingScreens.ImportMethod} name={OnboardingScreens.ImportMethod}
/> />
<OnboardingStack.Screen
component={OnDeviceRecoveryScreen}
name={OnboardingScreens.OnDeviceRecovery}
options={navOptions.noHeader}
/>
<OnboardingStack.Screen <OnboardingStack.Screen
component={RestoreCloudBackupLoadingScreen} component={RestoreCloudBackupLoadingScreen}
name={OnboardingScreens.RestoreCloudBackupLoading} name={OnboardingScreens.RestoreCloudBackupLoading}
......
...@@ -90,6 +90,7 @@ export type SharedUnitagScreenParams = { ...@@ -90,6 +90,7 @@ export type SharedUnitagScreenParams = {
} }
export type OnboardingStackParamList = { export type OnboardingStackParamList = {
[OnboardingScreens.AppLoading]: undefined
[OnboardingScreens.BackupManual]: OnboardingStackBaseParams [OnboardingScreens.BackupManual]: OnboardingStackBaseParams
[OnboardingScreens.BackupCloudPasswordCreate]: { [OnboardingScreens.BackupCloudPasswordCreate]: {
address: Address address: Address
...@@ -104,6 +105,7 @@ export type OnboardingStackParamList = { ...@@ -104,6 +105,7 @@ export type OnboardingStackParamList = {
// import // import
[OnboardingScreens.ImportMethod]: OnboardingStackBaseParams [OnboardingScreens.ImportMethod]: OnboardingStackBaseParams
[OnboardingScreens.OnDeviceRecovery]: OnboardingStackBaseParams & { mnemonicIds: Address[] }
[OnboardingScreens.RestoreCloudBackupLoading]: OnboardingStackBaseParams [OnboardingScreens.RestoreCloudBackupLoading]: OnboardingStackBaseParams
[OnboardingScreens.RestoreCloudBackup]: OnboardingStackBaseParams [OnboardingScreens.RestoreCloudBackup]: OnboardingStackBaseParams
[OnboardingScreens.RestoreCloudBackupPassword]: { [OnboardingScreens.RestoreCloudBackupPassword]: {
......
...@@ -30,12 +30,6 @@ import { ...@@ -30,12 +30,6 @@ import {
editAccountSaga, editAccountSaga,
editAccountSagaName, editAccountSagaName,
} from 'wallet/src/features/wallet/accounts/editAccountSaga' } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import {
createAccountActions,
createAccountReducer,
createAccountSaga,
createAccountSagaName,
} from 'wallet/src/features/wallet/create/createAccountSaga'
import { import {
createAccountsActions, createAccountsActions,
createAccountsReducer, createAccountsReducer,
...@@ -70,12 +64,6 @@ const sagas = [ ...@@ -70,12 +64,6 @@ const sagas = [
// All monitored sagas must be included here // All monitored sagas must be included here
export const monitoredSagas: Record<string, MonitoredSaga> = { export const monitoredSagas: Record<string, MonitoredSaga> = {
[createAccountSagaName]: {
name: createAccountSagaName,
wrappedSaga: createAccountSaga,
reducer: createAccountReducer,
actions: createAccountActions,
},
[createAccountsSagaName]: { [createAccountsSagaName]: {
name: createAccountsSagaName, name: createAccountsSagaName,
wrappedSaga: createAccountsSaga, wrappedSaga: createAccountsSaga,
......
...@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react' ...@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react'
import { MMKV } from 'react-native-mmkv' import { MMKV } from 'react-native-mmkv'
import { Storage, persistReducer, persistStore } from 'redux-persist' import { Storage, persistReducer, persistStore } from 'redux-persist'
import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations' import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations'
import { fiatOnRampAggregatorApi as sharedFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { isNonJestDev } from 'utilities/src/environment' import { isNonJestDev } from 'utilities/src/environment'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api' import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api'
...@@ -92,7 +93,11 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({ ...@@ -92,7 +93,11 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({
}, },
}) })
const middlewares: Middleware[] = [fiatOnRampApi.middleware, fiatOnRampAggregatorApi.middleware] const middlewares: Middleware[] = [
fiatOnRampApi.middleware,
fiatOnRampAggregatorApi.middleware,
sharedFiatOnRampAggregatorApi.middleware,
]
if (isNonJestDev) { if (isNonJestDev) {
const createDebugger = require('redux-flipper').default const createDebugger = require('redux-flipper').default
middlewares.push(createDebugger()) middlewares.push(createDebugger())
......
...@@ -10,11 +10,11 @@ import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' ...@@ -10,11 +10,11 @@ import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import { AnimatedFlex, Button, Flex, Text, useDeviceDimensions, useSporeColors } from 'ui/src' import { AnimatedFlex, Button, Flex, Text, useDeviceDimensions, useSporeColors } from 'ui/src'
import CameraScan from 'ui/src/assets/icons/camera-scan.svg' import CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, Photo } from 'ui/src/components/icons' import { Global, Photo } from 'ui/src/components/icons'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { Sentry } from 'utilities/src/logger/Sentry' import { Sentry } from 'utilities/src/logger/Sentry'
import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly' import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly'
import PasteButton from 'wallet/src/components/buttons/PasteButton' import PasteButton from 'wallet/src/components/buttons/PasteButton'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { openSettings } from 'wallet/src/utils/linking' import { openSettings } from 'wallet/src/utils/linking'
type QRCodeScannerProps = { type QRCodeScannerProps = {
......
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, CheckBox, Flex, Text } from 'ui/src' import { Button, CheckBox, Flex, Text } from 'ui/src'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
export function RemoveLastMnemonicWalletFooter({ export function RemoveLastMnemonicWalletFooter({
onPress, onPress,
......
...@@ -12,12 +12,14 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet ...@@ -12,12 +12,14 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState' import { selectModalState } from 'src/features/modals/selectModalState'
import { AnimatedFlex, Button, ColorTokens, Flex, Text, ThemeKeys, useSporeColors } from 'ui/src' import { AnimatedFlex, Button, ColorTokens, Flex, Text, ThemeKeys, useSporeColors } from 'ui/src'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { iconSizes, opacify } from 'ui/src/theme' import { iconSizes, opacify } from 'ui/src/theme'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' import { logger } from 'utilities/src/logger/logger'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { import {
EditAccountAction, EditAccountAction,
editAccountActions, editAccountActions,
...@@ -74,15 +76,34 @@ export function RemoveWalletModal(): JSX.Element | null { ...@@ -74,15 +76,34 @@ export function RemoveWalletModal(): JSX.Element | null {
}) })
} }
const accountsToRemove = isReplacing ? associatedAccounts : account ? [account] : [] const accountsToRemove = isReplacing ? associatedAccounts : account ? [account] : []
accountsToRemove.forEach(({ address: accAddress, pushNotificationsEnabled }) => {
// Remove mnemonic if it's replacing or there is only one signer mnemonic account left
if (isReplacing || associatedAccounts.length === 1) {
if (associatedAccounts[0]) {
Keyring.removeMnemonic(associatedAccounts[0].mnemonicId)
.then(() => {
// Only remove accounts if mnemonic is successfully removed
dispatch( dispatch(
editAccountActions.trigger({ editAccountActions.trigger({
type: EditAccountAction.Remove, type: EditAccountAction.Remove,
address: accAddress, accounts: accountsToRemove,
notificationsEnabled: !!pushNotificationsEnabled,
}) })
) )
}) })
.catch((error) => {
logger.error(error, {
tags: { file: 'RemoveWalletModal', function: 'Keyring.removeMnemonic' },
})
})
}
} else {
dispatch(
editAccountActions.trigger({
type: EditAccountAction.Remove,
accounts: accountsToRemove,
})
)
}
onClose() onClose()
setInProgress(false) setInProgress(false)
......
...@@ -13,14 +13,7 @@ import { ...@@ -13,14 +13,7 @@ import {
TAB_VIEW_SCROLL_THROTTLE, TAB_VIEW_SCROLL_THROTTLE,
TabProps, TabProps,
} from 'src/components/layout/TabHelpers' } from 'src/components/layout/TabHelpers'
import { import { AnimatedFlex, Flex, Loader, useDeviceInsets, useSporeColors } from 'ui/src'
AnimatedFlex,
Flex,
Loader,
useDeviceDimensions,
useDeviceInsets,
useSporeColors,
} from 'ui/src'
import { zIndices } from 'ui/src/theme' import { zIndices } from 'ui/src/theme'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
...@@ -88,7 +81,7 @@ export const TokenBalanceListInner = forwardRef< ...@@ -88,7 +81,7 @@ export const TokenBalanceListInner = forwardRef<
const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext() const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const { onContentSizeChange, adaptiveFooter, footerHeight } = useAdaptiveFooter( const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(
containerProps?.contentContainerStyle containerProps?.contentContainerStyle
) )
...@@ -145,19 +138,39 @@ export const TokenBalanceListInner = forwardRef< ...@@ -145,19 +138,39 @@ export const TokenBalanceListInner = forwardRef<
// In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change. // In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change.
// That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes. // That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes.
const renderItem = useCallback( const renderItem = useCallback(
({ item }: { item: TokenBalanceListRow }): JSX.Element => { ({ item }: { item: TokenBalanceListRow }): JSX.Element => <TokenBalanceItemRow item={item} />,
return <TokenBalanceItemRow footerHeight={footerHeight} item={item} /> []
},
[footerHeight]
) )
const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, [])
const ListEmptyComponent = useMemo(() => { const ListEmptyComponent = useMemo(() => {
if (!balancesById) {
return ( return (
<Flex grow px="$spacing24" style={containerProps?.emptyContainerStyle}> <Flex grow px="$spacing24">
{empty} {empty}
</Flex> </Flex>
) )
}, [containerProps?.emptyContainerStyle, empty]) }
if (isNonPollingRequestInFlight(networkStatus)) {
return (
<Flex px="$spacing24">
<Loader.Token withPrice repeat={6} />
</Flex>
)
}
return (
<Flex pt="$spacing24">
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
)
}, [balancesById, empty, t, networkStatus, refetch])
const hasError = isError(networkStatus, !!balancesById) const hasError = isError(networkStatus, !!balancesById)
...@@ -188,6 +201,8 @@ export const TokenBalanceListInner = forwardRef< ...@@ -188,6 +201,8 @@ export const TokenBalanceListInner = forwardRef<
[] []
) )
const data = balancesById ? (isFocused ? rows : cachedRows) : undefined
// Note: `PerformanceView` must wrap the entire return statement to properly track interactive states. // Note: `PerformanceView` must wrap the entire return statement to properly track interactive states.
return ( return (
<ReactNavigationPerformanceView <ReactNavigationPerformanceView
...@@ -196,21 +211,6 @@ export const TokenBalanceListInner = forwardRef< ...@@ -196,21 +211,6 @@ export const TokenBalanceListInner = forwardRef<
// Marks the home screen as interactive when balances are defined // Marks the home screen as interactive when balances are defined
MobileScreens.Home MobileScreens.Home
}> }>
{!balancesById ? (
isNonPollingRequestInFlight(networkStatus) ? (
<Flex px="$spacing24" style={containerProps?.loadingContainerStyle}>
<Loader.Token withPrice repeat={6} />
</Flex>
) : (
<Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}>
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
)
) : (
<List <List
ref={ref as never} ref={ref as never}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
...@@ -219,9 +219,10 @@ export const TokenBalanceListInner = forwardRef< ...@@ -219,9 +219,10 @@ export const TokenBalanceListInner = forwardRef<
ListFooterComponentStyle={ListFooterComponentStyle} ListFooterComponentStyle={ListFooterComponentStyle}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
contentContainerStyle={containerProps?.contentContainerStyle} contentContainerStyle={containerProps?.contentContainerStyle}
data={isFocused ? rows : cachedRows} data={data}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
initialNumToRender={20} initialNumToRender={20}
keyExtractor={keyExtractor}
maxToRenderPerBatch={20} maxToRenderPerBatch={20}
refreshControl={refreshControl} refreshControl={refreshControl}
refreshing={refreshing} refreshing={refreshing}
...@@ -236,20 +237,15 @@ export const TokenBalanceListInner = forwardRef< ...@@ -236,20 +237,15 @@ export const TokenBalanceListInner = forwardRef<
onScroll={scrollHandler} onScroll={scrollHandler}
onScrollEndDrag={containerProps?.onScrollEndDrag} onScrollEndDrag={containerProps?.onScrollEndDrag}
/> />
)}
</ReactNavigationPerformanceView> </ReactNavigationPerformanceView>
) )
}) })
const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
item, item,
footerHeight,
}: { }: {
item: TokenBalanceListRow item: TokenBalanceListRow
footerHeight: Animated.SharedValue<number>
}) { }) {
const { fullHeight } = useDeviceDimensions()
const { const {
balancesById, balancesById,
hiddenTokensCount, hiddenTokensCount,
...@@ -266,9 +262,6 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ ...@@ -266,9 +262,6 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
isExpanded={hiddenTokensExpanded} isExpanded={hiddenTokensExpanded}
numHidden={hiddenTokensCount} numHidden={hiddenTokensCount}
onPress={(): void => { onPress={(): void => {
if (hiddenTokensExpanded) {
footerHeight.value = fullHeight
}
setHiddenTokensExpanded(!hiddenTokensExpanded) setHiddenTokensExpanded(!hiddenTokensExpanded)
}} }}
/> />
......
...@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next' ...@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { SendButton } from './SendButton' import { SendButton } from './SendButton'
/** /**
......
import React from 'react' import React from 'react'
import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' import { Flex, flexStyles, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { import {
SafetyLevel, SafetyLevel,
TokenDetailsScreenQuery, TokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'wallet/src/components/icons/WarningIcon' import WarningIcon from 'wallet/src/components/icons/WarningIcon'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
......
...@@ -8,11 +8,11 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga' ...@@ -8,11 +8,11 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { Button, Flex, Text } from 'ui/src' import { Button, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { CHAIN_INFO } from 'wallet/src/constants/chains' import { CHAIN_INFO } from 'wallet/src/constants/chains'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
......
...@@ -2,9 +2,9 @@ import React from 'react' ...@@ -2,9 +2,9 @@ import React from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { DappInfo } from 'uniswap/src/types/walletConnect' import { DappInfo } from 'uniswap/src/types/walletConnect'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
......
...@@ -2,13 +2,13 @@ import React from 'react' ...@@ -2,13 +2,13 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useUSDValue } from 'wallet/src/features/gas/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount'
export function SpendingDetails({ export function SpendingDetails({
......
...@@ -5,12 +5,12 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated' ...@@ -5,12 +5,12 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay'
import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice' import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useIsDarkMode } from 'ui/src' import { Flex, Text, useIsDarkMode } from 'ui/src'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { NetworkFee } from 'wallet/src/components/network/NetworkFee' import { NetworkFee } from 'wallet/src/components/network/NetworkFee'
import { CHAIN_INFO } from 'wallet/src/constants/chains' import { CHAIN_INFO } from 'wallet/src/constants/chains'
import { GasFeeResult } from 'wallet/src/features/gas/types' import { GasFeeResult } from 'wallet/src/features/gas/types'
......
...@@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next' ...@@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'
import { Flex, Separator, Text, useSporeColors } from 'ui/src' import { Flex, Separator, Text, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg' import Check from 'ui/src/assets/icons/check.svg'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo'
import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal'
import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO } from 'wallet/src/constants/chains' import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO } from 'wallet/src/constants/chains'
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
} from 'src/features/deepLinking/constants' } from 'src/features/deepLinking/constants'
import { DynamicConfigs } from 'uniswap/src/features/gating/configs' import { DynamicConfigs } from 'uniswap/src/features/gating/configs'
import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' import { useDynamicConfig } from 'uniswap/src/features/gating/hooks'
import { RPCType } from 'uniswap/src/types/chains'
import { import {
EthMethod, EthMethod,
EthTransaction, EthTransaction,
...@@ -16,7 +17,6 @@ import { ...@@ -16,7 +17,6 @@ import {
UwULinkRequest, UwULinkRequest,
} from 'uniswap/src/types/walletConnect' } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { RPCType } from 'wallet/src/constants/chains'
import { AssetType } from 'wallet/src/entities/assets' import { AssetType } from 'wallet/src/entities/assets'
import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { ContractManager } from 'wallet/src/features/contracts/ContractManager'
import { ProviderManager } from 'wallet/src/features/providers' import { ProviderManager } from 'wallet/src/features/providers'
......
import { makeMutable } from 'react-native-reanimated' import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store' import configureMockStore from 'redux-mock-store'
import { act, cleanup, fireEvent, render, waitFor } from 'src/test/test-utils' import { act, cleanup, fireEvent, render, waitFor } from 'src/test/test-utils'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
import { Language } from 'wallet/src/features/language/constants' import { Language } from 'wallet/src/features/language/constants'
import { import {
...@@ -12,7 +13,6 @@ import { ...@@ -12,7 +13,6 @@ import {
tokenProjectMarket, tokenProjectMarket,
} from 'wallet/src/test/fixtures' } from 'wallet/src/test/fixtures'
import { queryResolvers } from 'wallet/src/test/utils' import { queryResolvers } from 'wallet/src/test/utils'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import FavoriteTokenCard, { FavoriteTokenCardProps } from './FavoriteTokenCard' import FavoriteTokenCard, { FavoriteTokenCardProps } from './FavoriteTokenCard'
const mockedNavigation = { const mockedNavigation = {
......
...@@ -11,12 +11,13 @@ import { disableOnPress } from 'src/utils/disableOnPress' ...@@ -11,12 +11,13 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks' import { usePollOnFocusOnly } from 'src/utils/hooks'
import { AnimatedFlex, AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text } from 'ui/src' import { AnimatedFlex, AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text } from 'ui/src'
import { borderRadii, imageSizes } from 'ui/src/theme' import { borderRadii, imageSizes } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { SectionName } from 'uniswap/src/features/telemetry/constants' import { SectionName } from 'uniswap/src/features/telemetry/constants'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { RelativeChange } from 'wallet/src/components/text/RelativeChange' import { RelativeChange } from 'wallet/src/components/text/RelativeChange'
import { PollingInterval } from 'wallet/src/constants/misc' import { PollingInterval } from 'wallet/src/constants/misc'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
...@@ -24,7 +25,6 @@ import { fromGraphQLChain } from 'wallet/src/features/chains/utils' ...@@ -24,7 +25,6 @@ import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
import { removeFavoriteToken } from 'wallet/src/features/favorites/slice' import { removeFavoriteToken } from 'wallet/src/features/favorites/slice'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114 export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114
......
...@@ -6,11 +6,11 @@ import { useExploreTokenContextMenu } from 'src/components/explore/hooks' ...@@ -6,11 +6,11 @@ import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { TokenMetadata } from 'src/components/tokens/TokenMetadata' import { TokenMetadata } from 'src/components/tokens/TokenMetadata'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { RelativeChange } from 'wallet/src/components/text/RelativeChange' import { RelativeChange } from 'wallet/src/components/text/RelativeChange'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
......
...@@ -5,10 +5,10 @@ import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' ...@@ -5,10 +5,10 @@ import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ElementName, MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { ElementName, MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'wallet/src/components/icons/WarningIcon' import WarningIcon from 'wallet/src/components/icons/WarningIcon'
import { SearchContext } from 'wallet/src/features/search/SearchContext' import { SearchContext } from 'wallet/src/features/search/SearchContext'
import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult'
......
...@@ -2,7 +2,7 @@ import React from 'react' ...@@ -2,7 +2,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'ui/src' import { Button } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons' import { InfoCircleFilled } from 'ui/src/components/icons'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
interface FiatOnRampCtaButtonProps { interface FiatOnRampCtaButtonProps {
onPress: () => void onPress: () => void
......
...@@ -4,8 +4,8 @@ import { StyleSheet } from 'react-native' ...@@ -4,8 +4,8 @@ import { StyleSheet } from 'react-native'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { concatStrings } from 'utilities/src/primitives/string' import { concatStrings } from 'utilities/src/primitives/string'
import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getOptionalServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { getOptionalServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
......
...@@ -6,7 +6,7 @@ import { useAppDispatch } from 'src/app/hooks' ...@@ -6,7 +6,7 @@ import { useAppDispatch } from 'src/app/hooks'
import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList' import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { WalletEmptyState } from 'src/components/home/WalletEmptyState' import { WalletEmptyState } from 'src/components/home/WalletEmptyState'
import { TabContentProps, TabProps } from 'src/components/layout/TabHelpers' import { TabProps } from 'src/components/layout/TabHelpers'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { NoTokens } from 'ui/src/components/icons' import { NoTokens } from 'ui/src/components/icons'
...@@ -20,8 +20,6 @@ import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceL ...@@ -20,8 +20,6 @@ import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceL
export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances] export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances]
// ignore ref type
export const TokensTab = memo( export const TokensTab = memo(
forwardRef<FlatList<TokenBalanceListRow>, TabProps & { isExternalProfile?: boolean }>( forwardRef<FlatList<TokenBalanceListRow>, TabProps & { isExternalProfile?: boolean }>(
function _TokensTab( function _TokensTab(
...@@ -50,17 +48,6 @@ export const TokensTab = memo( ...@@ -50,17 +48,6 @@ export const TokensTab = memo(
[startProfilerTimer, tokenDetailsNavigation] [startProfilerTimer, tokenDetailsNavigation]
) )
// Update list empty styling based on which empty state is used
const formattedContainerProps: TabContentProps | undefined = useMemo(() => {
if (!containerProps) {
return undefined
}
if (!isExternalProfile) {
return { ...containerProps, emptyContainerStyle: {} }
}
return containerProps
}, [containerProps, isExternalProfile])
const onPressAction = useCallback((): void => { const onPressAction = useCallback((): void => {
dispatch( dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
...@@ -85,7 +72,7 @@ export const TokensTab = memo( ...@@ -85,7 +72,7 @@ export const TokensTab = memo(
<Flex grow backgroundColor="$surface1"> <Flex grow backgroundColor="$surface1">
<TokenBalanceList <TokenBalanceList
ref={ref} ref={ref}
containerProps={formattedContainerProps} containerProps={containerProps}
empty={renderEmpty} empty={renderEmpty}
headerHeight={headerHeight} headerHeight={headerHeight}
isExternalProfile={isExternalProfile} isExternalProfile={isExternalProfile}
......
...@@ -12,9 +12,9 @@ import { ...@@ -12,9 +12,9 @@ import {
} from 'react-native' } from 'react-native'
import Animated, { SharedValue } from 'react-native-reanimated' import Animated, { SharedValue } from 'react-native-reanimated'
import { Route } from 'react-native-tab-view' import { Route } from 'react-native-tab-view'
import { PendingNotificationBadge } from 'src/features/notifications/PendingNotificationBadge'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { colorsLight, spacing } from 'ui/src/theme' import { colorsLight, spacing } from 'ui/src/theme'
import { PendingNotificationBadge } from 'wallet/src/features/notifications/components/PendingNotificationBadge'
export const TAB_VIEW_SCROLL_THROTTLE = 16 export const TAB_VIEW_SCROLL_THROTTLE = 16
export const TAB_BAR_HEIGHT = 48 export const TAB_BAR_HEIGHT = 48
...@@ -97,7 +97,7 @@ export type TabContentProps = Partial<FlatListProps<unknown>> & { ...@@ -97,7 +97,7 @@ export type TabContentProps = Partial<FlatListProps<unknown>> & {
scrollEventThrottle?: number scrollEventThrottle?: number
} }
export const renderTabLabel = ({ export const TabLabel = ({
route, route,
focused, focused,
isExternalProfile, isExternalProfile,
......
import React from 'react' import React from 'react'
import { Flex, Text } from 'ui/src' import { Flex } from 'ui/src'
const ROW_COUNT = 6
const LEFT_COLUMN_INDEXES = [1, 2, 3, 4, 5, 6]
const RIGHT_COLUMN_INDEXES = [7, 8, 9, 10, 11, 12]
export function HiddenMnemonicWordView(): JSX.Element { export function HiddenMnemonicWordView(): JSX.Element {
return ( return (
<Flex <Flex
row row
alignItems="stretch" alignItems="stretch"
backgroundColor="$surface1" backgroundColor="$surface2"
gap="$spacing24" borderRadius="$rounded20"
height="50%" gap="$spacing36"
justifyContent="space-evenly" height="40%"
mt="$spacing16" mt="$spacing16"
px="$spacing36"> px="$spacing32"
py="$spacing24">
<Flex grow justifyContent="space-between"> <Flex grow justifyContent="space-between">
<HiddenWordViewColumn indexes={LEFT_COLUMN_INDEXES} /> <HiddenWordViewColumn />
</Flex> </Flex>
<Flex grow justifyContent="space-between"> <Flex grow justifyContent="space-between">
<HiddenWordViewColumn indexes={RIGHT_COLUMN_INDEXES} /> <HiddenWordViewColumn />
</Flex> </Flex>
</Flex> </Flex>
) )
} }
function HiddenWordViewColumn({ indexes }: { indexes: number[] }): JSX.Element { function HiddenWordViewColumn(): JSX.Element {
return ( return (
<> <>
{indexes.map((value) => ( {new Array(ROW_COUNT).fill(0).map((_, idx) => (
<Flex <Flex key={idx} backgroundColor="$surface3" borderRadius="$rounded20" height={10} />
key={value}
row
alignItems="center"
gap="$spacing16"
justifyContent="space-between"
px="$spacing12"
py="$spacing16">
<Text color="$neutral2">{value}</Text>
<Flex fill backgroundColor="$neutral3" borderRadius="$rounded20" height={9} />
</Flex>
))} ))}
</> </>
) )
......
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { requireNativeComponent, StyleProp, ViewProps } from 'react-native' import { requireNativeComponent, StyleProp, ViewProps } from 'react-native'
import { FlexProps, flexStyles, HiddenFromScreenReaders, useDeviceDimensions } from 'ui/src' import { FlexProps, flexStyles, HiddenFromScreenReaders, useDeviceDimensions } from 'ui/src'
interface NativeMnemonicConfirmationProps { interface NativeMnemonicConfirmationProps {
mnemonicId: Address mnemonicId: Address
shouldShowSmallText: boolean shouldShowSmallText: boolean
selectedWordPlaceholder: string
onConfirmComplete: () => void onConfirmComplete: () => void
} }
...@@ -22,12 +24,14 @@ const mnemonicConfirmationStyle: StyleProp<FlexProps> = { ...@@ -22,12 +24,14 @@ const mnemonicConfirmationStyle: StyleProp<FlexProps> = {
} }
export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element { export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element {
const { t } = useTranslation()
const { fullHeight } = useDeviceDimensions() const { fullHeight } = useDeviceDimensions()
const shouldShowSmallText = fullHeight < 700 const shouldShowSmallText = fullHeight < 700
return ( return (
<HiddenFromScreenReaders style={flexStyles.fill}> <HiddenFromScreenReaders style={flexStyles.fill}>
<NativeMnemonicConfirmation <NativeMnemonicConfirmation
selectedWordPlaceholder={t('onboarding.backup.manual.selectedWordPlaceholder')}
shouldShowSmallText={shouldShowSmallText} shouldShowSmallText={shouldShowSmallText}
style={mnemonicConfirmationStyle} style={mnemonicConfirmationStyle}
{...props} {...props}
......
import React from 'react' import React, { useState } from 'react'
import { requireNativeComponent, StyleSheet, ViewProps } from 'react-native' import { useTranslation } from 'react-i18next'
import { flexStyles, HiddenFromScreenReaders } from 'ui/src' import { NativeSyntheticEvent, StyleSheet, ViewProps, requireNativeComponent } from 'react-native'
import { Flex, HiddenFromScreenReaders, Text, flexStyles } from 'ui/src'
import { GraduationCap } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme'
type HeightMeasuredEvent = {
height: number
}
interface NativeMnemonicDisplayProps { interface NativeMnemonicDisplayProps {
mnemonicId: Address copyText: string
copiedText: string
mnemonicId: string
onHeightMeasured: (event: NativeSyntheticEvent<HeightMeasuredEvent>) => void
} }
const NativeMnemonicDisplay = requireNativeComponent<NativeMnemonicDisplayProps>('MnemonicDisplay') const NativeMnemonicDisplay = requireNativeComponent<NativeMnemonicDisplayProps>('MnemonicDisplay')
type MnemonicDisplayProps = ViewProps & NativeMnemonicDisplayProps type MnemonicDisplayProps = ViewProps & Pick<NativeMnemonicDisplayProps, 'mnemonicId'>
const styles = StyleSheet.create({
mnemonicDisplay: {
flex: 1,
flexGrow: 1,
},
})
export function MnemonicDisplay(props: MnemonicDisplayProps): JSX.Element { export function MnemonicDisplay(props: MnemonicDisplayProps): JSX.Element {
const { t } = useTranslation()
const [height, setHeight] = useState(0)
return ( return (
<HiddenFromScreenReaders style={flexStyles.fill}> <HiddenFromScreenReaders style={flexStyles.fill}>
<NativeMnemonicDisplay style={styles.mnemonicDisplay} {...props} /> <NativeMnemonicDisplay
copiedText={t('common.button.copied')}
copyText={t('common.button.copy')}
style={[styles.mnemonicDisplay, { maxHeight: height }]}
onHeightMeasured={(event) => {
setHeight(event.nativeEvent.height)
}}
{...props}
/>
<Flex
row
alignItems="center"
backgroundColor="$surface2"
borderRadius="$rounded16"
// Hide the component rendered below the native mnemonic display
// until the height is measured
display={height ? 'flex' : 'none'}
gap="$spacing8"
p="$spacing16">
<GraduationCap color="$neutral2" size="$icon.20" />
<Flex shrink>
<Text color="$neutral2" variant="body4">
{t('onboarding.backup.manual.banner')}
</Text>
</Flex>
</Flex>
</HiddenFromScreenReaders> </HiddenFromScreenReaders>
) )
} }
const styles = StyleSheet.create({
mnemonicDisplay: {
// Set flex: 1 to prevent component from collapsing before it is measured
flex: 1,
marginBottom: spacing.spacing16,
},
})
...@@ -59,7 +59,6 @@ export function CloudBackupPasswordForm({ ...@@ -59,7 +59,6 @@ export function CloudBackupPasswordForm({
const onPasswordSubmitEditing = (): void => { const onPasswordSubmitEditing = (): void => {
if (!isConfirmation && !isStrongPassword) { if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return return
} }
if (isConfirmation && passwordToConfirm !== password) { if (isConfirmation && passwordToConfirm !== password) {
...@@ -71,10 +70,6 @@ export function CloudBackupPasswordForm({ ...@@ -71,10 +70,6 @@ export function CloudBackupPasswordForm({
} }
const onPressNext = (): void => { const onPressNext = (): void => {
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) { if (isConfirmation && passwordToConfirm !== password) {
setError(PasswordErrors.PasswordsDoNotMatch) setError(PasswordErrors.PasswordsDoNotMatch)
return return
...@@ -86,9 +81,7 @@ export function CloudBackupPasswordForm({ ...@@ -86,9 +81,7 @@ export function CloudBackupPasswordForm({
} }
let errorText = '' let errorText = ''
if (error === PasswordErrors.WeakPassword) { if (error === PasswordErrors.PasswordsDoNotMatch) {
errorText = t('settings.setting.backup.password.error.weak')
} else if (error === PasswordErrors.PasswordsDoNotMatch) {
errorText = t('settings.setting.backup.password.error.mismatch') errorText = t('settings.setting.backup.password.error.mismatch')
} else if (error) { } else if (error) {
// use the upstream zxcvbn error message // use the upstream zxcvbn error message
......
import React from 'react'
import { Image, StyleProp, StyleSheet, ViewStyle } from 'react-native'
import { Flex, useDeviceDimensions, useIsDarkMode } from 'ui/src'
import { UNISWAP_LOGO_LARGE } from 'ui/src/assets'
import { isAndroid } from 'utilities/src/platform'
export const SPLASH_SCREEN = { uri: 'SplashScreen' }
export function SplashScreen({ style }: { style?: StyleProp<ViewStyle> }): JSX.Element {
const dimensions = useDeviceDimensions()
const isDarkMode = useIsDarkMode()
return (
<Flex
alignItems="center"
backgroundColor={isDarkMode ? '$surface1' : '$sporeWhite'}
justifyContent={isAndroid ? 'center' : undefined}
pointerEvents="none"
style={style}>
{/* Android has a different implementation, which is not set in stone yet, so skipping it for now */}
{isAndroid ? (
<Image source={UNISWAP_LOGO_LARGE} style={fixedStyle.logoStyle} />
) : (
<Image
resizeMode="contain"
source={SPLASH_SCREEN}
style={{
width: dimensions.fullWidth,
height: dimensions.fullHeight,
}}
/>
)}
</Flex>
)
}
const fixedStyle = StyleSheet.create({
logoStyle: {
height: 180,
width: 165,
},
})
import React from 'react' import React from 'react'
import { Image, StyleSheet } from 'react-native'
import { Modal } from 'src/components/modals/Modal' import { Modal } from 'src/components/modals/Modal'
import { SplashScreen } from 'src/features/appLoading/SplashScreen'
import { useLockScreenContext } from 'src/features/authentication/lockScreenContext' import { useLockScreenContext } from 'src/features/authentication/lockScreenContext'
import { useBiometricPrompt } from 'src/features/biometrics/hooks' import { useBiometricPrompt } from 'src/features/biometrics/hooks'
import { Flex, TouchableArea, useDeviceDimensions, useDeviceInsets, useIsDarkMode } from 'ui/src' import { TouchableArea, useDeviceDimensions, useDeviceInsets } from 'ui/src'
import { UNISWAP_LOGO_LARGE } from 'ui/src/assets'
import { isAndroid } from 'utilities/src/platform'
export const SPLASH_SCREEN = { uri: 'SplashScreen' } export const SPLASH_SCREEN = { uri: 'SplashScreen' }
...@@ -14,7 +12,6 @@ export function LockScreenModal(): JSX.Element | null { ...@@ -14,7 +12,6 @@ export function LockScreenModal(): JSX.Element | null {
const { trigger } = useBiometricPrompt(() => setIsLockScreenVisible(false)) const { trigger } = useBiometricPrompt(() => setIsLockScreenVisible(false))
const insets = useDeviceInsets() const insets = useDeviceInsets()
const dimensions = useDeviceDimensions() const dimensions = useDeviceDimensions()
const isDarkMode = useIsDarkMode()
if (!isLockScreenVisible) { if (!isLockScreenVisible) {
return null return null
...@@ -33,38 +30,14 @@ export function LockScreenModal(): JSX.Element | null { ...@@ -33,38 +30,14 @@ export function LockScreenModal(): JSX.Element | null {
transparent={false} transparent={false}
width="100%"> width="100%">
<TouchableArea onPress={(): Promise<void> => trigger()}> <TouchableArea onPress={(): Promise<void> => trigger()}>
<Flex <SplashScreen
alignItems="center"
backgroundColor={isDarkMode ? '$surface1' : '$sporeWhite'}
justifyContent={isAndroid ? 'center' : undefined}
pointerEvents="none"
style={{ style={{
width: dimensions.fullWidth, width: dimensions.fullWidth,
height: dimensions.fullHeight, height: dimensions.fullHeight,
paddingBottom: insets.bottom, paddingBottom: insets.bottom,
}}>
{/* Android has a different implementation, which is not set in stone yet, so skipping it for now */}
{isAndroid ? (
<Image source={UNISWAP_LOGO_LARGE} style={style.logoStyle} />
) : (
<Image
resizeMode="contain"
source={SPLASH_SCREEN}
style={{
width: dimensions.fullWidth,
height: dimensions.fullHeight,
}} }}
/> />
)}
</Flex>
</TouchableArea> </TouchableArea>
</Modal> </Modal>
) )
} }
const style = StyleSheet.create({
logoStyle: {
height: 180,
width: 165,
},
})
import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
export interface ExchangeTransferModalState { export interface ExchangeTransferModalState {
serviceProvider: FORServiceProvider serviceProvider: FORServiceProvider
......
...@@ -5,8 +5,8 @@ import { useAppDispatch } from 'src/app/hooks' ...@@ -5,8 +5,8 @@ import { useAppDispatch } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage' import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
......
...@@ -9,33 +9,20 @@ import { ...@@ -9,33 +9,20 @@ import {
} from 'react-native' } from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler' import { TouchableOpacity } from 'react-native-gesture-handler'
import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { import { AnimatedFlex, ColorTokens, Flex, HapticFeedback, Text, useSporeColors } from 'ui/src'
AnimatedFlex, import { fonts, spacing } from 'ui/src/theme'
ColorTokens, import { Pill } from 'uniswap/src/components/pill/Pill'
Flex, import { SelectTokenButton } from 'uniswap/src/features/fiatOnRamp/SelectTokenButton'
HapticFeedback,
Text,
TouchableArea,
useSporeColors,
} from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons'
import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { AmountInput } from 'wallet/src/components/input/AmountInput' import { AmountInput } from 'wallet/src/components/input/AmountInput'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { Pill } from 'wallet/src/components/text/Pill'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { errorShakeAnimation } from 'wallet/src/utils/animations' import { errorShakeAnimation } from 'wallet/src/utils/animations'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing'
import { useFormatExactCurrencyAmount } from './hooks'
const MAX_INPUT_FONT_SIZE = 56 const MAX_INPUT_FONT_SIZE = 56
const MIN_INPUT_FONT_SIZE = 32 const MIN_INPUT_FONT_SIZE = 32
...@@ -146,6 +133,11 @@ export function FiatOnRampAmountSection({ ...@@ -146,6 +133,11 @@ export function FiatOnRampAmountSection({
// Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms // Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms
const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2) const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2)
const formattedAmount = useFormatExactCurrencyAmount(
quoteAmount.toString(),
currency.currencyInfo?.currency
)
return ( return (
<Flex onLayout={onInputPanelLayout}> <Flex onLayout={onInputPanelLayout}>
<Flex <Flex
...@@ -195,11 +187,11 @@ export function FiatOnRampAmountSection({ ...@@ -195,11 +187,11 @@ export function FiatOnRampAmountSection({
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
/> />
</AnimatedFlex> </AnimatedFlex>
{currency.currencyInfo && ( {currency.currencyInfo && formattedAmount && (
<SelectTokenButton <SelectTokenButton
amount={quoteAmount}
amountReady={quoteCurrencyAmountReady} amountReady={quoteCurrencyAmountReady}
disabled={notAvailableInThisRegion} disabled={notAvailableInThisRegion}
formattedAmount={formattedAmount}
loading={selectTokenLoading} loading={selectTokenLoading}
selectedCurrencyInfo={currency.currencyInfo} selectedCurrencyInfo={currency.currencyInfo}
onPress={onTokenSelectorPress} onPress={onTokenSelectorPress}
...@@ -229,58 +221,6 @@ export function FiatOnRampAmountSection({ ...@@ -229,58 +221,6 @@ export function FiatOnRampAmountSection({
) )
} }
interface SelectTokenButtonProps {
onPress: () => void
selectedCurrencyInfo: CurrencyInfo
amount: number
amountReady?: boolean
disabled?: boolean
loading?: boolean
}
function SelectTokenButton({
selectedCurrencyInfo,
onPress,
amount,
amountReady,
disabled,
loading,
}: SelectTokenButtonProps): JSX.Element {
const formattedAmount = useFormatExactCurrencyAmount(
amount.toString(),
selectedCurrencyInfo.currency
)
const textColor = !amountReady || disabled || loading ? '$neutral3' : '$neutral2'
return (
<TouchableArea
hapticFeedback
borderRadius="$roundedFull"
disabled={disabled}
testID={ElementName.TokenSelectorToggle}
onPress={onPress}>
<Flex centered row flexDirection="row" gap="$none" p="$spacing4">
{loading ? (
<SpinningLoader />
) : (
<CurrencyLogo
currencyInfo={selectedCurrencyInfo}
networkLogoBorderWidth={spacing.spacing1}
size={iconSizes.icon24}
/>
)}
<Text color={textColor} pl="$spacing8" variant="body1">
{formattedAmount}
</Text>
<Text color={textColor} pl="$spacing1" variant="body1">
{getSymbolDisplayText(selectedCurrencyInfo.currency.symbol)}
</Text>
<RotatableChevron color={textColor} direction="end" height={iconSizes.icon16} />
</Flex>
</TouchableArea>
)
}
// Predefined amount is only supported for certain currencies // Predefined amount is only supported for certain currencies
function PredefinedAmount({ function PredefinedAmount({
amount, amount,
......
...@@ -5,10 +5,10 @@ import React, { createContext, useContext, useState } from 'react' ...@@ -5,10 +5,10 @@ import React, { createContext, useContext, useState } from 'react'
import { SectionListData } from 'react-native' import { SectionListData } from 'react-native'
import { getCountry } from 'react-native-localize' import { getCountry } from 'react-native-localize'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { FORQuote, FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { getNativeAddress } from 'wallet/src/constants/addresses' import { getNativeAddress } from 'wallet/src/constants/addresses'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { buildCurrencyId } from 'wallet/src/utils/currencyId' import { buildCurrencyId } from 'wallet/src/utils/currencyId'
......
...@@ -17,14 +17,14 @@ import { ...@@ -17,14 +17,14 @@ import {
} from 'ui/src' } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg' import Check from 'ui/src/assets/icons/check.svg'
import { fonts, iconSizes, spacing } from 'ui/src/theme' import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { useFiatOnRampAggregatorCountryListQuery } from 'uniswap/src/features/fiatOnRamp/api'
import { FORCountry } from 'uniswap/src/features/fiatOnRamp/types'
import { getCountryFlagSvgUrl } from 'uniswap/src/features/fiatOnRamp/utils'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { bubbleToTop } from 'utilities/src/primitives/array' import { bubbleToTop } from 'utilities/src/primitives/array'
import { useDebounce } from 'utilities/src/time/timing' import { useDebounce } from 'utilities/src/time/timing'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
import { useFiatOnRampAggregatorCountryListQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORCountry } from 'wallet/src/features/fiatOnRamp/types'
import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils'
import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput'
const ICON_SIZE = 32 // design prefers a custom value here const ICON_SIZE = 32 // design prefers a custom value here
......
...@@ -3,6 +3,11 @@ import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react' ...@@ -3,6 +3,11 @@ import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Delay } from 'src/components/layout/Delayed' import { Delay } from 'src/components/layout/Delayed'
import { ColorTokens } from 'ui/src' import { ColorTokens } from 'ui/src'
import {
useFiatOnRampAggregatorCryptoQuoteQuery,
useFiatOnRampAggregatorSupportedFiatCurrenciesQuery,
} from 'uniswap/src/features/fiatOnRamp/api'
import { FORQuote, FORSupportedFiatCurrency } from 'uniswap/src/features/fiatOnRamp/types'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { useDebounce } from 'utilities/src/time/timing' import { useDebounce } from 'utilities/src/time/timing'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
...@@ -11,11 +16,6 @@ import { ...@@ -11,11 +16,6 @@ import {
useAppFiatCurrencyInfo, useAppFiatCurrencyInfo,
useFiatCurrencyInfo, useFiatCurrencyInfo,
} from 'wallet/src/features/fiatCurrency/hooks' } from 'wallet/src/features/fiatCurrency/hooks'
import {
useFiatOnRampAggregatorCryptoQuoteQuery,
useFiatOnRampAggregatorSupportedFiatCurrenciesQuery,
} from 'wallet/src/features/fiatOnRamp/api'
import { FORQuote, FORSupportedFiatCurrency } from 'wallet/src/features/fiatOnRamp/types'
import { import {
isFiatOnRampApiError, isFiatOnRampApiError,
isInvalidRequestAmountTooHigh, isInvalidRequestAmountTooHigh,
......
...@@ -8,6 +8,8 @@ import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' ...@@ -8,6 +8,8 @@ import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { ColorTokens, useSporeColors } from 'ui/src' import { ColorTokens, useSporeColors } from 'ui/src'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { useFiatOnRampAggregatorSupportedTokensQuery } from 'uniswap/src/features/fiatOnRamp/api'
import { FORSupportedToken } from 'uniswap/src/features/fiatOnRamp/types'
import { ChainId } from 'uniswap/src/types/chains' import { ChainId } from 'uniswap/src/types/chains'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useDebounce } from 'utilities/src/time/timing' import { useDebounce } from 'utilities/src/time/timing'
...@@ -18,7 +20,6 @@ import { ...@@ -18,7 +20,6 @@ import {
import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses'
import { fromMoonpayNetwork, toSupportedChainId } from 'wallet/src/features/chains/utils' import { fromMoonpayNetwork, toSupportedChainId } from 'wallet/src/features/chains/utils'
import { import {
useFiatOnRampAggregatorSupportedTokensQuery,
useFiatOnRampBuyQuoteQuery, useFiatOnRampBuyQuoteQuery,
useFiatOnRampIpAddressQuery, useFiatOnRampIpAddressQuery,
useFiatOnRampLimitsQuery, useFiatOnRampLimitsQuery,
...@@ -26,7 +27,7 @@ import { ...@@ -26,7 +27,7 @@ import {
useFiatOnRampWidgetUrlQuery, useFiatOnRampWidgetUrlQuery,
} from 'wallet/src/features/fiatOnRamp/api' } from 'wallet/src/features/fiatOnRamp/api'
import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks'
import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types' import { MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { addTransaction } from 'wallet/src/features/transactions/slice' import { addTransaction } from 'wallet/src/features/transactions/slice'
import { import {
......
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' import { FORQuote, FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
export function getServiceProviderForQuote( export function getServiceProviderForQuote(
quote: FORQuote | undefined, quote: FORQuote | undefined,
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
getFirestoreUidRef, getFirestoreUidRef,
} from 'src/features/firebase/utils' } from 'src/features/firebase/utils'
import { getOneSignalUserIdOrError } from 'src/features/notifications/Onesignal' import { getOneSignalUserIdOrError } from 'src/features/notifications/Onesignal'
import { call, put, select, takeEvery, takeLatest } from 'typed-redux-saga' import { all, call, put, select, takeEvery, takeLatest } from 'typed-redux-saga'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
import { Language } from 'wallet/src/features/language/constants' import { Language } from 'wallet/src/features/language/constants'
...@@ -113,9 +113,15 @@ function* editAccountDataInFirebase(actionData: ReturnType<typeof editAccountAct ...@@ -113,9 +113,15 @@ function* editAccountDataInFirebase(actionData: ReturnType<typeof editAccountAct
const { type, address } = payload const { type, address } = payload
switch (type) { switch (type) {
case EditAccountAction.Remove: case EditAccountAction.Remove: {
yield* call(removeAccountFromFirebase, address, payload.notificationsEnabled) const accountsToRemove = payload.accounts
break yield* all(
accountsToRemove.map((account: { address: Address; pushNotificationsEnabled: boolean }) =>
call(removeAccountFromFirebase, account.address, account.pushNotificationsEnabled)
)
)
return
}
case EditAccountAction.Rename: case EditAccountAction.Rename:
yield* call(renameAccountInFirebase, address, payload.newName) yield* call(renameAccountInFirebase, address, payload.newName)
break break
...@@ -169,6 +175,10 @@ export function* removeAccountFromFirebase(address: Address, notificationsEnable ...@@ -169,6 +175,10 @@ export function* removeAccountFromFirebase(address: Address, notificationsEnable
const selectAccountNotificationSetting = makeSelectAccountNotificationSetting() const selectAccountNotificationSetting = makeSelectAccountNotificationSetting()
export function* renameAccountInFirebase(address: Address, newName: string) { export function* renameAccountInFirebase(address: Address, newName: string) {
if (!address) {
throw new Error('Address is required for renameAccountInFirebase')
}
try { try {
yield* call(maybeUpdateFirebaseMetadata, address, { name: newName }) yield* call(maybeUpdateFirebaseMetadata, address, { name: newName })
} catch (error) { } catch (error) {
...@@ -180,6 +190,10 @@ export function* toggleFirebaseNotificationSettings({ ...@@ -180,6 +190,10 @@ export function* toggleFirebaseNotificationSettings({
address, address,
enabled, enabled,
}: TogglePushNotificationParams) { }: TogglePushNotificationParams) {
if (!address) {
throw new Error('Address is required for toggleFirebaseNotificationSettings')
}
try { try {
const accounts = yield* appSelect(selectAccounts) const accounts = yield* appSelect(selectAccounts)
const account = accounts[address] const account = accounts[address]
......
...@@ -3,10 +3,10 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet ...@@ -3,10 +3,10 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet
import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState' import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { TransactionState } from 'wallet/src/features/transactions/transactionState/types'
export interface AppModalState<T> { export interface AppModalState<T> {
......
import { Linking } from 'react-native' import { Linking } from 'react-native'
import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal' import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
export const initOneSignal = (): void => { export const initOneSignal = (): void => {
OneSignal.setAppId(config.onesignalAppId) OneSignal.setAppId(config.onesignalAppId)
...@@ -24,7 +24,7 @@ export const initOneSignal = (): void => { ...@@ -24,7 +24,7 @@ export const initOneSignal = (): void => {
setTimeout( setTimeout(
() => () =>
apolloClientRef.current?.refetchQueries({ apolloClientRef.current?.refetchQueries({
include: [GQLQueries.PortfolioBalances, GQLQueries.TransactionList], include: GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE,
}), }),
ONE_SECOND_MS // Delay by 1s to give a buffer for data sources to synchronize ONE_SECOND_MS // Delay by 1s to give a buffer for data sources to synchronize
) )
......
import { providers } from 'ethers' import { providers } from 'ethers'
import { default as React, useCallback, useEffect, useMemo, useReducer, useState } from 'react' import { default as React, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TouchableWithoutFeedback } from 'react-native' import { StyleSheet, TouchableWithoutFeedback } from 'react-native'
import { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated' import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
import { useShouldShowNativeKeyboard } from 'src/app/hooks' import { useShouldShowNativeKeyboard } from 'src/app/hooks'
import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect' import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import { Screen } from 'src/components/layout/Screen' import { Screen } from 'src/components/layout/Screen'
...@@ -10,7 +10,7 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet ...@@ -10,7 +10,7 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet
import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader' import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader'
import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus' import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus'
import { useWalletRestore } from 'src/features/wallet/hooks' import { useWalletRestore } from 'src/features/wallet/hooks'
import { AnimatedFlex, Flex, useDeviceDimensions, useDeviceInsets, useSporeColors } from 'ui/src' import { Flex, useDeviceDimensions, useDeviceInsets, useSporeColors } from 'ui/src'
import EyeIcon from 'ui/src/assets/icons/eye.svg' import EyeIcon from 'ui/src/assets/icons/eye.svg'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
...@@ -52,7 +52,6 @@ import { ...@@ -52,7 +52,6 @@ import {
TokenSelectorFlow, TokenSelectorFlow,
} from 'wallet/src/features/transactions/transfer/types' } from 'wallet/src/features/transactions/transfer/types'
import { TransactionStep, TransferFlowProps } from 'wallet/src/features/transactions/types' import { TransactionStep, TransferFlowProps } from 'wallet/src/features/transactions/types'
import { ANIMATE_SPRING_CONFIG } from 'wallet/src/features/transactions/utils'
import { currencyAddress } from 'wallet/src/utils/currencyId' import { currencyAddress } from 'wallet/src/utils/currencyId'
interface TransferFormProps { interface TransferFormProps {
...@@ -119,32 +118,66 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ...@@ -119,32 +118,66 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
setRenderInnerContentRouter(renderInnerContentRouter || !showRecipientSelector) setRenderInnerContentRouter(renderInnerContentRouter || !showRecipientSelector)
}, [renderInnerContentRouter, showRecipientSelector]) }, [renderInnerContentRouter, showRecipientSelector])
const screenXOffset = useSharedValue(showRecipientSelector ? -fullWidth : 0)
useEffect(() => {
const screenOffset = showRecipientSelector ? 1 : 0
screenXOffset.value = withSpring(-(fullWidth * screenOffset), ANIMATE_SPRING_CONFIG)
}, [screenXOffset, showRecipientSelector, fullWidth])
const wrapperStyle = useAnimatedStyle(() => ({
transform: [{ translateX: screenXOffset.value }],
}))
const onFormNext = useCallback(() => setStep(TransactionStep.REVIEW), [setStep]) const onFormNext = useCallback(() => setStep(TransactionStep.REVIEW), [setStep])
const onReviewNext = useCallback(() => setStep(TransactionStep.SUBMITTED), [setStep]) const onReviewNext = useCallback(() => setStep(TransactionStep.SUBMITTED), [setStep])
const onReviewPrev = useCallback(() => setStep(TransactionStep.FORM), [setStep]) const onReviewPrev = useCallback(() => setStep(TransactionStep.FORM), [setStep])
const onRetrySubmit = useCallback(() => setStep(TransactionStep.FORM), [setStep]) const onRetrySubmit = useCallback(() => setStep(TransactionStep.FORM), [setStep])
const exactValue = isFiatInput ? exactAmountFiat : exactAmountToken const exactValue = isFiatInput ? exactAmountFiat : exactAmountToken
const recipient = state.recipient
const isRecipientScreenOnLeft = useSharedValue(true)
const inputScreenOffsetX = useSharedValue(0)
useEffect(() => {
if (!recipient) {
// If starting from the recipient selector screen, move the input to the right
inputScreenOffsetX.value = fullWidth
return
}
if (!showRecipientSelector) {
// Transition input screen to the center if recipient selector is not shown
inputScreenOffsetX.value = withTiming(0, undefined, () => {
isRecipientScreenOnLeft.value = false
})
} else {
// Transition input screen to the left if recipient selector is shown
// and recipient is already selected
inputScreenOffsetX.value = withTiming(-fullWidth)
}
}, [showRecipientSelector, recipient, fullWidth, inputScreenOffsetX, isRecipientScreenOnLeft])
const recipientScreenStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: inputScreenOffsetX.value + (isRecipientScreenOnLeft.value ? -1 : 1) * fullWidth,
},
],
}))
const inputScreenStyle = useAnimatedStyle(() => ({
transform: [{ translateX: inputScreenOffsetX.value }],
}))
return ( return (
<> <>
<TouchableWithoutFeedback> <TouchableWithoutFeedback>
<Screen edges={['top']}> <Screen edges={['top']}>
<HandleBar backgroundColor="none" /> <HandleBar backgroundColor="none" />
<AnimatedFlex grow row height="100%" style={wrapperStyle}> <Flex fill>
<Animated.View style={[styles.screen, recipientScreenStyle]}>
<RecipientSelect
recipient={recipient}
onSelectRecipient={onSelectRecipient}
onToggleShowRecipientSelector={onToggleShowRecipientSelector}
/>
</Animated.View>
<Animated.View style={[styles.screen, inputScreenStyle]}>
{/* Padding bottom must have a similar size to the handlebar {/* Padding bottom must have a similar size to the handlebar
height as 100% height doesn't include the handlebar height */} height as 100% height doesn't include the handlebar height */}
<Flex gap="$spacing16" mb={insets.bottom} pb="$spacing12" px="$spacing16" width="100%"> <Flex fill gap="$spacing16" mb={insets.bottom} pb="$spacing12" px="$spacing16">
{step !== TransactionStep.SUBMITTED && ( {step !== TransactionStep.SUBMITTED && (
<TransferHeader <TransferHeader
dispatch={dispatch} dispatch={dispatch}
...@@ -176,16 +209,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ...@@ -176,16 +209,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
/> />
)} )}
</Flex> </Flex>
</Animated.View>
<Flex width="100%">
{showRecipientSelector ? (
<RecipientSelect
recipient={state.recipient}
onSelectRecipient={onSelectRecipient}
onToggleShowRecipientSelector={onToggleShowRecipientSelector}
/>
) : null}
</Flex>
{showViewOnlyModal && ( {showViewOnlyModal && (
<WarningModal <WarningModal
...@@ -205,7 +229,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ...@@ -205,7 +229,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
onConfirm={(): void => setShowViewOnlyModal(false)} onConfirm={(): void => setShowViewOnlyModal(false)}
/> />
)} )}
</AnimatedFlex> </Flex>
</Screen> </Screen>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
{!!state.selectingCurrencyField && ( {!!state.selectingCurrencyField && (
...@@ -340,3 +364,11 @@ function TransferInnerContent({ ...@@ -340,3 +364,11 @@ function TransferInnerContent({
return null return null
} }
} }
const styles = StyleSheet.create({
screen: {
height: '100%',
position: 'absolute',
width: '100%',
},
})
...@@ -23,6 +23,7 @@ import { ...@@ -23,6 +23,7 @@ import {
import { ENS_LOGO } from 'ui/src/assets' import { ENS_LOGO } from 'ui/src/assets'
import { InfoCircleFilled, LinkHorizontalAlt } from 'ui/src/components/icons' import { InfoCircleFilled, LinkHorizontalAlt } from 'ui/src/components/icons'
import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { Pill } from 'uniswap/src/components/pill/Pill'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants'
...@@ -34,7 +35,6 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time' ...@@ -34,7 +35,6 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { TextInput } from 'wallet/src/components/input/TextInput' import { TextInput } from 'wallet/src/components/input/TextInput'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink'
import { Pill } from 'wallet/src/components/text/Pill'
import { import {
useCreateOnboardingAccountIfNone, useCreateOnboardingAccountIfNone,
useOnboardingContext, useOnboardingContext,
......
...@@ -101,6 +101,14 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams ...@@ -101,6 +101,14 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams
} }
const { transactionResponse } = yield* call(sendTransaction, txParams) const { transactionResponse } = yield* call(sendTransaction, txParams)
signature = transactionResponse.hash signature = transactionResponse.hash
// Trigger a pending transaction notification after we send the transaction to chain
yield* put(
pushNotification({
type: AppNotificationType.TransactionPending,
chainId: txParams.chainId,
})
)
} }
if (params.dapp.source === 'walletconnect') { if (params.dapp.source === 'walletconnect') {
......
...@@ -8,7 +8,7 @@ import { ChainId } from 'uniswap/src/types/chains' ...@@ -8,7 +8,7 @@ import { ChainId } from 'uniswap/src/types/chains'
const EIP155_MAINNET = 'eip155:1' const EIP155_MAINNET = 'eip155:1'
const EIP155_POLYGON = 'eip155:137' const EIP155_POLYGON = 'eip155:137'
const EIP155_OPTIMISM = 'eip155:10' const EIP155_OPTIMISM = 'eip155:10'
const EIP155_AVAX_UNSUPPORTED = 'eip155:43114' const EIP155_LINEA_UNSUPPORTED = 'eip155:59144'
const TEST_ADDRESS = '0xdFb84E543C39ACa3c6a39ea4e3B6c40eE7d2EBdA' const TEST_ADDRESS = '0xdFb84E543C39ACa3c6a39ea4e3B6c40eE7d2EBdA'
...@@ -39,7 +39,7 @@ describe(getSupportedWalletConnectChains, () => { ...@@ -39,7 +39,7 @@ describe(getSupportedWalletConnectChains, () => {
it('handles list of valid chains including an invalid chain', () => { it('handles list of valid chains including an invalid chain', () => {
expect( expect(
getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_AVAX_UNSUPPORTED]) getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_LINEA_UNSUPPORTED])
).toEqual([ChainId.Mainnet, ChainId.Polygon]) ).toEqual([ChainId.Mainnet, ChainId.Polygon])
}) })
}) })
...@@ -54,6 +54,6 @@ describe(getChainIdFromEIP155String, () => { ...@@ -54,6 +54,6 @@ describe(getChainIdFromEIP155String, () => {
}) })
it('handles invalid eip155 address', () => { it('handles invalid eip155 address', () => {
expect(getChainIdFromEIP155String(EIP155_AVAX_UNSUPPORTED)).toBeNull() expect(getChainIdFromEIP155String(EIP155_LINEA_UNSUPPORTED)).toBeNull()
}) })
}) })
...@@ -18,6 +18,10 @@ export function importMnemonic(mnemonic: string): Promise<string> { ...@@ -18,6 +18,10 @@ export function importMnemonic(mnemonic: string): Promise<string> {
return RNEthersRS.importMnemonic(mnemonic) return RNEthersRS.importMnemonic(mnemonic)
} }
export function removeMnemonic(mnemonicId: string): Promise<boolean> {
return RNEthersRS.removeMnemonic(mnemonicId)
}
// returns the mnemonicId (derived address at index 0) of the stored mnemonic // returns the mnemonicId (derived address at index 0) of the stored mnemonic
export function generateAndStoreMnemonic(): Promise<string> { export function generateAndStoreMnemonic(): Promise<string> {
return RNEthersRS.generateAndStoreMnemonic() return RNEthersRS.generateAndStoreMnemonic()
...@@ -43,6 +47,10 @@ export function generateAndStorePrivateKey( ...@@ -43,6 +47,10 @@ export function generateAndStorePrivateKey(
return RNEthersRS.generateAndStorePrivateKey(mnemonicId, derivationIndex) return RNEthersRS.generateAndStorePrivateKey(mnemonicId, derivationIndex)
} }
export function removePrivateKey(address: string): Promise<boolean> {
return RNEthersRS.removePrivateKey(address)
}
export function signTransactionHashForAddress( export function signTransactionHashForAddress(
address: string, address: string,
hash: string, hash: string,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment