ci(release): publish latest release

parent 3defbbd8
IPFS hash of the deployment:
- CIDv0: `QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC`
- CIDv1: `bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu`
- CIDv0: `QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx`
- CIDv1: `bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu.ipfs.dweb.link/
- https://bafybeidu7lqw7ls7lcaxytltxl3vr7xk3hne5lrs22rmndx7vvc5ovsbiu.ipfs.cf-ipfs.com/
- [ipfs://QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC/](ipfs://QmWDHt9C34V3ARMDrzfCHtnhJd9T3RdJ5UEvYRV2cRhBEC/)
- https://bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu.ipfs.dweb.link/
- https://bafybeiazesdn7jujaopqkz3nwemsmtbdp4fcxvjv75cevqgm3fhe65nwdu.ipfs.cf-ipfs.com/
- [ipfs://QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx/](ipfs://QmQ2oCvxsKb6KLE7XeR4ppyZWhiAsEAMwtXta9Gwtvptnx/)
## 5.33.0 (2024-06-10)
## 5.34.0 (2024-06-12)
### 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
\ No newline at end of file
web/5.34.0
\ No newline at end of file
......@@ -131,17 +131,17 @@ android {
dev {
isDefault(true)
applicationIdSuffix ".dev"
versionName "1.28"
versionName "1.29"
dimension "variant"
}
beta {
applicationIdSuffix ".beta"
versionName "1.28"
versionName "1.29"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.28"
versionName "1.29"
}
}
......
......@@ -20,6 +20,15 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:localeConfig="@xml/locales_config"
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
android:name=".MainActivity"
android:label="@string/app_name"
......
......@@ -7,6 +7,10 @@ import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.module.annotations.ReactModule
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.
......@@ -18,6 +22,7 @@ import com.facebook.soloader.SoLoader
class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val ethersRs: RnEthersRs = RnEthersRs(reactContext.applicationContext)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// Needs to be initialized form a static context
companion object {
......@@ -40,6 +45,12 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase
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) {
promise.resolve(ethersRs.generateAndStoreMnemonic())
......@@ -65,6 +76,13 @@ class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBase
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) {
promise.resolve(ethersRs.signTransactionHashForAddress(address, hash, chainId.toLong()))
}
......
......@@ -10,6 +10,11 @@ import com.uniswap.EthersRs.signHashWithWallet
import com.uniswap.EthersRs.signMessageWithWallet
import com.uniswap.EthersRs.signTxWithWallet
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) {
......@@ -82,6 +87,11 @@ class RnEthersRs(applicationContext: Context) {
return keychain.getString(keychainKeyForMnemonicId(mnemonicId), null)
}
suspend fun removeMnemonic(mnemonicId: String): Boolean {
keychain.edit().remove(keychainKeyForMnemonicId(mnemonicId)).apply()
return true
}
val addressesForStoredPrivateKeys: List<String>
get() = keychain.all.keys
.filter { key -> key.contains(PRIVATE_KEY_PREFIX) }
......@@ -118,6 +128,11 @@ class RnEthersRs(applicationContext: Context) {
return address
}
suspend fun removePrivateKey(address: String): Boolean {
keychain.edit().remove(keychainKeyForPrivateKey(address)).apply()
return true
}
/**
* Signs a transaction for a given address.
* @param address The address to sign the transaction for.
......
......@@ -6,8 +6,8 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
import com.uniswap.onboarding.backup.MnemonicDisplayViewManager
import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager
import com.uniswap.onboarding.backup.MnemonicDisplayViewManager
import com.uniswap.onboarding.import.SeedPhraseInputViewManager
class UniswapPackage : ReactPackage {
......@@ -24,6 +24,7 @@ class UniswapPackage : ReactPackage {
): List<NativeModule> = listOf(
AndroidDeviceModule(reactContext),
RNEthersRSModule(reactContext),
ThemeModule(reactContext),
)
}
......@@ -28,19 +28,30 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() {
override fun getName(): String = REACT_CLASS
private val mnemonicIdFlow = MutableStateFlow("")
private val shouldShowSmallTextFlow = MutableStateFlow(false)
private val selectedWordPlaceholderFlow = MutableStateFlow("")
override fun createViewInstance(reactContext: ThemedReactContext): ComposeView {
val ethersRs = RnEthersRs(reactContext)
val viewModel = MnemonicConfirmationViewModel(ethersRs)
return ComposeView(reactContext).apply {
id = R.id.mnemonic_confirmation_compose_id // Needed for RN event emitter
setContent {
val mnemonicId by mnemonicIdFlow.collectAsState()
val shouldShowSmallText by shouldShowSmallTextFlow.collectAsState()
val selectedWordPlaceholder by selectedWordPlaceholderFlow.collectAsState()
viewModel.updatePlaceholder(selectedWordPlaceholder)
UniswapComponent {
MnemonicConfirmation(mnemonicId = mnemonicId, viewModel = viewModel) {
val reactContext = context as ReactContext
MnemonicConfirmation(
mnemonicId = mnemonicId,
viewModel = viewModel,
shouldShowSmallText = shouldShowSmallText,
) {
context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, EVENT_COMPLETED, null) // Sends event to RN bridge
......@@ -70,6 +81,16 @@ class MnemonicConfirmationViewManager : ViewGroupManager<ComposeView>() {
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 {
private const val REACT_CLASS = "MnemonicConfirmation"
private const val EVENT_COMPLETED = "onConfirmComplete"
......
......@@ -4,9 +4,13 @@ import android.view.View
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.ViewGroupManager
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter
import com.uniswap.RnEthersRs
import com.uniswap.onboarding.backup.ui.MnemonicDisplay
import com.uniswap.onboarding.backup.ui.MnemonicDisplayViewModel
......@@ -22,29 +26,83 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() {
override fun getName(): String = REACT_CLASS
private lateinit var context: ThemedReactContext
private val mnemonicIdFlow = MutableStateFlow("")
private val copyTextFlow = MutableStateFlow("")
private val copiedTextFlow = MutableStateFlow("")
override fun createViewInstance(reactContext: ThemedReactContext): ComposeView {
context = reactContext
val ethersRs = RnEthersRs(reactContext)
val viewModel = MnemonicDisplayViewModel(ethersRs)
return ComposeView(reactContext).apply {
setContent {
val mnemonicId by mnemonicIdFlow.collectAsState()
val copyText by copyTextFlow.collectAsState()
val copiedText by copiedTextFlow.collectAsState()
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")
fun setMnemonicId(view: View, mnemonicId: String) {
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 {
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
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
......@@ -13,7 +12,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.uniswap.theme.UniswapTheme
/**
......@@ -24,10 +22,11 @@ import com.uniswap.theme.UniswapTheme
fun MnemonicConfirmation(
viewModel: MnemonicConfirmationViewModel,
mnemonicId: String,
shouldShowSmallText: Boolean,
onCompleted: () -> Unit,
) {
val displayedWords by viewModel.displayWords.collectAsState()
val displayedWords by viewModel.selectedWords.collectAsState()
val wordBankList by viewModel.wordBankList.collectAsState()
val completed by viewModel.completed.collectAsState()
......@@ -41,28 +40,20 @@ fun MnemonicConfirmation(
}
}
BoxWithConstraints(
modifier = Modifier.padding(horizontal = UniswapTheme.spacing.spacing16)
) {
val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp
BoxWithConstraints {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
MnemonicWordsGroup(words = displayedWords, showCompact = showCompact) {
viewModel.handleWordRowClick(it)
}
MnemonicWordsGroup(
words = displayedWords,
shouldShowSmallText = shouldShowSmallText,
)
Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing24))
MnemonicWordBank(words = wordBankList, showCompact = showCompact) {
viewModel.handleWordBankClick(it)
MnemonicWordBank(words = wordBankList, shouldShowSmallText = shouldShowSmallText) {
viewModel.handleWordBankClick(it.index)
}
}
}
}
private const val SCREEN_HEIGHT_BREAKPOINT = 500
......@@ -3,6 +3,7 @@ package com.uniswap.onboarding.backup.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.MnemonicWordUiState
import kotlinx.coroutines.flow.MutableStateFlow
......@@ -10,38 +11,36 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class MnemonicConfirmationViewModel(
private val ethersRs: RnEthersRs, // Move to repository layer if app gets more complex
) : ViewModel() {
private val defaultMnemonicsCount = 12
private var sourceWords = emptyList<String>()
private var sourceWords = List(defaultMnemonicsCount) { "" }
private var shuffledWords = emptyList<String>()
private val focusedIndex = MutableStateFlow(0)
private val selectedWords = MutableStateFlow<List<String?>>(emptyList())
val displayWords: StateFlow<List<MnemonicWordUiState>> =
combine(focusedIndex, selectedWords) { focusedIndexValue, words ->
words.mapIndexed { index, word ->
MnemonicWordUiState(
num = index + 1,
text = word ?: "",
focused = index == focusedIndexValue,
hasError = word != null && word != sourceWords[index],
)
private val selectedWordsIndexes =
MutableStateFlow<List<Int?>>(List(defaultMnemonicsCount) { null })
private val selectedWordPlaceholderFlow = MutableStateFlow("")
val selectedWords: StateFlow<List<MnemonicWordUiState>> =
selectedWordsIndexes.combine(selectedWordPlaceholderFlow) { _, placeholder ->
List(sourceWords.size) { index ->
getMnemonicWordUiState(index, placeholder)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val wordBankList: StateFlow<List<MnemonicWordBankCellUiState>> =
combine(focusedIndex, selectedWords) { focusedIndexValue, words ->
val counter = words.groupingBy { it }.eachCount().toMutableMap()
shuffledWords.map { shuffledWord ->
counter[shuffledWord] = counter.getOrDefault(shuffledWord, 0) - 1
selectedWordsIndexes.map { selectedWordsIndexes ->
shuffledWords.mapIndexed { index, word ->
MnemonicWordBankCellUiState(
text = shuffledWord,
used = counter.getOrDefault(shuffledWord, -1) >= 0,
index = index,
text = word,
used = selectedWordsIndexes.contains(index),
)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
......@@ -60,46 +59,74 @@ class MnemonicConfirmationViewModel(
val words = mnemonic.split(" ")
sourceWords = words
shuffledWords = words.shuffled()
selectedWords.update { List(words.size) { null } }
selectedWordsIndexes.update { List(words.size) { null } }
}
}
}
private fun reset() {
sourceWords = emptyList()
sourceWords = List(defaultMnemonicsCount) { "" }
shuffledWords = emptyList()
focusedIndex.update { 0 }
selectedWords.update { emptyList() }
selectedWordsIndexes.update { emptyList() }
_completed.update { false }
}
fun handleWordRowClick(word: MnemonicWordUiState) {
val index = displayWords.value.indexOf(word)
focusedIndex.update { index }
fun updatePlaceholder(newPlaceholder: String) {
selectedWordPlaceholderFlow.value = newPlaceholder
}
fun handleWordBankClick(wordBankIndex: Int) {
selectedWordsIndexes.update { indexes ->
val updatedIndexes = indexes.toMutableList()
updatedIndexes[focusedIndex.value] = wordBankIndex
updatedIndexes
}
fun handleWordBankClick(state: MnemonicWordBankCellUiState) {
val focusedIndexValue = focusedIndex.value
selectedWords.update { words ->
val updated = words.mapIndexed { index, word ->
if (index == focusedIndexValue) {
if (state.text == sourceWords[focusedIndexValue]) {
focusedIndex.update { focusedIndexValue + 1 }
if (focusedIndex.value == sourceWords.size - 1) {
checkIfCompleted()
} else if (sourceWords[focusedIndex.value] == shuffledWords[wordBankIndex] && focusedIndex.value < sourceWords.size - 1) {
focusedIndex.update { it + 1 }
}
state.text
} else {
word
}
private fun checkIfCompleted() {
if (selectedWordsIndexes.value.size != sourceWords.size) {
return
}
checkIfCompleted(updated)
updated
for (i in selectedWordsIndexes.value.indices) {
val selectedWord = getSelectedWord(i)
if (sourceWords[i].isEmpty() || selectedWord != sourceWords[i]) {
return
}
}
private fun checkIfCompleted(words: List<String?>) {
if (sourceWords == words) {
_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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.uniswap.extensions.fadingEdges
import com.uniswap.theme.UniswapTheme
import com.uniswap.onboarding.shared.CopyButton
import com.uniswap.theme.relativeOffset
import kotlin.math.abs
@Composable
fun MnemonicDisplay(
viewModel: MnemonicDisplayViewModel,
mnemonicId: String,
copyText: String,
copiedText: String,
onHeightMeasured: (height: Float) -> Unit
) {
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) {
viewModel.setup(mnemonicId)
}
BoxWithConstraints {
val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(horizontal = UniswapTheme.spacing.spacing16)
.fadingEdges(scrollState)
.fillMaxWidth()
.wrapContentHeight()
.verticalScroll(rememberScrollState())
.onSizeChanged { size ->
onHeightMeasured(size.height / density)
}
) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = buttonOffset)
.wrapContentSize(Alignment.Center)
) {
MnemonicWordsGroup(words = words)
Box(
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
class MnemonicDisplayViewModel(
private val ethersRs: RnEthersRs // Move to repository layer if app gets more complex
) : ViewModel() {
private val _words = MutableStateFlow<List<MnemonicWordUiState>>(emptyList())
private val defaultMnemonicsCount = 12
private val _words =
MutableStateFlow(List(defaultMnemonicsCount) { MnemonicWordUiState(num = it + 1, text = "") })
val words = _words.asStateFlow()
private var currentMnemonicId = ""
......
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.Box
import androidx.compose.foundation.layout.fillMaxWidth
......@@ -10,6 +11,8 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.MainAxisAlignment
import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState
......@@ -21,19 +24,19 @@ import com.uniswap.theme.UniswapTheme
@Composable
fun MnemonicWordBank(
words: List<MnemonicWordBankCellUiState>,
showCompact: Boolean = false,
shouldShowSmallText: Boolean = false,
onClick: (word: MnemonicWordBankCellUiState) -> Unit
) {
FlowRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
mainAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
crossAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
mainAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
crossAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8,
mainAxisAlignment = MainAxisAlignment.Center,
) {
words.forEach {
MnemonicWordBankCell(word = it, showCompact = showCompact) {
MnemonicWordBankCell(word = it, shouldShowSmallText = shouldShowSmallText) {
onClick(it)
}
}
......@@ -43,29 +46,35 @@ fun MnemonicWordBank(
@Composable
private fun MnemonicWordBankCell(
word: MnemonicWordBankCellUiState,
showCompact: Boolean,
shouldShowSmallText: Boolean,
onClick: () -> Unit
) {
val textStyle =
if (showCompact) UniswapTheme.typography.body2 else UniswapTheme.typography.body1
if (shouldShowSmallText) UniswapTheme.typography.body3 else UniswapTheme.typography.body2
val verticalPadding =
if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8
if (shouldShowSmallText) UniswapTheme.spacing.spacing8 else 10.dp
val horizontalPadding =
if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8
if (shouldShowSmallText) 10.dp else UniswapTheme.spacing.spacing12
Box(
modifier = Modifier
.clip(UniswapTheme.shapes.xlarge)
.background(UniswapTheme.colors.surface2)
.padding(vertical = verticalPadding)
.padding(horizontal = horizontalPadding)
.clickable { onClick() },
.shadow(
10.dp,
spotColor = UniswapTheme.colors.black.copy(alpha = 0.04f),
shape = UniswapTheme.shapes.xlarge
)
) {
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 = word.text,
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
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.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
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.text.style.TextAlign
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.theme.UniswapTheme
......@@ -24,44 +17,31 @@ import com.uniswap.theme.UniswapTheme
*/
@Composable
fun MnemonicWordCell(
modifier: Modifier = Modifier,
word: MnemonicWordUiState,
showCompact: Boolean = false,
onClick: (() -> Unit)? = null,
shouldShowSmallText: Boolean = false,
) {
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
var rowModifier = modifier
.clip(shape)
.shadow(1.dp, shape)
.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() }
val textColor = when (word.status) {
MnemonicInputStatus.NO_INPUT -> UniswapTheme.colors.neutral3
MnemonicInputStatus.CORRECT_INPUT -> UniswapTheme.colors.neutral1
MnemonicInputStatus.WRONG_INPUT -> UniswapTheme.colors.statusCritical
}
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)
) {
Row {
Text(
text = "${word.num}",
color = UniswapTheme.colors.neutral3,
modifier = Modifier
.defaultMinSize(minWidth = 24.dp)
.align(Alignment.CenterVertically),
textAlign = TextAlign.Center,
color = UniswapTheme.colors.neutral2,
modifier = Modifier.defaultMinSize(minWidth = if (shouldShowSmallText) 14.dp else 16.dp),
style = textStyle,
)
Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing16))
Text(
modifier = Modifier.weight(1f),
text = word.text,
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
fun MnemonicWordsColumn(
modifier: Modifier = Modifier,
words: List<MnemonicWordUiState>,
showCompact: Boolean = false,
onClick: ((word: MnemonicWordUiState) -> Unit)? = null,
shouldShowSmallText: Boolean = false,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(
if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12
),
verticalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing8),
) {
words.forEachIndexed { index, word ->
val onWordClick = onClick?.let {
{ it(word) }
}
MnemonicWordCell(word = word, showCompact = showCompact, onClick = onWordClick)
words.forEach { word ->
MnemonicWordCell(
word = word,
shouldShowSmallText = shouldShowSmallText,
)
}
}
}
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState
import com.uniswap.theme.UniswapTheme
......@@ -17,15 +22,24 @@ fun MnemonicWordsGroup(
modifier: Modifier = Modifier,
words: List<MnemonicWordUiState>,
columnCount: Int = DEFAULT_COLUMN_COUNT,
showCompact: Boolean = false,
onClick: ((word: MnemonicWordUiState) -> Unit)? = null,
shouldShowSmallText: Boolean = false,
) {
Row(
modifier = modifier
.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(
if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12
if (shouldShowSmallText) UniswapTheme.spacing.spacing16 else UniswapTheme.spacing.spacing24
)
) {
val size = words.size / columnCount
......@@ -35,8 +49,7 @@ fun MnemonicWordsGroup(
MnemonicWordsColumn(
modifier = Modifier.weight(1f),
words = words.subList(starting, ending),
showCompact = showCompact,
onClick = onClick,
shouldShowSmallText = shouldShowSmallText,
)
}
}
......
package com.uniswap.onboarding.backup.ui.model
data class MnemonicWordBankCellUiState(
val index: Int,
val text: String,
val used: Boolean = false,
)
package com.uniswap.onboarding.backup.ui.model
enum class MnemonicInputStatus {
NO_INPUT,
CORRECT_INPUT,
WRONG_INPUT
}
data class MnemonicWordUiState(
val num: Int,
val text: String,
val focused: Boolean = false,
val hasError: Boolean = false,
val sourceIndex: Int? = null,
val status: MnemonicInputStatus = MnemonicInputStatus.CORRECT_INPUT
)
package com.uniswap.onboarding.import
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.core.view.updateLayoutParams
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.uimanager.annotations.ReactProp
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.RnEthersRs
import com.uniswap.theme.UniswapComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.json.Json
/**
......@@ -86,13 +75,22 @@ class SeedPhraseInputViewManager : ViewGroupManager<ComposeView>() {
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return 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(
"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(
"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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.toLowerCase
import androidx.lifecycle.ViewModel
import com.uniswap.EthersRs
import com.uniswap.RnEthersRs
......@@ -17,9 +16,9 @@ class SeedPhraseInputViewModel(
) : ViewModel() {
sealed interface Status {
object None: Status
object Valid: Status
class Error(val error: MnemonicError): Status
object None : Status
object Valid : Status
class Error(val error: MnemonicError) : Status
}
sealed interface MnemonicError {
......@@ -27,7 +26,7 @@ class SeedPhraseInputViewModel(
object TooManyWords : MnemonicError
object NotEnoughWords : MnemonicError
object WrongRecoveryPhrase : MnemonicError
object InvalidPhrase: MnemonicError
object InvalidPhrase : MnemonicError
}
data class ReactNativeStrings(
......@@ -42,7 +41,8 @@ class SeedPhraseInputViewModel(
// Sourced externally from RN
var mnemonicIdForRecovery by mutableStateOf<String?>(null)
var rnStrings by mutableStateOf(ReactNativeStrings(
var rnStrings by mutableStateOf(
ReactNativeStrings(
helpText = "",
inputPlaceholder = "",
pasteButton = "",
......@@ -50,7 +50,8 @@ class SeedPhraseInputViewModel(
errorPhraseLength = "",
errorWrongPhrase = "",
errorInvalidPhrase = "",
))
)
)
var input by mutableStateOf(TextFieldValue(""))
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
import androidx.compose.ui.unit.dp
data class CustomShapes(
val small: RoundedCornerShape = RoundedCornerShape(16.dp),
val medium: RoundedCornerShape = RoundedCornerShape(20.dp),
val large: RoundedCornerShape = RoundedCornerShape(24.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"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
android:viewportWidth="20"
android:viewportHeight="20">
<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"
/>
</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 @@
<string name="app_name">Uniswap</string>
<string name="expo_splash_screen_resize_mode">cover</string>
<string name="expo_splash_screen_status_bar_translucent">true</string>
<string name="notification_accent_color">f50db4</string>
</resources>
......@@ -8,6 +8,7 @@
import SwiftUI
struct Colors {
static let surface1 = Color("surface1")
static let surface2 = Color("surface2")
static let surface3 = Color("surface3")
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()
RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_PROPERTY(shouldShowSmallText, BOOL, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_CALLBACK(onConfirmComplete, RCTDirectEventBlock, MnemonicConfirmationView);
RCT_EXPORT_SWIFTUI_PROPERTY(selectedWordPlaceholder, NSString, MnemonicConfirmationView);
- (UIView *)view
{
......
......@@ -18,6 +18,11 @@ import SwiftUI
get { return vc.rootView.props.mnemonicId }
}
var selectedWordPlaceholder: String {
set { vc.rootView.props.selectedWordPlaceholder = newValue}
get { return vc.rootView.props.selectedWordPlaceholder }
}
var shouldShowSmallText: Bool {
set { vc.rootView.props.shouldShowSmallText = newValue}
get { return vc.rootView.props.shouldShowSmallText }
......@@ -36,11 +41,12 @@ import SwiftUI
class MnemonicConfirmationProps : ObservableObject {
@Published var mnemonicId: String = ""
@Published var selectedWordPlaceholder: String = ""
@Published var shouldShowSmallText: Bool = false
@Published var onConfirmComplete: RCTDirectEventBlock = { _ in }
@Published var mnemonicWords: [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
}
......@@ -58,49 +64,70 @@ struct MnemonicConfirmation: View {
}
}
func onSuggestionTapped(word: String) {
props.typedWords[props.selectedIndex] = word
func onSuggestionTapped(tappedIndex: Int) {
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([:])
} 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
}
}
func onFieldTapped(fieldNumber: Int) {
props.selectedIndex = fieldNumber - 1
func isMnemonicMatch() -> Bool {
for i in 0..<props.typedWordIndexes.count {
if (getTypedWord(index: i) != props.mnemonicWords[i]) {
return false
}
}
return true
}
func getLabelFocusState(index: Int) -> InputFocusState{
let isTextFieldFocused = index == props.selectedIndex
let isTextFieldValid = props.mnemonicWords[index] == props.typedWords[index]
let isTextFieldEmpty = props.typedWords[index].count == 0
func getTypedWord(index: Int) -> String {
guard index >= 0 && index < props.typedWordIndexes.count else {
return ""
}
if (isTextFieldFocused && !isTextFieldEmpty && !isTextFieldValid) {
return InputFocusState.focusedWrongInput
} else if (isTextFieldFocused) {
return InputFocusState.focusedNoInput
} else if (!isTextFieldEmpty && !isTextFieldValid) {
return InputFocusState.notFocusedWrongInput
let scrambledWordIndex = props.typedWordIndexes[index]
if scrambledWordIndex == -1 {
return ""
}
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 {
let end = props.mnemonicWords.count - 1
let middle = end / 2
VStack(alignment: HorizontalAlignment.leading, spacing: 0) {
HStack(alignment: VerticalAlignment.center, spacing: 12) {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
ForEach((0...middle), id: \.self) {index in
MnemonicTextField(index: index + 1,
initialText: props.typedWords[index],
shouldShowSmallText: props.shouldShowSmallText,
focusState: getLabelFocusState(index: index),
onFieldTapped: onFieldTapped
word: getFieldText(index: index),
status: getFieldStatus(index: index),
shouldShowSmallText: props.shouldShowSmallText
)
.frame(maxWidth: .infinity, alignment: .leading)
}
......@@ -108,20 +135,24 @@ struct MnemonicConfirmation: View {
VStack(alignment: .leading, spacing: 12) {
ForEach((middle + 1...end), id: \.self) {index in
MnemonicTextField(index: index + 1,
initialText: props.typedWords[index],
shouldShowSmallText: props.shouldShowSmallText,
focusState: getLabelFocusState(index: index),
onFieldTapped: onFieldTapped
word: getFieldText(index: index),
status: getFieldStatus(index: index),
shouldShowSmallText: props.shouldShowSmallText
)
.frame(maxWidth: .infinity, alignment: .leading)
}
}.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,
usedWords: props.typedWords,
usedWordIndexes: props.typedWordIndexes,
labelCallback: onSuggestionTapped,
shouldShowSmallText: props.shouldShowSmallText)
.frame(maxWidth: .infinity)
......
......@@ -8,33 +8,29 @@
import SwiftUI
struct BankWord: Hashable {
var index: Int
var word: String = ""
var used: Bool = false
}
struct MnemonicConfirmationWordBankView: View {
let smallFont = UIFont(name: "Basel-Book", size: 14)
let mediumFont = UIFont(name: "Basel-Book", size: 16)
var groupedWords: [[BankWord]] = [[BankWord]]()
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
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.shouldShowSmallText = shouldShowSmallText
// Mark words as used individually to handle case of duplicate words
var wordStructs = words.map { word in BankWord(word: word) }
// Use used words to mark used
usedWords.forEach{ usedWord in
for idx in 0...wordStructs.count-1 {
if (usedWord == wordStructs[idx].word && !wordStructs[idx].used) {
// Ensure that proper words are displayed as used in case of duplicates
var wordStructs = words.enumerated().map { index, word in BankWord(index: index, word: word) }
usedWordIndexes.forEach{ idx in
if (idx != -1) {
wordStructs[idx].used = true
return
}
}
}
......@@ -43,13 +39,11 @@ struct MnemonicConfirmationWordBankView: View {
}
private func createGroupedWords(_ items: [BankWord]) -> [[BankWord]] {
var groupedItems: [[BankWord]] = [[BankWord]]()
var tempItems: [BankWord] = [BankWord]()
var width: CGFloat = 0
for word in items {
let label = UILabel()
label.text = word.word
label.sizeToFit()
......@@ -65,31 +59,33 @@ struct MnemonicConfirmationWordBankView: View {
tempItems.removeAll()
tempItems.append(word)
}
}
groupedItems.append(tempItems)
return groupedItems
}
var body: some View {
VStack(alignment: .center) {
ForEach(groupedWords, id: \.self) { subItems in
HStack(spacing: 8) {
HStack(spacing: shouldShowSmallText ? 4 : 8) {
ForEach(subItems, id: \.self) { bankWord in
Text(bankWord.word)
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.fixedSize()
.padding(shouldShowSmallText ? EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12) : EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
.background(Colors.surface2)
.padding(shouldShowSmallText ? EdgeInsets(top: 8, leading: 10, bottom: 8, trailing: 10) : EdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12))
.background(Colors.surface1)
.foregroundColor(Colors.neutral1)
.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 {
labelCallback?(bankWord.word)
labelCallback?(bankWord.index)
}
.opacity(bankWord.used ? 0.60 : 1)
.opacity(bankWord.used ? 0.5 : 1)
}
}
}
......
......@@ -13,12 +13,14 @@
@end
@implementation MnemonicDisplayManager
RCT_EXPORT_MODULE()
RCT_EXPORT_MODULE(MnemonicDisplay)
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];
UIView *view = [proxy view];
NSMutableDictionary *storage = [MnemonicDisplayView storage];
......
......@@ -17,19 +17,40 @@ import SwiftUI
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 {
vc.view.backgroundColor = .clear
return vc.view
}
}
class MnemonicDisplayProps : ObservableObject {
class MnemonicDisplayProps: ObservableObject {
@Published var mnemonicId: String = ""
@Published var copyText: String = ""
@Published var copiedText: String = ""
@Published var mnemonicWords: [String] = Array(repeating: "", count: 12)
var onHeightMeasured: ((CGFloat) -> Void)?
}
struct MnemonicDisplay: View {
private let buttonOffset: CGFloat = 20
@ObservedObject var props = MnemonicDisplayProps()
......@@ -43,6 +64,12 @@ struct MnemonicDisplay: View {
}
}
func copyToClipboard() {
let mnemonicString = props.mnemonicWords.joined(separator: " ")
UIPasteboard.general.string = mnemonicString
}
var body: some View {
if (props.mnemonicWords.count > 12) {
ScrollView {
......@@ -58,26 +85,61 @@ struct MnemonicDisplay: View {
let end = props.mnemonicWords.count - 1
let middle = end / 2
VStack(alignment: HorizontalAlignment.leading, spacing: 0) {
HStack(alignment: VerticalAlignment.center, spacing: 12) {
VStack(alignment: .leading, spacing: 0) {
ZStack {
HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
ForEach((0...middle), id: \.self) {index in
ForEach((0...middle), id: \.self) { index in
MnemonicTextField(index: index + 1,
initialText: props.mnemonicWords[index]
word: props.mnemonicWords[index]
)
.frame(maxWidth: .infinity, alignment: .leading)
}
}.frame(maxWidth: .infinity)
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,
initialText: props.mnemonicWords[index]
word: props.mnemonicWords[index]
)
.frame(maxWidth: .infinity, alignment: .leading)
}
}.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 @@
import SwiftUI
enum InputFocusState {
case notFocused
case focusedNoInput
case focusedWrongInput
case notFocusedWrongInput
enum MnemonicInputStatus {
case noInput
case correctInput
case wrongInput
}
struct MnemonicTextField: View {
@Environment(\.colorScheme) var colorScheme
let smallFont = UIFont(name: "Basel-Book", size: 14)
let mediumFont = UIFont(name: "Basel-Book", size: 16)
var index: Int
var initialText = ""
var word = ""
var shouldShowSmallText: Bool
var onFieldTapped: ((Int) -> Void)?
var focusState: InputFocusState
var status: MnemonicInputStatus
init(index: Int,
initialText: String,
shouldShowSmallText: Bool = false,
focusState: InputFocusState = InputFocusState.notFocused,
onFieldTapped: ((Int) -> Void)? = nil
word: String,
status: MnemonicInputStatus = .correctInput,
shouldShowSmallText: Bool = false
) {
self.index = index
self.initialText = initialText
self.word = word
self.status = status
self.shouldShowSmallText = shouldShowSmallText
self.focusState = focusState
self.onFieldTapped = onFieldTapped
}
func getLabelBackground(focusState: InputFocusState) -> some View {
switch (focusState) {
case .focusedNoInput:
return AnyView(RoundedRectangle(cornerRadius: 100)
.strokeBorder(Colors.accent1, lineWidth: 2)
.background(Colors.surface2)
.cornerRadius(100)
)
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)
)
func getLabelColor() -> Color {
switch (status) {
case .noInput:
return Colors.neutral3
case .correctInput:
return Colors.neutral1
case .wrongInput:
return Colors.statusCritical
}
}
var body: some View {
HStack(alignment: VerticalAlignment.center, spacing: 0) {
Text(String(index)).cornerRadius(16)
HStack(alignment: VerticalAlignment.center, spacing: 18) {
Text(String(index))
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.foregroundColor(Colors.neutral3)
.padding(shouldShowSmallText ? EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 12) : EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 12))
.frame(alignment: Alignment.leading)
.foregroundColor(Colors.neutral2)
.frame(width: shouldShowSmallText ? 14 : 16, alignment: Alignment.leading)
Text(initialText)
Text(word)
.font(Font((shouldShowSmallText ? smallFont : mediumFont)!))
.multilineTextAlignment(TextAlignment.leading)
.foregroundColor(Colors.neutral1)
.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)
.foregroundColor(getLabelColor())
.multilineTextAlignment(.leading)
}
.background(getLabelBackground(focusState: focusState))
.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 {
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.
......@@ -189,6 +199,14 @@ class RNEthersRS: NSObject {
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:)
func signTransactionForAddress(
address: String, hash: String, chainId: NSNumber, resolve: RCTPromiseResolveBlock,
......
......@@ -16,6 +16,10 @@ RCT_EXTERN_METHOD(importMnemonic: (NSString *)mnemonic
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeMnemonic: (NSString *)mnemonicId
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(generateAndStoreMnemonic: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
......@@ -32,6 +36,10 @@ RCT_EXTERN_METHOD(generateAndStorePrivateKey: (NSString *)mnemonicId
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removePrivateKey: (NSString *)address
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(signTransactionHashForAddress: (NSString *)address
hash: (NSString *)hash
chainId: NSNumber
......
......@@ -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 { localizeMock as mockRNLocalize } from 'react-native-localize/mock'
import { TextDecoder, TextEncoder } from 'util'
import { AppearanceSettingType } from 'wallet/src/features/appearance/slice'
import { mockLocalizationContext } from 'wallet/src/test/mocks/utils'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
// Mock Sentry crash reporting
jest.mock('@sentry/react-native', () => ({
init: () => jest.fn(),
......@@ -65,6 +69,10 @@ jest.mock('react-native', () => {
return RN
})
jest.mock('expo-localization', () => ({
getLocales: jest.fn(() => [{ languageCode: 'en', countryCode: 'US' }]),
}))
jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn().mockImplementation(() => ({})),
useSafeAreaFrame: jest.fn().mockImplementation(() => ({})),
......
......@@ -53,7 +53,7 @@
},
"dependencies": {
"@amplitude/analytics-react-native": "1.4.0",
"@apollo/client": "3.9.6",
"@apollo/client": "3.10.4",
"@ethersproject/shims": "5.6.0",
"@formatjs/intl-datetimeformat": "4.5.1",
"@formatjs/intl-getcanonicallocales": "1.9.0",
......@@ -84,8 +84,8 @@
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.32.0",
"@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.2.1",
"@uniswap/v3-sdk": "3.11.1",
"@uniswap/sdk-core": "5.0.0",
"@uniswap/v3-sdk": "3.11.2",
"@walletconnect/core": "2.11.2",
"@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/utils": "2.11.2",
......
......@@ -45,11 +45,16 @@ import {
import { flexStyles, useIsDarkMode } from 'ui/src'
import { config } from 'uniswap/src/config'
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 { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags'
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 { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -133,7 +138,12 @@ function App(): JSX.Element | null {
const deviceId = useAsyncData(fetchAndSetDeviceId).data
const statSigOptions = {
const statSigOptions: {
user: StatsigUser
options: StatsigOptions
sdkKey: string
waitForInitialization: boolean
} = {
options: {
environment: {
tier: getStatsigEnvironmentTier(),
......@@ -144,7 +154,12 @@ function App(): JSX.Element | null {
initCompletionCallback: loadStatsigOverrides,
},
sdkKey: DUMMY_STATSIG_SDK_KEY,
user: deviceId ? { userID: deviceId } : {},
user: {
...(deviceId ? { userID: deviceId } : {}),
custom: {
app: StatsigCustomAppValue.Mobile,
},
},
waitForInitialization: true,
}
......
......@@ -17,7 +17,6 @@ import {
useDeviceInsets,
useSporeColors,
} from 'ui/src'
import { Plus } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme'
import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -25,6 +24,7 @@ import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'utilities/src/platform'
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 { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
......@@ -296,10 +296,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress={onPressAccount}
/>
<TouchableArea hapticFeedback mt="$spacing16" onPress={onPressAddWallet}>
<Flex row alignItems="center" gap="$spacing16" ml="$spacing24">
<Flex borderColor="$surface3" borderRadius="$roundedFull" borderWidth={1} p="$spacing8">
<Plus color="$neutral2" size="$icon.12" strokeWidth={2} />
</Flex>
<Flex row alignItems="center" gap="$spacing8" ml="$spacing24">
<PlusCircle />
<Text color="$neutral2" variant="buttonLabel3">
{t('account.wallet.button.add')}
</Text>
......
......@@ -544,7 +544,7 @@ exports[`AccountSwitcher renders correctly 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
"gap": 16,
"gap": 8,
"marginLeft": 24,
}
}
......@@ -552,6 +552,8 @@ exports[`AccountSwitcher renders correctly 1`] = `
<View
style={
{
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderBottomColor": "#2222220D",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
......@@ -566,17 +568,27 @@ exports[`AccountSwitcher renders correctly 1`] = `
"borderTopRightRadius": 999999,
"borderTopWidth": 1,
"flexDirection": "column",
"height": 40,
"justifyContent": "center",
"paddingBottom": 8,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
"shadowColor": "rgb(34,34,34)",
"shadowOffset": {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.050980392156862744,
"shadowRadius": 10,
"width": 40,
}
}
>
<RNSVGSvgView
align="xMidYMid"
bbHeight="12"
bbWidth="12"
bbHeight="16"
bbWidth="16"
fill="none"
focusable={false}
meetOrSlice={0}
......@@ -591,13 +603,13 @@ exports[`AccountSwitcher renders correctly 1`] = `
},
{
"color": "#7D7D7D",
"height": 12,
"width": 12,
"height": 16,
"width": 16,
},
{
"flex": 0,
"height": 12,
"width": 12,
"height": 16,
"width": 16,
},
]
}
......
......@@ -19,6 +19,7 @@ import { ChooseProfilePictureScreen } from 'src/features/unitags/ChooseProfilePi
import { ClaimUnitagScreen } from 'src/features/unitags/ClaimUnitagScreen'
import { EditUnitagProfileScreen } from 'src/features/unitags/EditUnitagProfileScreen'
import { UnitagConfirmationScreen } from 'src/features/unitags/UnitagConfirmationScreen'
import { AppLoadingScreen } from 'src/screens/AppLoadingScreen'
import { DevScreen } from 'src/screens/DevScreen'
import { EducationScreen } from 'src/screens/EducationScreen'
import { ExploreScreen } from 'src/screens/ExploreScreen'
......@@ -28,6 +29,7 @@ import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen'
import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders'
import { HomeScreen } from 'src/screens/HomeScreen'
import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen'
import { OnDeviceRecoveryScreen } from 'src/screens/Import/OnDeviceRecoveryScreen'
import { RestoreCloudBackupLoadingScreen } from 'src/screens/Import/RestoreCloudBackupLoadingScreen'
import { RestoreCloudBackupPasswordScreen } from 'src/screens/Import/RestoreCloudBackupPasswordScreen'
import { RestoreCloudBackupScreen } from 'src/screens/Import/RestoreCloudBackupScreen'
......@@ -235,6 +237,8 @@ export function OnboardingStackNavigator(): JSX.Element {
? SeedPhraseInputScreenV2
: SeedPhraseInputScreen
const isOnboardingKeyringEnabled = useFeatureFlag(FeatureFlags.OnboardingKeyring)
return (
<OnboardingContextProvider>
<OnboardingStack.Navigator>
......@@ -251,6 +255,13 @@ export function OnboardingStackNavigator(): JSX.Element {
headerRightContainerStyle: { paddingRight: spacing.spacing16 },
...TransitionPresets.SlideFromRightIOS,
}}>
{isOnboardingKeyringEnabled && (
<OnboardingStack.Screen
component={AppLoadingScreen}
name={OnboardingScreens.AppLoading}
options={navOptions.noHeader}
/>
)}
<OnboardingStack.Screen
component={LandingScreen}
name={OnboardingScreens.Landing}
......@@ -295,6 +306,11 @@ export function OnboardingStackNavigator(): JSX.Element {
component={ImportMethodScreen}
name={OnboardingScreens.ImportMethod}
/>
<OnboardingStack.Screen
component={OnDeviceRecoveryScreen}
name={OnboardingScreens.OnDeviceRecovery}
options={navOptions.noHeader}
/>
<OnboardingStack.Screen
component={RestoreCloudBackupLoadingScreen}
name={OnboardingScreens.RestoreCloudBackupLoading}
......
......@@ -90,6 +90,7 @@ export type SharedUnitagScreenParams = {
}
export type OnboardingStackParamList = {
[OnboardingScreens.AppLoading]: undefined
[OnboardingScreens.BackupManual]: OnboardingStackBaseParams
[OnboardingScreens.BackupCloudPasswordCreate]: {
address: Address
......@@ -104,6 +105,7 @@ export type OnboardingStackParamList = {
// import
[OnboardingScreens.ImportMethod]: OnboardingStackBaseParams
[OnboardingScreens.OnDeviceRecovery]: OnboardingStackBaseParams & { mnemonicIds: Address[] }
[OnboardingScreens.RestoreCloudBackupLoading]: OnboardingStackBaseParams
[OnboardingScreens.RestoreCloudBackup]: OnboardingStackBaseParams
[OnboardingScreens.RestoreCloudBackupPassword]: {
......
......@@ -30,12 +30,6 @@ import {
editAccountSaga,
editAccountSagaName,
} from 'wallet/src/features/wallet/accounts/editAccountSaga'
import {
createAccountActions,
createAccountReducer,
createAccountSaga,
createAccountSagaName,
} from 'wallet/src/features/wallet/create/createAccountSaga'
import {
createAccountsActions,
createAccountsReducer,
......@@ -70,12 +64,6 @@ const sagas = [
// All monitored sagas must be included here
export const monitoredSagas: Record<string, MonitoredSaga> = {
[createAccountSagaName]: {
name: createAccountSagaName,
wrappedSaga: createAccountSaga,
reducer: createAccountReducer,
actions: createAccountActions,
},
[createAccountsSagaName]: {
name: createAccountsSagaName,
wrappedSaga: createAccountsSaga,
......
......@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react'
import { MMKV } from 'react-native-mmkv'
import { Storage, persistReducer, persistStore } from 'redux-persist'
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 { logger } from 'utilities/src/logger/logger'
import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api'
......@@ -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) {
const createDebugger = require('redux-flipper').default
middlewares.push(createDebugger())
......
......@@ -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 CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, Photo } from 'ui/src/components/icons'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { iconSizes, spacing } from 'ui/src/theme'
import { Sentry } from 'utilities/src/logger/Sentry'
import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly'
import PasteButton from 'wallet/src/components/buttons/PasteButton'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { openSettings } from 'wallet/src/utils/linking'
type QRCodeScannerProps = {
......
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, CheckBox, Flex, Text } from 'ui/src'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
export function RemoveLastMnemonicWalletFooter({
onPress,
......
......@@ -12,12 +12,14 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
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 { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
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 { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import {
EditAccountAction,
editAccountActions,
......@@ -74,15 +76,34 @@ export function RemoveWalletModal(): JSX.Element | null {
})
}
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(
editAccountActions.trigger({
type: EditAccountAction.Remove,
address: accAddress,
notificationsEnabled: !!pushNotificationsEnabled,
accounts: accountsToRemove,
})
)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'RemoveWalletModal', function: 'Keyring.removeMnemonic' },
})
})
}
} else {
dispatch(
editAccountActions.trigger({
type: EditAccountAction.Remove,
accounts: accountsToRemove,
})
)
}
onClose()
setInProgress(false)
......
......@@ -13,14 +13,7 @@ import {
TAB_VIEW_SCROLL_THROTTLE,
TabProps,
} from 'src/components/layout/TabHelpers'
import {
AnimatedFlex,
Flex,
Loader,
useDeviceDimensions,
useDeviceInsets,
useSporeColors,
} from 'ui/src'
import { AnimatedFlex, Flex, Loader, useDeviceInsets, useSporeColors } from 'ui/src'
import { zIndices } from 'ui/src/theme'
import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......@@ -88,7 +81,7 @@ export const TokenBalanceListInner = forwardRef<
const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const { onContentSizeChange, adaptiveFooter, footerHeight } = useAdaptiveFooter(
const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(
containerProps?.contentContainerStyle
)
......@@ -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.
// 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(
({ item }: { item: TokenBalanceListRow }): JSX.Element => {
return <TokenBalanceItemRow footerHeight={footerHeight} item={item} />
},
[footerHeight]
({ item }: { item: TokenBalanceListRow }): JSX.Element => <TokenBalanceItemRow item={item} />,
[]
)
const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, [])
const ListEmptyComponent = useMemo(() => {
if (!balancesById) {
return (
<Flex grow px="$spacing24" style={containerProps?.emptyContainerStyle}>
<Flex grow px="$spacing24">
{empty}
</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)
......@@ -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.
return (
<ReactNavigationPerformanceView
......@@ -196,21 +211,6 @@ export const TokenBalanceListInner = forwardRef<
// Marks the home screen as interactive when balances are defined
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
ref={ref as never}
ListEmptyComponent={ListEmptyComponent}
......@@ -219,9 +219,10 @@ export const TokenBalanceListInner = forwardRef<
ListFooterComponentStyle={ListFooterComponentStyle}
ListHeaderComponent={ListHeaderComponent}
contentContainerStyle={containerProps?.contentContainerStyle}
data={isFocused ? rows : cachedRows}
data={data}
getItemLayout={getItemLayout}
initialNumToRender={20}
keyExtractor={keyExtractor}
maxToRenderPerBatch={20}
refreshControl={refreshControl}
refreshing={refreshing}
......@@ -236,20 +237,15 @@ export const TokenBalanceListInner = forwardRef<
onScroll={scrollHandler}
onScrollEndDrag={containerProps?.onScrollEndDrag}
/>
)}
</ReactNavigationPerformanceView>
)
})
const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
item,
footerHeight,
}: {
item: TokenBalanceListRow
footerHeight: Animated.SharedValue<number>
}) {
const { fullHeight } = useDeviceDimensions()
const {
balancesById,
hiddenTokensCount,
......@@ -266,9 +262,6 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
isExpanded={hiddenTokensExpanded}
numHidden={hiddenTokensCount}
onPress={(): void => {
if (hiddenTokensExpanded) {
footerHeight.value = fullHeight
}
setHiddenTokensExpanded(!hiddenTokensExpanded)
}}
/>
......
......@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { CurrencyId } from 'uniswap/src/types/currency'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { SendButton } from './SendButton'
/**
......
import React from 'react'
import { Flex, flexStyles, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import {
SafetyLevel,
TokenDetailsScreenQuery,
} 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 { fromGraphQLChain } from 'wallet/src/features/chains/utils'
......
......@@ -8,11 +8,11 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { Button, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
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 { CHAIN_INFO } from 'wallet/src/constants/chains'
import { pushNotification } from 'wallet/src/features/notifications/slice'
......
......@@ -2,9 +2,9 @@ import React from 'react'
import { StyleSheet } from 'react-native'
import { Flex } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { DappInfo } from 'uniswap/src/types/walletConnect'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { ImageUri } from 'wallet/src/features/images/ImageUri'
......
......@@ -2,13 +2,13 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
import { ChainId } from 'uniswap/src/types/chains'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { useUSDValue } from 'wallet/src/features/gas/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount'
export function SpendingDetails({
......
......@@ -5,12 +5,12 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay'
import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useIsDarkMode } from 'ui/src'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
import { iconSizes, spacing } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
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 { CHAIN_INFO } from 'wallet/src/constants/chains'
import { GasFeeResult } from 'wallet/src/features/gas/types'
......
......@@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'
import { Flex, Separator, Text, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg'
import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ChainId } from 'uniswap/src/types/chains'
import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo'
import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal'
import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO } from 'wallet/src/constants/chains'
......
......@@ -8,6 +8,7 @@ import {
} from 'src/features/deepLinking/constants'
import { DynamicConfigs } from 'uniswap/src/features/gating/configs'
import { useDynamicConfig } from 'uniswap/src/features/gating/hooks'
import { RPCType } from 'uniswap/src/types/chains'
import {
EthMethod,
EthTransaction,
......@@ -16,7 +17,6 @@ import {
UwULinkRequest,
} from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
import { RPCType } from 'wallet/src/constants/chains'
import { AssetType } from 'wallet/src/entities/assets'
import { ContractManager } from 'wallet/src/features/contracts/ContractManager'
import { ProviderManager } from 'wallet/src/features/providers'
......
import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store'
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 { Language } from 'wallet/src/features/language/constants'
import {
......@@ -12,7 +13,6 @@ import {
tokenProjectMarket,
} from 'wallet/src/test/fixtures'
import { queryResolvers } from 'wallet/src/test/utils'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import FavoriteTokenCard, { FavoriteTokenCardProps } from './FavoriteTokenCard'
const mockedNavigation = {
......
......@@ -11,12 +11,13 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks'
import { AnimatedFlex, AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text } from 'ui/src'
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 { SectionName } from 'uniswap/src/features/telemetry/constants'
import { ChainId } from 'uniswap/src/types/chains'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types'
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 { PollingInterval } from 'wallet/src/constants/misc'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
......@@ -24,7 +25,6 @@ import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
import { removeFavoriteToken } from 'wallet/src/features/favorites/slice'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114
......
......@@ -6,11 +6,11 @@ import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { TokenMetadata } from 'src/components/tokens/TokenMetadata'
import { disableOnPress } from 'src/utils/disableOnPress'
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 { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ChainId } from 'uniswap/src/types/chains'
import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { RelativeChange } from 'wallet/src/components/text/RelativeChange'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
......
......@@ -5,10 +5,10 @@ import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress'
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 { ElementName, MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
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 { SearchContext } from 'wallet/src/features/search/SearchContext'
import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult'
......
......@@ -2,7 +2,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { SpinningLoader } from 'ui/src/loading/SpinningLoader'
interface FiatOnRampCtaButtonProps {
onPress: () => void
......
......@@ -4,8 +4,8 @@ import { StyleSheet } from 'react-native'
import { Loader } from 'src/components/loading'
import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { concatStrings } from 'utilities/src/primitives/string'
import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getOptionalServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri'
......
......@@ -6,7 +6,7 @@ import { useAppDispatch } from 'src/app/hooks'
import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
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 { Flex } from 'ui/src'
import { NoTokens } from 'ui/src/components/icons'
......@@ -20,8 +20,6 @@ import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceL
export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances]
// ignore ref type
export const TokensTab = memo(
forwardRef<FlatList<TokenBalanceListRow>, TabProps & { isExternalProfile?: boolean }>(
function _TokensTab(
......@@ -50,17 +48,6 @@ export const TokensTab = memo(
[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 => {
dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
......@@ -85,7 +72,7 @@ export const TokensTab = memo(
<Flex grow backgroundColor="$surface1">
<TokenBalanceList
ref={ref}
containerProps={formattedContainerProps}
containerProps={containerProps}
empty={renderEmpty}
headerHeight={headerHeight}
isExternalProfile={isExternalProfile}
......
......@@ -12,9 +12,9 @@ import {
} from 'react-native'
import Animated, { SharedValue } from 'react-native-reanimated'
import { Route } from 'react-native-tab-view'
import { PendingNotificationBadge } from 'src/features/notifications/PendingNotificationBadge'
import { Flex, Text } from 'ui/src'
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_BAR_HEIGHT = 48
......@@ -97,7 +97,7 @@ export type TabContentProps = Partial<FlatListProps<unknown>> & {
scrollEventThrottle?: number
}
export const renderTabLabel = ({
export const TabLabel = ({
route,
focused,
isExternalProfile,
......
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 {
return (
<Flex
row
alignItems="stretch"
backgroundColor="$surface1"
gap="$spacing24"
height="50%"
justifyContent="space-evenly"
backgroundColor="$surface2"
borderRadius="$rounded20"
gap="$spacing36"
height="40%"
mt="$spacing16"
px="$spacing36">
px="$spacing32"
py="$spacing24">
<Flex grow justifyContent="space-between">
<HiddenWordViewColumn indexes={LEFT_COLUMN_INDEXES} />
<HiddenWordViewColumn />
</Flex>
<Flex grow justifyContent="space-between">
<HiddenWordViewColumn indexes={RIGHT_COLUMN_INDEXES} />
<HiddenWordViewColumn />
</Flex>
</Flex>
)
}
function HiddenWordViewColumn({ indexes }: { indexes: number[] }): JSX.Element {
function HiddenWordViewColumn(): JSX.Element {
return (
<>
{indexes.map((value) => (
<Flex
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>
{new Array(ROW_COUNT).fill(0).map((_, idx) => (
<Flex key={idx} backgroundColor="$surface3" borderRadius="$rounded20" height={10} />
))}
</>
)
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { requireNativeComponent, StyleProp, ViewProps } from 'react-native'
import { FlexProps, flexStyles, HiddenFromScreenReaders, useDeviceDimensions } from 'ui/src'
interface NativeMnemonicConfirmationProps {
mnemonicId: Address
shouldShowSmallText: boolean
selectedWordPlaceholder: string
onConfirmComplete: () => void
}
......@@ -22,12 +24,14 @@ const mnemonicConfirmationStyle: StyleProp<FlexProps> = {
}
export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element {
const { t } = useTranslation()
const { fullHeight } = useDeviceDimensions()
const shouldShowSmallText = fullHeight < 700
return (
<HiddenFromScreenReaders style={flexStyles.fill}>
<NativeMnemonicConfirmation
selectedWordPlaceholder={t('onboarding.backup.manual.selectedWordPlaceholder')}
shouldShowSmallText={shouldShowSmallText}
style={mnemonicConfirmationStyle}
{...props}
......
import React from 'react'
import { requireNativeComponent, StyleSheet, ViewProps } from 'react-native'
import { flexStyles, HiddenFromScreenReaders } from 'ui/src'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
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 {
mnemonicId: Address
copyText: string
copiedText: string
mnemonicId: string
onHeightMeasured: (event: NativeSyntheticEvent<HeightMeasuredEvent>) => void
}
const NativeMnemonicDisplay = requireNativeComponent<NativeMnemonicDisplayProps>('MnemonicDisplay')
type MnemonicDisplayProps = ViewProps & NativeMnemonicDisplayProps
const styles = StyleSheet.create({
mnemonicDisplay: {
flex: 1,
flexGrow: 1,
},
})
type MnemonicDisplayProps = ViewProps & Pick<NativeMnemonicDisplayProps, 'mnemonicId'>
export function MnemonicDisplay(props: MnemonicDisplayProps): JSX.Element {
const { t } = useTranslation()
const [height, setHeight] = useState(0)
return (
<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>
)
}
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({
const onPasswordSubmitEditing = (): void => {
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
......@@ -71,10 +70,6 @@ export function CloudBackupPasswordForm({
}
const onPressNext = (): void => {
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
setError(PasswordErrors.PasswordsDoNotMatch)
return
......@@ -86,9 +81,7 @@ export function CloudBackupPasswordForm({
}
let errorText = ''
if (error === PasswordErrors.WeakPassword) {
errorText = t('settings.setting.backup.password.error.weak')
} else if (error === PasswordErrors.PasswordsDoNotMatch) {
if (error === PasswordErrors.PasswordsDoNotMatch) {
errorText = t('settings.setting.backup.password.error.mismatch')
} else if (error) {
// 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 { Image, StyleSheet } from 'react-native'
import { Modal } from 'src/components/modals/Modal'
import { SplashScreen } from 'src/features/appLoading/SplashScreen'
import { useLockScreenContext } from 'src/features/authentication/lockScreenContext'
import { useBiometricPrompt } from 'src/features/biometrics/hooks'
import { Flex, TouchableArea, useDeviceDimensions, useDeviceInsets, useIsDarkMode } from 'ui/src'
import { UNISWAP_LOGO_LARGE } from 'ui/src/assets'
import { isAndroid } from 'utilities/src/platform'
import { TouchableArea, useDeviceDimensions, useDeviceInsets } from 'ui/src'
export const SPLASH_SCREEN = { uri: 'SplashScreen' }
......@@ -14,7 +12,6 @@ export function LockScreenModal(): JSX.Element | null {
const { trigger } = useBiometricPrompt(() => setIsLockScreenVisible(false))
const insets = useDeviceInsets()
const dimensions = useDeviceDimensions()
const isDarkMode = useIsDarkMode()
if (!isLockScreenVisible) {
return null
......@@ -33,38 +30,14 @@ export function LockScreenModal(): JSX.Element | null {
transparent={false}
width="100%">
<TouchableArea onPress={(): Promise<void> => trigger()}>
<Flex
alignItems="center"
backgroundColor={isDarkMode ? '$surface1' : '$sporeWhite'}
justifyContent={isAndroid ? 'center' : undefined}
pointerEvents="none"
<SplashScreen
style={{
width: dimensions.fullWidth,
height: dimensions.fullHeight,
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>
</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 {
serviceProvider: FORServiceProvider
......
......@@ -5,8 +5,8 @@ import { useAppDispatch } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice'
import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
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 { RemoteImage } from 'wallet/src/features/images/RemoteImage'
......
......@@ -9,33 +9,20 @@ import {
} from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler'
import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import {
AnimatedFlex,
ColorTokens,
Flex,
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 { AnimatedFlex, ColorTokens, Flex, HapticFeedback, Text, useSporeColors } from 'ui/src'
import { fonts, spacing } from 'ui/src/theme'
import { Pill } from 'uniswap/src/components/pill/Pill'
import { SelectTokenButton } from 'uniswap/src/features/fiatOnRamp/SelectTokenButton'
import { NumberType } from 'utilities/src/format/types'
import { usePrevious } from 'utilities/src/react/hooks'
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 { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { Pill } from 'wallet/src/components/text/Pill'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { errorShakeAnimation } from 'wallet/src/utils/animations'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing'
import { useFormatExactCurrencyAmount } from './hooks'
const MAX_INPUT_FONT_SIZE = 56
const MIN_INPUT_FONT_SIZE = 32
......@@ -146,6 +133,11 @@ export function FiatOnRampAmountSection({
// Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms
const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2)
const formattedAmount = useFormatExactCurrencyAmount(
quoteAmount.toString(),
currency.currencyInfo?.currency
)
return (
<Flex onLayout={onInputPanelLayout}>
<Flex
......@@ -195,11 +187,11 @@ export function FiatOnRampAmountSection({
onSelectionChange={onSelectionChange}
/>
</AnimatedFlex>
{currency.currencyInfo && (
{currency.currencyInfo && formattedAmount && (
<SelectTokenButton
amount={quoteAmount}
amountReady={quoteCurrencyAmountReady}
disabled={notAvailableInThisRegion}
formattedAmount={formattedAmount}
loading={selectTokenLoading}
selectedCurrencyInfo={currency.currencyInfo}
onPress={onTokenSelectorPress}
......@@ -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
function PredefinedAmount({
amount,
......
......@@ -5,10 +5,10 @@ import React, { createContext, useContext, useState } from 'react'
import { SectionListData } from 'react-native'
import { getCountry } from 'react-native-localize'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { FORQuote, FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ChainId } from 'uniswap/src/types/chains'
import { getNativeAddress } from 'wallet/src/constants/addresses'
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 { buildCurrencyId } from 'wallet/src/utils/currencyId'
......
......@@ -17,14 +17,14 @@ import {
} from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg'
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 { bubbleToTop } from 'utilities/src/primitives/array'
import { useDebounce } from 'utilities/src/time/timing'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
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'
const ICON_SIZE = 32 // design prefers a custom value here
......
......@@ -3,6 +3,11 @@ import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react'
import { useTranslation } from 'react-i18next'
import { Delay } from 'src/components/layout/Delayed'
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 { useDebounce } from 'utilities/src/time/timing'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
......@@ -11,11 +16,6 @@ import {
useAppFiatCurrencyInfo,
useFiatCurrencyInfo,
} 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 {
isFiatOnRampApiError,
isInvalidRequestAmountTooHigh,
......
......@@ -8,6 +8,8 @@ import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { ColorTokens, useSporeColors } from 'ui/src'
import { uniswapUrls } from 'uniswap/src/constants/urls'
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 { logger } from 'utilities/src/logger/logger'
import { useDebounce } from 'utilities/src/time/timing'
......@@ -18,7 +20,6 @@ import {
import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses'
import { fromMoonpayNetwork, toSupportedChainId } from 'wallet/src/features/chains/utils'
import {
useFiatOnRampAggregatorSupportedTokensQuery,
useFiatOnRampBuyQuoteQuery,
useFiatOnRampIpAddressQuery,
useFiatOnRampLimitsQuery,
......@@ -26,7 +27,7 @@ import {
useFiatOnRampWidgetUrlQuery,
} from 'wallet/src/features/fiatOnRamp/api'
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 { addTransaction } from 'wallet/src/features/transactions/slice'
import {
......
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { FORQuote, FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
export function getServiceProviderForQuote(
quote: FORQuote | undefined,
......
......@@ -8,7 +8,7 @@ import {
getFirestoreUidRef,
} from 'src/features/firebase/utils'
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 { getKeys } from 'utilities/src/primitives/objects'
import { Language } from 'wallet/src/features/language/constants'
......@@ -113,9 +113,15 @@ function* editAccountDataInFirebase(actionData: ReturnType<typeof editAccountAct
const { type, address } = payload
switch (type) {
case EditAccountAction.Remove:
yield* call(removeAccountFromFirebase, address, payload.notificationsEnabled)
break
case EditAccountAction.Remove: {
const accountsToRemove = payload.accounts
yield* all(
accountsToRemove.map((account: { address: Address; pushNotificationsEnabled: boolean }) =>
call(removeAccountFromFirebase, account.address, account.pushNotificationsEnabled)
)
)
return
}
case EditAccountAction.Rename:
yield* call(renameAccountInFirebase, address, payload.newName)
break
......@@ -169,6 +175,10 @@ export function* removeAccountFromFirebase(address: Address, notificationsEnable
const selectAccountNotificationSetting = makeSelectAccountNotificationSetting()
export function* renameAccountInFirebase(address: Address, newName: string) {
if (!address) {
throw new Error('Address is required for renameAccountInFirebase')
}
try {
yield* call(maybeUpdateFirebaseMetadata, address, { name: newName })
} catch (error) {
......@@ -180,6 +190,10 @@ export function* toggleFirebaseNotificationSettings({
address,
enabled,
}: TogglePushNotificationParams) {
if (!address) {
throw new Error('Address is required for toggleFirebaseNotificationSettings')
}
try {
const accounts = yield* appSelect(selectAccounts)
const account = accounts[address]
......
......@@ -3,10 +3,10 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet
import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
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'
export interface AppModalState<T> {
......
import { Linking } from 'react-native'
import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal'
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 { ONE_SECOND_MS } from 'utilities/src/time/time'
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 => {
OneSignal.setAppId(config.onesignalAppId)
......@@ -24,7 +24,7 @@ export const initOneSignal = (): void => {
setTimeout(
() =>
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
)
......
import { providers } from 'ethers'
import { default as React, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TouchableWithoutFeedback } from 'react-native'
import { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
import { StyleSheet, TouchableWithoutFeedback } from 'react-native'
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
import { useShouldShowNativeKeyboard } from 'src/app/hooks'
import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import { Screen } from 'src/components/layout/Screen'
......@@ -10,7 +10,7 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet
import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader'
import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus'
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 { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
......@@ -52,7 +52,6 @@ import {
TokenSelectorFlow,
} from 'wallet/src/features/transactions/transfer/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'
interface TransferFormProps {
......@@ -119,32 +118,66 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
setRenderInnerContentRouter(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 onReviewNext = useCallback(() => setStep(TransactionStep.SUBMITTED), [setStep])
const onReviewPrev = useCallback(() => setStep(TransactionStep.FORM), [setStep])
const onRetrySubmit = useCallback(() => setStep(TransactionStep.FORM), [setStep])
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 (
<>
<TouchableWithoutFeedback>
<Screen edges={['top']}>
<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
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 && (
<TransferHeader
dispatch={dispatch}
......@@ -176,16 +209,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
/>
)}
</Flex>
<Flex width="100%">
{showRecipientSelector ? (
<RecipientSelect
recipient={state.recipient}
onSelectRecipient={onSelectRecipient}
onToggleShowRecipientSelector={onToggleShowRecipientSelector}
/>
) : null}
</Flex>
</Animated.View>
{showViewOnlyModal && (
<WarningModal
......@@ -205,7 +229,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
onConfirm={(): void => setShowViewOnlyModal(false)}
/>
)}
</AnimatedFlex>
</Flex>
</Screen>
</TouchableWithoutFeedback>
{!!state.selectingCurrencyField && (
......@@ -340,3 +364,11 @@ function TransferInnerContent({
return null
}
}
const styles = StyleSheet.create({
screen: {
height: '100%',
position: 'absolute',
width: '100%',
},
})
......@@ -23,6 +23,7 @@ import {
import { ENS_LOGO } from 'ui/src/assets'
import { InfoCircleFilled, LinkHorizontalAlt } from 'ui/src/components/icons'
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 Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants'
......@@ -34,7 +35,6 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink'
import { Pill } from 'wallet/src/components/text/Pill'
import {
useCreateOnboardingAccountIfNone,
useOnboardingContext,
......
......@@ -101,6 +101,14 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams
}
const { transactionResponse } = yield* call(sendTransaction, txParams)
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') {
......
......@@ -8,7 +8,7 @@ import { ChainId } from 'uniswap/src/types/chains'
const EIP155_MAINNET = 'eip155:1'
const EIP155_POLYGON = 'eip155:137'
const EIP155_OPTIMISM = 'eip155:10'
const EIP155_AVAX_UNSUPPORTED = 'eip155:43114'
const EIP155_LINEA_UNSUPPORTED = 'eip155:59144'
const TEST_ADDRESS = '0xdFb84E543C39ACa3c6a39ea4e3B6c40eE7d2EBdA'
......@@ -39,7 +39,7 @@ describe(getSupportedWalletConnectChains, () => {
it('handles list of valid chains including an invalid chain', () => {
expect(
getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_AVAX_UNSUPPORTED])
getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_LINEA_UNSUPPORTED])
).toEqual([ChainId.Mainnet, ChainId.Polygon])
})
})
......@@ -54,6 +54,6 @@ describe(getChainIdFromEIP155String, () => {
})
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> {
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
export function generateAndStoreMnemonic(): Promise<string> {
return RNEthersRS.generateAndStoreMnemonic()
......@@ -43,6 +47,10 @@ export function generateAndStorePrivateKey(
return RNEthersRS.generateAndStorePrivateKey(mnemonicId, derivationIndex)
}
export function removePrivateKey(address: string): Promise<boolean> {
return RNEthersRS.removePrivateKey(address)
}
export function signTransactionHashForAddress(
address: 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