diff --git a/packages/yoroi-extension/app/Routes.js b/packages/yoroi-extension/app/Routes.js index 9ce82dcbbf..d3cdf1b58e 100644 --- a/packages/yoroi-extension/app/Routes.js +++ b/packages/yoroi-extension/app/Routes.js @@ -75,8 +75,6 @@ const WalletSettingsPagePromise = () => import('./containers/settings/categories const WalletSettingsPage = React.lazy(WalletSettingsPagePromise); const ExternalStorageSettingsPagePromise = () => import('./containers/settings/categories/ExternalStorageSettingsPage'); const ExternalStorageSettingsPage = React.lazy(ExternalStorageSettingsPagePromise); -const OAuthDropboxPagePromise = () => import('./containers/settings/categories/OAuthDropboxPage'); -const OAuthDropboxPage = React.lazy(OAuthDropboxPagePromise); const TermsOfUseSettingsPagePromise = () => import('./containers/settings/categories/TermsOfUseSettingsPage'); const TermsOfUseSettingsPage = React.lazy(TermsOfUseSettingsPagePromise); const SupportSettingsPagePromise = () => import('./containers/settings/categories/SupportSettingsPage'); @@ -87,9 +85,6 @@ const AnalyticsSettingsPage = React.lazy(AnalyticsSettingsPagePromise); const NightlyPagePromise = () => import('./containers/profile/NightlyPage'); const NightlyPage = React.lazy(NightlyPagePromise); -const MyWalletsPagePromise = () => import('./containers/wallet/MyWalletsPage'); -const MyWalletsPage = React.lazy(MyWalletsPagePromise); - const WalletSummaryPagePromise = () => import('./containers/wallet/WalletSummaryPage'); const WalletSummaryPage = React.lazy(WalletSummaryPagePromise); @@ -132,6 +127,9 @@ const TokensPageRevamp = React.lazy(TokensPageRevampPromise); const TokensDetailPageRevampPromise = () => import('./containers/wallet/TokenDetailPageRevamp'); const TokensDetailPageRevamp = React.lazy(TokensDetailPageRevampPromise); +const CashbackPagePromise = () => import('./containers/cashback/CashbackPage'); +const CashbackPage = React.lazy(CashbackPagePromise) + const NFTsPageRevampPromise = () => import('./containers/wallet/NFTsPageRevamp'); const NFTsPageRevamp = React.lazy(NFTsPageRevampPromise); @@ -157,11 +155,9 @@ export const LazyLoadPromises: Array<() => any> = [ GeneralSettingsPagePromise, WalletSettingsPagePromise, ExternalStorageSettingsPagePromise, - OAuthDropboxPagePromise, TermsOfUseSettingsPagePromise, SupportSettingsPagePromise, NightlyPagePromise, - MyWalletsPagePromise, WalletSummaryPagePromise, WalletSendPagePromise, WalletAssetsPagePromise, @@ -216,10 +212,13 @@ export const Routes = (stores: StoresMap): Node => { path={ROUTES.PROFILE.OPT_FOR_ANALYTICS} component={props => } /> - } /> } /> wrapAssets({ ...props, stores }, AssetsSubpages(stores))} /> wrapNFTs({ ...props, stores }, NFTsSubPages(stores))} /> + } + /> } /> { wrapSwap({ ...props, stores }, SwapSubpages(stores))} /> } /> } /> - } - /> } /> } /> } /> @@ -259,8 +253,7 @@ export const Routes = (stores: StoresMap): Node => { path={ROUTES.PORTFOLIO.ROOT} component={() => PortfolioSubpages(stores)} /> - - + diff --git a/packages/yoroi-extension/app/UI/context/IntlProvider.tsx b/packages/yoroi-extension/app/UI/context/IntlProvider.tsx index 76d0e5b1f0..105696d691 100644 --- a/packages/yoroi-extension/app/UI/context/IntlProvider.tsx +++ b/packages/yoroi-extension/app/UI/context/IntlProvider.tsx @@ -8,4 +8,4 @@ export const IntlProvider = ({ children, intl }: { children: React.ReactNode; in }; export const useIntl = () => - React.useContext(IntlProviderContext) ?? console.warn('IntlProviderontext: needs to be wrapped in a IntlProvider'); + React.useContext(IntlProviderContext) ?? console.warn('IntlProviderContext: needs to be wrapped in a IntlProvider'); diff --git a/packages/yoroi-extension/app/api/ada/index.js b/packages/yoroi-extension/app/api/ada/index.js index 30e293a58a..21a62fc39b 100644 --- a/packages/yoroi-extension/app/api/ada/index.js +++ b/packages/yoroi-extension/app/api/ada/index.js @@ -44,9 +44,10 @@ import { asGetSigningKey, asHasLevels, asHasUtxoChains, + asGetStakingKey, } from './lib/storage/models/PublicDeriver/traits'; import { ConceptualWallet } from './lib/storage/models/ConceptualWallet/index'; -import type { IHasLevels } from './lib/storage/models/ConceptualWallet/interfaces'; +import { WalletTypeOption, type IHasLevels } from './lib/storage/models/ConceptualWallet/interfaces'; import type { Address, Addressing, @@ -165,6 +166,7 @@ import TimeUtils from './lib/storage/bridge/timeUtils'; import type { IFetcher } from './lib/state-fetch/IFetcher.types'; import { Bip44DerivationLevels, CoinType } from '@emurgo/yoroi-lib'; import type { ProtocolParameters } from '@emurgo/yoroi-lib/dist/protocol-parameters/models'; +import { encryptWithPassword } from '../../utils/passwordCipher'; // ADA specific Request / Response params @@ -494,7 +496,6 @@ export type CreateHardwareWalletRequest = {| publicKey: string, ...Addressing, hwFeatures: HWFeatures, - checkAddressesInUse: FilterFunc, network: $ReadOnly, |}; export type CreateHardwareWalletResponse = {| @@ -1808,6 +1809,86 @@ export default class AdaApi { } } + async cloneWallet( + db: lf$Database, + publicDeriver: PublicDeriver<>, + network: $ReadOnly, + ): Promise> { + const withPublicKey = asGetPublicKey(publicDeriver); + if (!withPublicKey) { + throw new Error('unable to get public key'); + } + const publicKey = await withPublicKey.getPublicKey(); + const accountPublicKey = RustModule.WalletV4.Bip32PublicKey.from_hex(publicKey.Hash); + + const conceptualWallet = publicDeriver.getParent(); + const walletName = (await conceptualWallet.getFullConceptualWalletInfo()).Name; + + let wallet; + if (conceptualWallet.getWalletType() === WalletTypeOption.HARDWARE_WALLET) { + const withStakingKey = asGetStakingKey(publicDeriver); + if (!withStakingKey) { + throw new Error('unable to get staking key'); + } + const stakingKey = await withStakingKey.getStakingKey(); + + const hwMeta = conceptualWallet.getHwWalletMeta(); + if (!hwMeta) { + throw new Error('unexpectedly missing hardware metadata'); + } + + wallet = await createHardwareCip1852Wallet({ + db, + accountPublicKey, + accountIndex: stakingKey.addressing.path[2], + walletName, + accountName: '', + hwWalletMetaInsert: { + Vendor: hwMeta.Vendor, + Model: hwMeta.Model, + DeviceId: hwMeta.DeviceId, + }, + network, + }); + } else { + const withSigningKey = asGetSigningKey(publicDeriver); + if (!withSigningKey) { + throw new Error('unable to get signing key'); + } + const signingKey = await withSigningKey.getSigningKey(); + const encryptedRoot = signingKey.row.Hash; + + const accountIndex = signingKey.path[3].Index; + if (accountIndex === null) { + throw new Error('missing account index'); + } + + wallet = await createStandardCip1852Wallet({ + db, + encryptedRoot, + accountPublicKey, + accountIndex, + walletName, + accountName: '', // set account name empty now + network, + }); + } + + const cip1852Wallet = await Cip1852Wallet.createCip1852Wallet( + db, + wallet.cip1852WrapperRow, + ); + + if (wallet.publicDeriver.length !== 1) { + throw new Error(`${nameof(AdaApi)}::${nameof(this.cloneWallet)} should only do 1 HW derivation at a time`); + } + const pubDeriverResult = wallet.publicDeriver[0].publicDeriverResult; + return await PublicDeriver.createPublicDeriver( + pubDeriverResult, + cip1852Wallet, + ); + } + /** * Creates wallet and saves result to DB */ @@ -1823,11 +1904,21 @@ export default class AdaApi { try { // Note: we only restore for 0th account const rootPk = generateWalletRootKey(recoveryPhrase); + const encryptedRoot = encryptWithPassword( + walletPassword, + rootPk.as_bytes(), + ); + const accountPublicKey = rootPk + .derive(WalletTypePurpose.CIP1852) + .derive(CoinTypes.CARDANO) + .derive(request.accountIndex) + .to_public(); + const newPubDerivers = []; const wallet = await createStandardCip1852Wallet({ db: request.db, - rootPk, - password: walletPassword, + encryptedRoot, + accountPublicKey, accountIndex: request.accountIndex, walletName, accountName: '', // set account name empty now @@ -2694,11 +2785,13 @@ export async function encodeHardwareWalletSignResult( signatureHex: string, payloadHex: string, signingPublicKeyHex: string, + payloadHashed: boolean = false, ): Promise<{| signature: string, key: string |}> { const coseSign1 = await buildCoseSign1FromSignature ( hexToBytes(addressHex), hexToBytes(signatureHex), hexToBytes(payloadHex), + payloadHashed, ); const key = makeCip8Key(hexToBytes(signingPublicKeyHex)); diff --git a/packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/utils.js b/packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/utils.js index b602dc350c..29e11fff60 100644 --- a/packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/utils.js +++ b/packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/utils.js @@ -178,6 +178,7 @@ export const buildCoseSign1FromSignature = async ( address: Buffer, signature: Buffer, payload: Buffer, + payloadHashed: boolean = false, ): Promise => { const protectedHeader = RustModule.MessageSigning.HeaderMap.new(); protectedHeader.set_algorithm_id( @@ -191,8 +192,19 @@ export const buildCoseSign1FromSignature = async ( ); const protectedSerialized = RustModule.MessageSigning.ProtectedHeaderMap.new(protectedHeader); const unprotected = RustModule.MessageSigning.HeaderMap.new(); + if (payloadHashed) { + unprotected.set_header( + RustModule.MessageSigning.Label.new_text('hashed'), + RustModule.MessageSigning.CBORValue.new_special( + RustModule.MessageSigning.CBORSpecial.new_bool(true) + ), + ); + } const headers = RustModule.MessageSigning.Headers.new(protectedSerialized, unprotected); const builder = RustModule.MessageSigning.COSESign1Builder.new(headers, payload, false); + if (payloadHashed) { + builder.hash_payload(); + } return builder.build(signature); } diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/adaMigration.js b/packages/yoroi-extension/app/api/ada/lib/storage/adaMigration.js index d7e6b98024..038da97853 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/adaMigration.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/adaMigration.js @@ -21,7 +21,7 @@ import { migrateFromStorageV1 } from './bridge/walletBuilder/byron'; import { RustModule } from '../cardanoCrypto/rustLoader'; import { removeAllTransactions } from './bridge/updateTransactions'; import { removePublicDeriver } from './bridge/walletBuilder/remove'; -import { asGetAllUtxos, asHasLevels, } from './models/PublicDeriver/traits'; +import { asGetAllUtxos, asHasLevels, asGetPublicKey, } from './models/PublicDeriver/traits'; import { ConceptualWallet, isLedgerNanoWallet, } from './models/ConceptualWallet/index'; import { loadWalletsFromStorage } from './models/load'; import environment from '../../../../environment'; @@ -32,6 +32,7 @@ import { getAllSchemaTables, raii, } from './database/utils'; import type { BlockRow } from './database/primitives/tables'; import { GetBlock } from './database/primitives/api/read'; import { ModifyUtxoAtSafePoint } from './database/utxo/api/write'; +import type { PublicDeriver } from './models/PublicDeriver/index'; export async function migrateToLatest( localStorageApi: LocalStorageApi, @@ -98,6 +99,7 @@ export async function migrateToLatest( ['<3.8.0', () => cardanoTxHistoryReset(persistentDb)], ['<4.18', () => populateNewUtxodata(persistentDb)], ['<5.4', () => unsetLegacyThemeFlags(localStorageApi)], + ['<5.5', () => migrateWalletOrderAndSelectedWalletForNetworkSwitch(persistentDb, localStorageApi)], ]; let appliedMigration = false; @@ -421,4 +423,43 @@ async function unsetLegacyThemeFlags(localStorageApi: LocalStorageApi): Promise< } await localStorageApi.unsetLegacyThemeFlags(); return true; -} \ No newline at end of file +} + +async function migrateWalletOrderAndSelectedWalletForNetworkSwitch( + db: lf$Database, + localStorageApi: LocalStorageApi, +): Promise { + const wallets = await loadWalletsFromStorage(db); + + const oldWalletOrder = (await localStorageApi.getWalletsNavigation())?.cardano || []; + const publicKeyList = []; + for (const id of oldWalletOrder) { + const wallet = wallets.find(w => w.getPublicDeriverId() === id); + if (wallet) { + publicKeyList.push(await getPublicKey(wallet)); + } + } + for (const wallet of wallets) { + if (!oldWalletOrder.includes(wallet.getPublicDeriverId())) { + publicKeyList.push(await getPublicKey(wallet)); + } + } + await localStorageApi.saveWalletListOrder(publicKeyList); + + const selectedWalletId = await localStorageApi.getSelectedWalletId(); + const selectedWallet = wallets.find( + wallet => wallet.getPublicDeriverId() === selectedWalletId + ); + if (selectedWallet) { + await localStorageApi.setSelectedWalletPublicKey(await getPublicKey(selectedWallet)); + } + return true; +} + +async function getPublicKey(publicDeriver: PublicDeriver<>): Promise { + const withPubKey = asGetPublicKey(publicDeriver); + if (withPubKey == null) { + throw new Error('unexpected missing asGetPublicKey result'); + } + return (await withPubKey.getPublicKey()).Hash; +} diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/byron.js b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/byron.js index e166436923..6c06b4b270 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/byron.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/byron.js @@ -217,7 +217,7 @@ export async function createStandardBip44Wallet(request: {| } return { deriverRequest: { - decryptPrivateDeriverPassword: request.password, + decryptPrivateDeriver: { preDerived: false, password: request.password }, publicDeriverMeta: { name: request.accountName, }, diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/shelley.js b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/shelley.js index 6da1605320..f8c9cdb6ff 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/shelley.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/walletBuilder/shelley.js @@ -23,7 +23,6 @@ import type { HWFeatures, } from '../../database/walletTypes/core/tables'; import { WalletBuilder } from './builder'; import { RustModule } from '../../../cardanoCrypto/rustLoader'; -import { encryptWithPassword } from '../../../../../../utils/passwordCipher'; import { Bip44DerivationLevels, @@ -153,8 +152,8 @@ export async function getAccountDefaultDerivations( export async function createStandardCip1852Wallet(request: {| db: lf$Database, - rootPk: RustModule.WalletV4.Bip32PrivateKey, - password: string, + encryptedRoot: string, + accountPublicKey: RustModule.WalletV4.Bip32PublicKey, accountIndex: number, walletName: string, accountName: string, @@ -164,24 +163,13 @@ export async function createStandardCip1852Wallet(request: {| throw new Error(`${nameof(createStandardCip1852Wallet)} needs hardened index`); } - const encryptedRoot = encryptWithPassword( - request.password, - request.rootPk.as_bytes(), - ); - - const accountPublicKey = request.rootPk - .derive(WalletTypePurpose.CIP1852) - .derive(CoinTypes.CARDANO) - .derive(request.accountIndex) - .to_public(); - if (request.network.BaseConfig[0].ChainNetworkId == null) { throw new Error(`${nameof(createStandardCip1852Wallet)} missing Byron network id`); } const initialDerivations = await getAccountDefaultDerivations( Number.parseInt(request.network.BaseConfig[0].ChainNetworkId, 10), - accountPublicKey, + request.accountPublicKey, rawGenAddByHash(new Set()), ); @@ -203,7 +191,7 @@ export async function createStandardCip1852Wallet(request: {| _finalState => ({ rootInsert: { privateKeyInfo: { - Hash: encryptedRoot, + Hash: request.encryptedRoot, IsEncrypted: true, PasswordLastUpdate: null, Type: KeyKind.BIP32ED25519, @@ -244,7 +232,10 @@ export async function createStandardCip1852Wallet(request: {| } return { deriverRequest: { - decryptPrivateDeriverPassword: request.password, + decryptPrivateDeriver: { + preDerived: true, + result: { pubKeyHex: request.accountPublicKey.to_hex() } + }, publicDeriverMeta: { name: request.accountName, }, diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/interfaces.js b/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/interfaces.js index c24351f6b3..23de72313c 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/interfaces.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/interfaces.js @@ -75,13 +75,15 @@ export type IDerivePublicFromPrivateRequest = {| publicDeriverMeta: {| name: string, |}, - decryptPrivateDeriverPassword: null | string, - /** - * void -> do not store key - * null -> store as unencrypted - * string -> store and encrypt - */ - encryptPublicDeriverPassword?: null | string, + decryptPrivateDeriver: {| + preDerived: false, + password: null | string + |} | {| + preDerived: true, + result: {| + pubKeyHex: string, + |}, + |}, initialDerivations: TreeInsert, path: Array<{| index: number, diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/traits.js b/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/traits.js index 899718810f..0588d2b8a7 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/traits.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/models/ConceptualWallet/traits.js @@ -39,8 +39,6 @@ import { Mixin, } from 'mixwith'; -import { encryptWithPassword } from '../../../../../../utils/passwordCipher'; - import { DerivePublicDeriverFromKey, AddAdhocPublicDeriver, } from '../../database/walletTypes/common/api/write'; @@ -56,7 +54,6 @@ import { import type { IChangePasswordRequest, IChangePasswordResponse, } from '../common/interfaces'; -import { hexToBytes } from '../../../../../../coreUtils'; // =========================== // DerivePublicFromPrivate @@ -77,11 +74,14 @@ export async function derivePublicDeriver( { publicDeriverMeta: body.publicDeriverMeta, pathToPublic: privateKeyRow => { - const pubDeriverKey = normalizeToPubDeriverLevel({ - privateKeyRow, - password: body.decryptPrivateDeriverPassword, - path: body.path.map(entry => entry.index), - }); + const pubDeriverKey = body.decryptPrivateDeriver.preDerived ? + body.decryptPrivateDeriver.result : + normalizeToPubDeriverLevel({ + privateKeyRow, + password: body.decryptPrivateDeriver.password, + path: body.path.map(entry => entry.index), + }); + return [ ...body.path.slice(0, body.path.length - 1).map(pathEntry => ({ index: pathEntry.index, @@ -98,19 +98,7 @@ export async function derivePublicDeriver( KeyDerivationId: insertRequest.keyDerivationId, ...body.path[body.path.length - 1].insert, }), - privateKey: body.encryptPublicDeriverPassword === undefined - ? null - : { - Hash: body.encryptPublicDeriverPassword === null - ? pubDeriverKey.prvKeyHex - : encryptWithPassword( - body.encryptPublicDeriverPassword, - hexToBytes(pubDeriverKey.prvKeyHex) - ), - IsEncrypted: true, - PasswordLastUpdate: null, - Type: privateKeyRow.Type, // type doesn't change with derivations - }, + privateKey: null, publicKey: { Hash: pubDeriverKey.pubKeyHex, IsEncrypted: false, diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/tests/index.test.js b/packages/yoroi-extension/app/api/ada/lib/storage/tests/index.test.js index 39d5c3fba4..6343005a99 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/tests/index.test.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/tests/index.test.js @@ -126,7 +126,7 @@ test('Can add and fetch address in wallet', async (done) => { insert: {}, }, ], - decryptPrivateDeriverPassword: privateDeriverPassword, + decryptPrivateDeriver: { preDerived: false, password: privateDeriverPassword }, initialDerivations: [ { index: 0, // external chain, diff --git a/packages/yoroi-extension/app/api/ada/lib/test-config.forTests.js b/packages/yoroi-extension/app/api/ada/lib/test-config.forTests.js index a32c7589f9..b1adda2090 100644 --- a/packages/yoroi-extension/app/api/ada/lib/test-config.forTests.js +++ b/packages/yoroi-extension/app/api/ada/lib/test-config.forTests.js @@ -21,6 +21,16 @@ const CONFIG: ConfigType = { pubKeyData: '', pubKeyMaster: '', }, + bring: { + baseUrl: '', + identifier: '', + apiEndpoint: '' + }, + bringSandbox: { + baseUrl: '', + identifier: '', + apiEndpoint: '' + }, }; global.CONFIG = CONFIG; diff --git a/packages/yoroi-extension/app/api/localStorage/index.js b/packages/yoroi-extension/app/api/localStorage/index.js index 0b27f0a342..10df67febb 100644 --- a/packages/yoroi-extension/app/api/localStorage/index.js +++ b/packages/yoroi-extension/app/api/localStorage/index.js @@ -27,15 +27,18 @@ const storageKeys = { COIN_PRICE_PUB_KEY_DATA: networkForLocalStorage + '-COIN-PRICE-PUB-KEY-DATA', EXTERNAL_STORAGE: networkForLocalStorage + '-EXTERNAL-STORAGE', TOGGLE_SIDEBAR: networkForLocalStorage + '-TOGGLE-SIDEBAR', - WALLETS_NAVIGATION: networkForLocalStorage + '-WALLETS-NAVIGATION', SUBMITTED_TRANSACTIONS: 'submittedTransactions', CATALYST_ROUND_INFO: networkForLocalStorage + '-CATALYST_ROUND_INFO', FLAGS: networkForLocalStorage + '-FLAGS', USER_THEME: networkForLocalStorage + '-USER-THEME', PORTFOLIO_FIAT_PAIR: networkForLocalStorage + '-PORTFOLIO_FIAT_PAIR', + BRING_SANDBOX: networkForLocalStorage + '-BRING_SANDBOX', + CURRENT_NETWORK_ID: networkForLocalStorage + '-CURRENT_NETWORK_ID', + WALLET_LIST_ORDER: networkForLocalStorage + '-WALLET_LIST_ORDER', + SELECTED_WALLET_PUBLIC_KEY: networkForLocalStorage + '_SELECTED_WALLET_PUBLIC_KEY', + // ========== CONNECTOR ========== // DAPP_CONNECTOR_WHITELIST: 'connector_whitelist', - SELECTED_WALLET: 'SELECTED_WALLET', IS_ANALYTICS_ALLOWED: networkForLocalStorage + '-IS_ANALYTICS_ALLOWED', ACCEPTED_TOS_VERSION: networkForLocalStorage + '-ACCEPTED_TOS_VERSION', @@ -43,6 +46,11 @@ const storageKeys = { // ========== LEGACY USED FOR MIGRATIONS ========== // CUSTOM_THEME: networkForLocalStorage + '-CUSTOM-THEME', THEME: networkForLocalStorage + '-THEME', + + CASHBACK_WALLET_ID: 'CASHBACK_WALLET_ID', + SHOWN_DISCLAIMERS: 'SHOWN_DISCLAIMERS', + WALLETS_NAVIGATION: networkForLocalStorage + '-WALLETS-NAVIGATION', + SELECTED_WALLET: 'SELECTED_WALLET', }; export type SetCustomUserThemeRequest = {| @@ -53,6 +61,8 @@ export type WalletsNavigation = {| cardano: number[], |}; +type Disclaimer = 'cashback' | 'buySellAda' | 'swap'; + /** * This api layer provides access to the electron local storage * for user settings that are not synced with any coin backend. @@ -121,17 +131,17 @@ export default class LocalStorageApi { setUserRevampAnnouncementStatus: boolean => Promise = status => setLocalItem(storageKeys.IS_REVAMP_THEME_ANNOUNCED, status.toString()); - // ========== Select Wallet ========== // + // ========== Legacy Select Wallet ========== // getSelectedWalletId: void => Promise = async () => { let id = await getLocalItem(storageKeys.SELECTED_WALLET); - // previously it was stored in window.localStorage, which is not accessible in the mv3 service worker if (!id) { - id = window?.localStorage.getItem(storageKeys.SELECTED_WALLET); - if (/^\d+$/.test(id)) { - await this.setSelectedWalletId(Number(id)); + id = window.localStorage?.getItem(storageKeys.SELECTED_WALLET); + if (!/^\d+$/.test(id)) { + id = null; } } + if (!id) { return null; } @@ -139,10 +149,15 @@ export default class LocalStorageApi { return Number(id); }; - setSelectedWalletId: number => Promise = async (id) => { - await setLocalItem(storageKeys.SELECTED_WALLET, id.toString()); + // ========== Selected Wallet ========== // + getSelectedWalletPublicKey: void => Promise = async () => { + return await getLocalItem(storageKeys.SELECTED_WALLET_PUBLIC_KEY); }; + setSelectedWalletPublicKey: string => Promise = async (publicKey) => { + await setLocalItem(storageKeys.SELECTED_WALLET_PUBLIC_KEY, publicKey); + } + // ========== Legacy Theme ========== // hasAnyLegacyThemeFlags: void => Promise = async () => { @@ -212,6 +227,19 @@ export default class LocalStorageApi { unsetToggleSidebar: void => Promise = () => removeLocalItem(storageKeys.TOGGLE_SIDEBAR); + // ========== Expand / retract Sidebar ========== // + + getBringSandbox: void => Promise = () => + getLocalItem(storageKeys.BRING_SANDBOX).then(s => s === 'true'); + + setBringSandbox: boolean => Promise = flag => { + return flag + ? setLocalItem(storageKeys.BRING_SANDBOX, 'true') + : this.unsetBringSandbox(); + }; + + unsetBringSandbox: void => Promise = () => removeLocalItem(storageKeys.BRING_SANDBOX); + // ============ External storage provider ============ // getExternalStorage: void => Promise = () => @@ -343,6 +371,60 @@ export default class LocalStorageApi { unsetIsAnalyticsAllowed: void => Promise = () => removeLocalItem(storageKeys.IS_ANALYTICS_ALLOWED); + saveCashbackWalletId: (number) => Promise = (id) => setLocalItem( + storageKeys.CASHBACK_WALLET_ID, + String(id) + ); + + getCashbackWalletId: () => Promise = async () => { + const v = await getLocalItem(storageKeys.CASHBACK_WALLET_ID); + if (!v) { + return null; + } + return Number(v); + } + + _getShownDisclaimerObject: () => Promise = async () => { + const raw = await getLocalItem(storageKeys.SHOWN_DISCLAIMERS); + const val = raw ? JSON.parse(raw) : {}; + return val; + } + + setShownDisclaimer: (Disclaimer) => Promise = async (which) => { + const val = await this._getShownDisclaimerObject(); + val[which] = true; + await setLocalItem(storageKeys.SHOWN_DISCLAIMERS, JSON.stringify(val)); + }; + + isDisclaimerShown: (Disclaimer) => Promise = async (which) => { + const val = await this._getShownDisclaimerObject(); + return val[which] === true; + } + + loadCurrentNetworkId: () => Promise = async () => { + const raw = await getLocalItem(storageKeys.CURRENT_NETWORK_ID); + if (raw == null) { + return undefined; + } + return Number(raw); + } + + saveCurrentNetworkId: (number) => Promise = async (networkId) => { + await setLocalItem(storageKeys.CURRENT_NETWORK_ID, String(networkId)); + } + + loadWalletListOrder: () => Promise> = async () => { + const raw = await getLocalItem(storageKeys.WALLET_LIST_ORDER); + if (raw == null) { + return []; + } + return JSON.parse(raw); + } + + saveWalletListOrder: Array => Promise = async (publicKeyList) => { + await setLocalItem(storageKeys.WALLET_LIST_ORDER, JSON.stringify(publicKeyList)); + } + async reset(): Promise { await this.unsetUserLocale(); await this.unsetComplexityLevel(); @@ -355,6 +437,7 @@ export default class LocalStorageApi { await this.unsetAcceptedTosVersion(); await this.unsetIsAnalyticsAllowed(); await this.unsetPortfolioFiatPair(); + await this.unsetBringSandbox(); } getItem: string => Promise = key => getLocalItem(key); diff --git a/packages/yoroi-extension/app/api/thunk.js b/packages/yoroi-extension/app/api/thunk.js index aad58fddd0..c6fab20ae4 100644 --- a/packages/yoroi-extension/app/api/thunk.js +++ b/packages/yoroi-extension/app/api/thunk.js @@ -154,8 +154,8 @@ function patchWalletState(walletState: Object): WalletState { return walletState; } -export async function getWallets(walletId?: number): Promise> { - const resp = await callBackground({ type: GetWallets.typeTag, request: { walletId } }); +export async function getWallets(networkId: number): Promise> { + const resp = await callBackground({ type: GetWallets.typeTag, request: { networkId } }); if (resp.error) { console.error('error when loading wallets:', resp.error); throw new Error(`error when loading wallets: ${resp.error}`); @@ -412,7 +412,12 @@ export const getProtocolParameters: GetProtocolParametersType = async ( ) => { return await callBackground({ type: GetProtocolParameters.typeTag, request: { networkId } }); } - + + +export function setCashbackWallet(id: number): void { + chrome.runtime.sendMessage({ type: 'bring_rpc_request', function: 'set-cashback-wallet', params: id }); +} + // Background -> UI notifications: const callbacks = Object.freeze({ walletStateUpdate: [], diff --git a/packages/yoroi-extension/app/assets/images/sidebar/cashback.inline.svg b/packages/yoroi-extension/app/assets/images/sidebar/cashback.inline.svg new file mode 100644 index 0000000000..67c836e086 --- /dev/null +++ b/packages/yoroi-extension/app/assets/images/sidebar/cashback.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js index 3bc08499cc..1d06d7fa88 100644 --- a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js +++ b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js @@ -59,19 +59,11 @@ const messages = defineMessages({ id: 'buysell.dialog.sellProviderFee', defaultMessage: '!!!2.5% fee', }, - disclaimer: { - id: 'buysell.dialog.disclaimer', - defaultMessage: '!!!Disclaimer', - }, disclaimerText: { id: 'buysell.dialog.disclaimerText', defaultMessage: '!!!Yoroi Wallet utilizes third-party web3 on-and-off ramp solutions for direct Fiat-ADA exchanges. By clicking "Proceed," you acknowledge that you will be redirected to our partner\'s website, where you may need to accept their terms and conditions. Please note, the third party web3 solution may have limitations based on your location and financial institution.', }, - proceed: { - id: 'buysell.dialog.proceed', - defaultMessage: 'PROCEED', - }, urlGenerationErrorDialogTitle: { id: 'buysell.dialog.error.dialog.title', defaultMessage: '!!!url generation', @@ -426,7 +418,7 @@ export default class BuySellDialog extends Component { - {intl.formatMessage(messages.disclaimer)} + {intl.formatMessage(globalMessages.disclaimer)} {intl.formatMessage(messages.disclaimerText)} @@ -494,7 +486,7 @@ export default class BuySellDialog extends Component { forceBottomDivider dialogActions={[ { - label: intl.formatMessage(messages.proceed), + label: intl.formatMessage(globalMessages.proceed), primary: true, disabled: state.amountAda === '' || state.inputError !== null, onClick: this.onSubmit, diff --git a/packages/yoroi-extension/app/components/settings/categories/general-setting/AboutYoroiSettingsBlock.js b/packages/yoroi-extension/app/components/settings/categories/general-setting/AboutYoroiSettingsBlock.js index 5e1be3172d..8ae8b62f8a 100644 --- a/packages/yoroi-extension/app/components/settings/categories/general-setting/AboutYoroiSettingsBlock.js +++ b/packages/yoroi-extension/app/components/settings/categories/general-setting/AboutYoroiSettingsBlock.js @@ -18,7 +18,7 @@ import { ReactComponent as mediumSvg } from '../../../../assets/images/social/me import environment from '../../../../environment'; import LinkButton from '../../../widgets/LinkButton'; import { handleExternalLinkClick } from '../../../../utils/routing'; -import { Box, Link, Typography } from '@mui/material'; +import { Box, Button, Link, Typography } from '@mui/material'; const messages = defineMessages({ aboutYoroiLabel: { @@ -77,6 +77,10 @@ const messages = defineMessages({ id: 'settings.general.aboutYoroi.git.branch', defaultMessage: '!!!Branch:', }, + switchNetwork: { + id: 'settings.general.aboutYoroi.switchNetwork', + defaultMessage: '!!!SWITCH NETWORK', + }, }); const basePageComponentPath = 'settings:general'; @@ -130,7 +134,8 @@ const socialMediaLinks = [ const baseGithubUrl = 'https://github.com/Emurgo/yoroi-frontend/'; type Props = {| - wallet: null | { isTestnet: boolean, ... } + wallet: null | { isTestnet: boolean, ... }, + onSwitchNetwork: () => void, |}; @observer @@ -186,6 +191,13 @@ export default class AboutYoroiSettingsBlock extends Component { /> )} + +
{socialMediaLinks.map(link => ( diff --git a/packages/yoroi-extension/app/components/settings/categories/general-setting/BringCashbackSettings.js b/packages/yoroi-extension/app/components/settings/categories/general-setting/BringCashbackSettings.js new file mode 100644 index 0000000000..da7d7a21bb --- /dev/null +++ b/packages/yoroi-extension/app/components/settings/categories/general-setting/BringCashbackSettings.js @@ -0,0 +1,200 @@ +// @flow +import { Component } from 'react'; +import type { Node, ComponentType } from 'react'; +import { observer } from 'mobx-react'; +import classNames from 'classnames'; +import Select from '../../../common/Select'; +import { MenuItem, Typography, Stack } from '@mui/material'; +import { Box } from '@mui/system'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import ReactToolboxMobxForm from '../../../../utils/ReactToolboxMobxForm'; +import LocalizableError from '../../../../i18n/LocalizableError'; +import styles from './UnitOfAccountSettings.scss'; +import Dialog from '../../../widgets/Dialog'; +import VerticalFlexContainer from '../../../layout/VerticalFlexContainer'; +import LoadingSpinner from '../../../widgets/LoadingSpinner'; +import globalMessages from '../../../../i18n/global-messages'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import WalletAccountIcon from '../../../topbar/WalletAccountIcon'; +import type { WalletChecksum } from '@emurgo/cip4-js'; +import { RevampSwitch } from '../../../widgets/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; + +const messages = defineMessages({ + bringCashbackTitle: { + id: 'settings.cashback.title', + defaultMessage: '!!!Cashback rewards wallet', + }, + note: { + id: 'settings.cashback.note', + defaultMessage: + '!!!Your connected wallet is the designated wallet for receiving ADA cashback rewards and applied to all partner websites. You can switch to a different wallet anytime to ensure your cashback is directed to your preferred wallet or select “None” to decline this service.', + }, + label: { + id: 'settings.cashback.label', + defaultMessage: '!!!Connected Wallet', + }, +}); + +type Props = {| + +onSelect: number => Promise, + +isSubmitting: boolean, + +cardanoWallets: Array<{ publicDeriverId: number, name: string, plate: WalletChecksum, isTestnet: boolean, ... }>, + +currentValue: ?number, + +error?: ?LocalizableError, + +isUseSandbox: ?boolean, + +onSetUseSandbox: null | (boolean) => *, +|}; + +@observer +class BringCashbackSettings extends Component { + static defaultProps = { + error: undefined, + }; + + static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { + intl: intlShape.isRequired, + }; + + form: ReactToolboxMobxForm = new ReactToolboxMobxForm({ + fields: { + cashbackWalletId: { + label: this.context.intl.formatMessage(messages.label), + }, + }, + }); + + render(): Node { + const { cardanoWallets, error, currentValue, onSetUseSandbox } = this.props; + const { intl } = this.context; + const { form } = this; + const cashbackWalletId = form.$('cashbackWalletId'); + const componentClassNames = classNames([styles.component, 'currency']); + + const optionRenderer = option => { + return ( + + + + + + {option.plate.TextPart} | {option.name} + + + + + ); + }; + + const dialog = this.props.isSubmitting ? ( + + + + + + ) : null; + + const mainnetWallets = + cardanoWallets.filter(({ isTestnet }) => !isTestnet); + + return ( + + {dialog} + + {intl.formatMessage(messages.bringCashbackTitle)} + + + + + + + + + {onSetUseSandbox != null ? ( + + onSetUseSandbox(event.target.checked)} + /> + + } + labelPlacement="start" + sx={{ marginLeft: '0px', marginTop: '40px' }} + /> + ) : null} + + ); + } +} + +export default (BringCashbackSettings: ComponentType); + +const WalletIcon = ({ imagePart }: {| imagePart: string |}) => { + return ( + + + + ); +}; diff --git a/packages/yoroi-extension/app/components/topbar/WalletListDialog.js b/packages/yoroi-extension/app/components/topbar/WalletListDialog.js index 71c793d204..53c0b95409 100644 --- a/packages/yoroi-extension/app/components/topbar/WalletListDialog.js +++ b/packages/yoroi-extension/app/components/topbar/WalletListDialog.js @@ -4,7 +4,6 @@ import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import type { TokenLookupKey } from '../../api/common/lib/MultiToken'; import type { TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import type { UnitOfAccountSettingType } from '../../types/unitOfAccountType'; -import type { WalletsNavigation } from '../../api/localStorage'; import { BigNumber } from 'bignumber.js'; import { Component } from 'react'; import { observer } from 'mobx-react'; @@ -67,67 +66,25 @@ type Props = {| +unitOfAccountSetting: UnitOfAccountSettingType, +getCurrentPrice: (from: string, to: string) => ?string, +cardanoWallets: Array, - +walletsNavigation: WalletsNavigation, - +updateSortedWalletList: WalletsNavigation => Promise, + +onUpdateWalletListOrder: (from: number, to: number) => Promise, +onSelect: number => void, +selectedWalletId: ?number, |}; type State = {| - cardanoWalletsIdx: number[], selectedWalletId: number | null, |}; -const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - return result; -}; - -const getGeneratedWalletIds = (sortedWalletListIdx, currentWalletIdx) => { - let generatedWalletIds; - if (sortedWalletListIdx !== undefined && sortedWalletListIdx.length > 0) { - const newWalletIds = currentWalletIdx.filter(id => { - const index = sortedWalletListIdx.indexOf(id); - if (index === -1) { - return true; - } - return false; - }); - generatedWalletIds = [...sortedWalletListIdx, ...newWalletIds]; - } else { - generatedWalletIds = currentWalletIdx; - } - - return generatedWalletIds; -}; @observer export default class WalletListDialog extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired, }; state: State = { - cardanoWalletsIdx: [], selectedWalletId: null, }; async componentDidMount(): Promise { - const cardanoWalletsId = getGeneratedWalletIds( - this.props.walletsNavigation.cardano, - this.props.cardanoWallets.map(wallet => wallet.walletId) - ); - - this.setState( - { - cardanoWalletsIdx: cardanoWalletsId, - selectedWalletId: this.props.selectedWalletId, - }, - async () => { - await this.props.updateSortedWalletList({ - cardano: cardanoWalletsId, - }); - } - ); + this.setState({ selectedWalletId: this.props.selectedWalletId }); } onDragEnd: (network: 'cardano', result: Object) => any = async (network, result) => { @@ -135,20 +92,7 @@ export default class WalletListDialog extends Component { if (!destination || destination.index === source.index) { return; } - - this.setState( - prev => { - const walletListIdx = reorder(prev.cardanoWalletsIdx, result.source.index, result.destination.index); - return { - cardanoWalletsIdx: walletListIdx, - }; - }, - async function () { - await this.props.updateSortedWalletList({ - cardano: this.state.cardanoWalletsIdx, - }); - } - ); + this.props.onUpdateWalletListOrder(source.index, destination.index); }; onSelect: void => void = () => { @@ -165,7 +109,6 @@ export default class WalletListDialog extends Component { render(): Node { const { intl } = this.context; - const { cardanoWalletsIdx } = this.state; const { shouldHideBalance, @@ -239,35 +182,25 @@ export default class WalletListDialog extends Component { {provided => (
- {cardanoWalletsIdx.length > 0 && - cardanoWalletsIdx - .map((walletId, idx) => { - const wallet = cardanoWallets.find(w => w.walletId === walletId); - if (!wallet) { - return null; - } - - return ( - this.setState({ selectedWalletId: wallet.walletId })} - isCurrentWallet={this.isCurrentWallet(wallet.walletId, 'local')} - plate={wallet.plate} - type={wallet.type} - name={wallet.name} - rewards={wallet.rewards} - shouldHideBalance={this.props.shouldHideBalance} - walletAmount={wallet.amount} - walletId={walletId} - getTokenInfo={this.props.getTokenInfo} - unitOfAccountSetting={unitOfAccountSetting} - getCurrentPrice={getCurrentPrice} - id="changeWalletDialog:walletsList" - /> - ); - }) - .filter(Boolean)} + {cardanoWallets.map((wallet, idx) => ( + this.setState({ selectedWalletId: wallet.walletId })} + isCurrentWallet={this.isCurrentWallet(wallet.walletId, 'local')} + plate={wallet.plate} + type={wallet.type} + name={wallet.name} + rewards={wallet.rewards} + shouldHideBalance={this.props.shouldHideBalance} + walletAmount={wallet.amount} + walletId={wallet.walletId} + getTokenInfo={this.props.getTokenInfo} + unitOfAccountSetting={unitOfAccountSetting} + getCurrentPrice={getCurrentPrice} + id="changeWalletDialog:walletsList" + /> + ))} {provided.placeholder}
)} diff --git a/packages/yoroi-extension/app/components/wallet/create-wallet/CreateWalletPage.js b/packages/yoroi-extension/app/components/wallet/create-wallet/CreateWalletPage.js index 9b4412efd2..f95181fc22 100644 --- a/packages/yoroi-extension/app/components/wallet/create-wallet/CreateWalletPage.js +++ b/packages/yoroi-extension/app/components/wallet/create-wallet/CreateWalletPage.js @@ -5,14 +5,11 @@ import { Box } from '@mui/material'; import { observer } from 'mobx-react'; import CreateWalletSteps from './CreateWalletSteps'; import LearnAboutRecoveryPhrase from './LearnAboutRecoveryPhrase'; -import { CREATE_WALLET_SETPS, getFirstStep, markDialogAsShown } from './steps'; +import { CREATE_WALLET_SETPS, markDialogAsShown } from './steps'; import SaveRecoveryPhraseStep from './SaveRecoveryPhraseStep'; import VerifyRecoveryPhraseStep from './VerifyRecoveryPhraseStep'; import AddWalletDetailsStep from './AddWalletDetailsStep'; -import { networks } from '../../../api/ada/lib/storage/database/prepackaged/networks'; import CreateWalletPageHeader from './CreateWalletPageHeader'; -import SelectNetworkStep from './SelectNetworkStep'; -import environment from '../../../environment'; import { ROUTES } from '../../../routes-config'; import type { NetworkRow } from '../../../api/ada/lib/storage/database/primitives/tables'; import { ampli } from '../../../../ampli/index'; @@ -25,7 +22,6 @@ type Props = {| recoveryPhrase: Array, |}) => void, selectedNetwork: $ReadOnly, - setSelectedNetwork: (params: void | $ReadOnly) => void, openDialog(dialog: any): void, closeDialog(): void, isDialogOpen(dialog: any): boolean, @@ -42,14 +38,13 @@ function CreateWalletPage(props: Props): Node { const { genWalletRecoveryPhrase, createWallet, - setSelectedNetwork, selectedNetwork, isDialogOpen, openDialog, closeDialog, goToRoute, } = props; - const [currentStep, setCurrentStep] = useState(getFirstStep()); + const [currentStep, setCurrentStep] = useState(CREATE_WALLET_SETPS.LEARN_ABOUT_RECOVERY_PHRASE); const setCurrentStepAndTrack = (step) => { setCurrentStep(step); if (step === CREATE_WALLET_SETPS.LEARN_ABOUT_RECOVERY_PHRASE) { @@ -76,20 +71,9 @@ function CreateWalletPage(props: Props): Node { }; const steps = { - [CREATE_WALLET_SETPS.SELECT_NETWORK]: ( - { - setSelectedNetwork(network); - setCurrentStepAndTrack(CREATE_WALLET_SETPS.LEARN_ABOUT_RECOVERY_PHRASE); - }} - goBack={() => goToRoute(ROUTES.WALLETS.ADD)} - /> - ), [CREATE_WALLET_SETPS.LEARN_ABOUT_RECOVERY_PHRASE]: ( { - if (!environment.isDev() && !environment.isNightly()) - setSelectedNetwork(networks.CardanoMainnet); setCurrentStepAndTrack(CREATE_WALLET_SETPS.SAVE_RECOVERY_PHRASE); if (recoveryPhrase === null) { genWalletRecoveryPhrase() @@ -101,12 +85,7 @@ function CreateWalletPage(props: Props): Node { }); } }} - prevStep={() => { - if (environment.isProduction()) { - return goToRoute(ROUTES.WALLETS.ADD); - } - setCurrentStep(CREATE_WALLET_SETPS.SELECT_NETWORK); - }} + prevStep={() => goToRoute(ROUTES.WALLETS.ADD)} {...manageDialogsProps} /> ), @@ -142,9 +121,6 @@ function CreateWalletPage(props: Props): Node { prevStep={() => setCurrentStep(CREATE_WALLET_SETPS.VERIFY_RECOVERY_PHRASE)} onSubmit={(walletName: string, walletPassword: string) => { if (!recoveryPhrase) throw new Error('Recovery phrase must be generated first'); - if (!environment.isDev() && !environment.isNightly()) { - setSelectedNetwork(networks.CardanoMainnet); - } if (!selectedNetwork) throw new Error('Network must be selected to create a wallet. Should never happen'); @@ -163,7 +139,6 @@ function CreateWalletPage(props: Props): Node { }; const CurrentStep = steps[currentStep]; - if (currentStep === CREATE_WALLET_SETPS.SELECT_NETWORK) return CurrentStep; return ( diff --git a/packages/yoroi-extension/app/components/wallet/create-wallet/steps.js b/packages/yoroi-extension/app/components/wallet/create-wallet/steps.js index 3a9ee15cc2..b8f5ba50ee 100644 --- a/packages/yoroi-extension/app/components/wallet/create-wallet/steps.js +++ b/packages/yoroi-extension/app/components/wallet/create-wallet/steps.js @@ -1,12 +1,9 @@ // @flow -import environment from '../../../environment'; - export const CREATE_WALLET_SETPS = Object.freeze({ LEARN_ABOUT_RECOVERY_PHRASE: 'LEARN_ABOUT_RECOVER_PHRASE', SAVE_RECOVERY_PHRASE: 'SAVE_RECOVERY_PHRASE', VERIFY_RECOVERY_PHRASE: 'VERIFY_RECOVERY_PHRASE', ADD_WALLET_DETAILS: 'ADD_WALLET_DETAILS', - SELECT_NETWORK: 'SELECT_NETWORK', }); export const TIPS_DIALOGS = Object.freeze({ @@ -16,14 +13,6 @@ export const TIPS_DIALOGS = Object.freeze({ WALLET_CHECKSUM: 'WALLET_CHECKSUM', }); -export function getFirstStep(): string { - if (environment.isDev() || environment.isNightly()) { - return CREATE_WALLET_SETPS.SELECT_NETWORK; - } - - return CREATE_WALLET_SETPS.LEARN_ABOUT_RECOVERY_PHRASE; -} - const asDialogId: string => string = (dialogId: string) => `dialog__${dialogId}`; export function markDialogAsShown(dialogId: string): void { diff --git a/packages/yoroi-extension/app/components/wallet/restore/RestoreWalletPage.js b/packages/yoroi-extension/app/components/wallet/restore/RestoreWalletPage.js index e5bc417ae8..075d5d305e 100644 --- a/packages/yoroi-extension/app/components/wallet/restore/RestoreWalletPage.js +++ b/packages/yoroi-extension/app/components/wallet/restore/RestoreWalletPage.js @@ -5,17 +5,14 @@ import { useState } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { Box, Typography, styled } from '@mui/material'; import { observer } from 'mobx-react'; -import { RESTORE_WALLET_STEPS, getFirstRestorationStep } from './steps'; +import { RESTORE_WALLET_STEPS } from './steps'; import { ReactComponent as YoroiLogo } from '../../../assets/images/yoroi-logo-shape-blue.inline.svg'; import SelectWalletTypeStep from './steps/type/SelectWalletTypeStep'; import Stepper from '../../common/stepper/Stepper'; import EnterRecoveryPhraseStep from './steps/phrase/EnterRecoveryPhraseStep'; import AddWalletDetailsStep from '../create-wallet/AddWalletDetailsStep'; -import { networks } from '../../../api/ada/lib/storage/database/prepackaged/networks'; import { markDialogAsShown } from '../dialogs/utils'; import { ROUTES } from '../../../routes-config'; -import SelectNetworkStep from '../create-wallet/SelectNetworkStep'; -import environment from '../../../environment'; import { useRestoreWallet } from './hooks'; import { ampli } from '../../../../ampli/index'; import { runInAction } from 'mobx'; @@ -80,7 +77,7 @@ function RestoreWalletPage(props: Props & Intl): Node { tokenInfoStore, } = stores; - const [currentStep, setCurrentStep] = useState(getFirstRestorationStep()); + const [currentStep, setCurrentStep] = useState(RESTORE_WALLET_STEPS.SELECT_WALLET_TYPE); const [selectedRestoreMode, setSelectedRestoreMode] = useState(null); const { recoveryPhrase, duplicatedWallet, setRestoreWalletData, resetRestoreWalletData } = useRestoreWallet(); @@ -115,20 +112,6 @@ function RestoreWalletPage(props: Props & Intl): Node { } const steps = { - [RESTORE_WALLET_STEPS.SELECT_NETWORK]: { - stepId: RESTORE_WALLET_STEPS.SELECT_NETWORK, - message: messages.firstStep, - component: ( - { - stores.profile.setSelectedNetwork(network); - setCurrentStep(RESTORE_WALLET_STEPS.SELECT_WALLET_TYPE); - ampli.restoreWalletTypeStepViewed(); - }} - goBack={goToAddWalletScreen} - /> - ), - }, [RESTORE_WALLET_STEPS.SELECT_WALLET_TYPE]: { stepId: RESTORE_WALLET_STEPS.SELECT_WALLET_TYPE, message: messages.firstStep, @@ -136,8 +119,6 @@ function RestoreWalletPage(props: Props & Intl): Node { { resetRestoreWalletData(); - if (!environment.isDev() && !environment.isNightly()) - stores.profile.setSelectedNetwork(networks.CardanoMainnet); runInAction(() => { setSelectedRestoreMode(mode); setCurrentStep(RESTORE_WALLET_STEPS.ENTER_RECOVERY_PHRASE); @@ -148,8 +129,7 @@ function RestoreWalletPage(props: Props & Intl): Node { }} goBack={() => { resetRestoreWalletData(); - if (!environment.isDev() && !environment.isNightly()) goToAddWalletScreen(); - else setCurrentStep(RESTORE_WALLET_STEPS.SELECT_NETWORK); + goToAddWalletScreen(); }} /> ), @@ -225,12 +205,9 @@ function RestoreWalletPage(props: Props & Intl): Node { const stepperSteps = Object.keys(steps) .map(key => ({ stepId: steps[key].stepId, message: steps[key].message })) - .filter(step => step.stepId !== RESTORE_WALLET_STEPS.SELECT_NETWORK); const CurrentStep = steps[currentStep].component; - if (currentStep === RESTORE_WALLET_STEPS.SELECT_NETWORK) return CurrentStep; - return ( ({ + fontSize: '16px', + fontWeight: 400, + lineHeight: '24px', +})); + +type Props = {| + +onCancel: void => void, + +networks: Array<{| + id: number, + name: $npm$ReactIntl$MessageDescriptor, + |}>, + +onApply: (number) => Promise, + +currentNetworkId: number, +|}; + +@observer +export default class Switch extends Component { + static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { + intl: intlShape.isRequired, + }; + + form: ReactToolboxMobxForm = new ReactToolboxMobxForm({ + fields: { + selectedNetwork: { + label: this.context.intl.formatMessage(messages.selectLabel), + value: this.props.currentNetworkId, + }, + }, + }); + + render(): Node { + const { intl } = this.context; + const { onCancel, onApply, networks } = this.props; + + return ( + } + id="switchNetworkDialog" + dialogActions={[ + { + label: intl.formatMessage(globalMessages.cancel), + onClick: onCancel, + primary: false, + }, + { + label: intl.formatMessage(messages.applyButton), + onClick: () => onApply(this.form.$('selectedNetwork').value), + primary: true, + }, + ]} + > + + {intl.formatMessage(messages.dialogText)} + + + + ); + } +} diff --git a/packages/yoroi-extension/app/components/widgets/DisclaimerDialog.js b/packages/yoroi-extension/app/components/widgets/DisclaimerDialog.js new file mode 100644 index 0000000000..2023ec928c --- /dev/null +++ b/packages/yoroi-extension/app/components/widgets/DisclaimerDialog.js @@ -0,0 +1,94 @@ +// @flow +import { useState } from 'react'; +import { injectIntl, defineMessages, type $npm$ReactIntl$IntlShape } from 'react-intl'; +import Dialog from './Dialog'; +import globalMessages from '../../i18n/global-messages'; +import { Typography, Checkbox, FormControlLabel } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const messages = defineMessages({ + disclaimer: { + id: 'cashback.disclaimer', + defaultMessage: '!!!By clicking "Proceed," you acknowledge that you will be redirected to a third-party service provider offering cashback services in ADA currency. You may be required to agree to the terms, conditions, and privacy policies of the third-party provider to complete the transaction. Yoroi Wallet does not control, endorse, or assume responsibility for the content, security, policies, or services provided by the third party.', + }, + disclaimerNote: { + id: 'cashback.disclaimer.note', + defaultMessage: '!!!Please note:', + }, + disclaimerNote1: { + id: 'cashback.disclaimer.note.1', + defaultMessage: '!!!1. Yoroi Wallet is not liable for any losses, delays, or errors that may occur while using the third-party service.', + }, + disclaimerNote2: { + id: 'cashback.disclaimer.note.2', + defaultMessage: '!!!2. Transactions may be subject to restrictions based on your geographic location, applicable laws, financial institution policies, or the service provider\'s limitations.', + }, + disclaimerNote3: { + id: 'cashback.disclaimer.note.3', + defaultMessage: '!!!3. Ensure you review and understand the third party\'s terms, as your interactions are solely governed by their agreements.', + }, + disclaimerNote4: { + id: 'cashback.disclaimer.note.4', + defaultMessage: '!!!4. Yoroi Wallet does not collect or store any personal or financial data submitted through the third-party platform.', + }, + disclaimerAgree: { + id: 'cashback.disclaimer.agree', + defaultMessage: '!!!I understand this disclaimer.', + }, +}); + +type Props = {| + onProceed: () => void, +|}; +type Intl = {| + intl: $npm$ReactIntl$IntlShape +|}; + +const DisclaimerText = styled(Typography)(() => ({ + fontSize: '16px', + lineHeight: '24px', +})); +const DisclaimerBold = styled(Typography)(() => ({ + fontSize: '16px', + lineHeight: '24px', + fontWeight: 500, +})); + +const DisclaimerDialog: React$ComponentType = injectIntl((props: Props & Intl) => { + const { intl } = props; + const [disclaimerAgreed, setDisclaimerAgreed] = useState(false); + return ( + + {intl.formatMessage(messages.disclaimer)} +   + {intl.formatMessage(messages.disclaimerNote)} + {intl.formatMessage(messages.disclaimerNote1)} + {intl.formatMessage(messages.disclaimerNote2)} + {intl.formatMessage(messages.disclaimerNote3)} + {intl.formatMessage(messages.disclaimerNote4)} +   + setDisclaimerAgreed(event.target.checked)} + /> + } + label={intl.formatMessage(messages.disclaimerAgree)} + /> + + ); +}); + +export default DisclaimerDialog; diff --git a/packages/yoroi-extension/app/connector/App.js b/packages/yoroi-extension/app/connector/App.js index 6994d4ca6a..1d47137bc4 100644 --- a/packages/yoroi-extension/app/connector/App.js +++ b/packages/yoroi-extension/app/connector/App.js @@ -124,7 +124,11 @@ class App extends Component { if (this.state.crashed === true) { return ; } - return {Routes(stores)}; + return ( + + + + ); }; } diff --git a/packages/yoroi-extension/app/connector/Routes.js b/packages/yoroi-extension/app/connector/Routes.js index 84a97b2105..19d7bbdbf0 100644 --- a/packages/yoroi-extension/app/connector/Routes.js +++ b/packages/yoroi-extension/app/connector/Routes.js @@ -1,21 +1,40 @@ // @flow import type { Node } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route, Switch, useLocation } from 'react-router-dom'; import type { StoresMap } from './stores/index'; import { ROUTES } from './routes-config'; +import Helmet from 'react-helmet'; +import { injectIntl } from 'react-intl'; +import type { $npm$ReactIntl$IntlShape } from 'react-intl'; +import { observer } from 'mobx-react'; // PAGES import ConnectContainer from './containers/ConnectContainer'; -import Layout from './components/layout/Layout'; +import Layout, { messages } from './components/layout/Layout'; import SignTxContainer from './containers/SignTxContainer'; import LoadingPage from '../containers/LoadingPage'; +import SelectCashbackWalletContainer from './containers/SelectCashbackWalletContainer'; -export const Routes = (stores: StoresMap): Node => { - if (stores.loading.isLoading) { - return ; - } - return wrapPages(getContent(stores)); -}; +type Props = {| stores: StoresMap |}; +type Intl = {| intl: $npm$ReactIntl$IntlShape |}; +export const Routes: React$ComponentType = injectIntl(observer((props: Props & Intl) => { + const { stores, intl } = props; + const title = intl.formatMessage( + useLocation().pathname === ROUTES.SELECT_CASHBACK_WALLET ? + messages.yoroiConnector : messages.yoroiDappConnector + ); + + return ( + <> + Yoroi Connector + {stores.loading.isLoading ? ( + + ) : ( + wrapPages(getContent(stores)) + )} + + ); +})); const getContent = (stores) => ( @@ -29,6 +48,11 @@ const getContent = (stores) => ( path={ROUTES.SIGNIN_TRANSACTION} component={props => } /> + } + /> ); diff --git a/packages/yoroi-extension/app/connector/components/connect/ConnectPage.js b/packages/yoroi-extension/app/connector/components/connect/ConnectPage.js index 090dd697e0..774291477a 100644 --- a/packages/yoroi-extension/app/connector/components/connect/ConnectPage.js +++ b/packages/yoroi-extension/app/connector/components/connect/ConnectPage.js @@ -73,6 +73,18 @@ const messages = defineMessages({ id: 'connector.connect.hardwareWalletsConnectWithAuthNotSupported', defaultMessage: '!!!Connecting to hardware wallet with authentication is not supported', }, + cashbackApplyAll: { + id: 'connector.connect.cashback.apply.all', + defaultMessage: '!!!The wallet you select will be applied for all partner websites.' + }, + cashbackDisabledTrezor: { + id: 'connector.connect.cashback.trezor.disabled', + defaultMessage: '!!!Cashback service doesn’t support Trezor wallet connection', + }, + addWallet: { + id: 'connector.connect.cashback.addWallet', + defaultMessage: '!!!Add wallet', + }, }); type Props = {| @@ -96,6 +108,7 @@ type Props = {| +unitOfAccount: UnitOfAccountSettingType, +getCurrentPrice: (from: string, to: string) => ?string, +onUpdateHideBalance: void => Promise, + +isSelectingCashbackWallet?: boolean, |}; @observer @@ -184,6 +197,7 @@ export default class ConnectPage extends Component { isAppAuth, onUpdateHideBalance, selectedWallet, + isSelectingCashbackWallet = false, } = this.props; const isNightly = environment.isNightly(); @@ -255,7 +269,7 @@ export default class ConnectPage extends Component {
{hasWallets ? ( <> - + {!isSelectingCashbackWallet && ()} { > {intl.formatMessage(messages.connectWallet)} -
-
- {faviconUrl != null && faviconUrl !== '' ? {`${url} : } -
- - - {intl.formatMessage(messages.subtitle)}{' '} - - {url} + {!isSelectingCashbackWallet && ( +
+
+ {faviconUrl != null && faviconUrl !== '' ? {`${url} : } +
+ + + {intl.formatMessage(messages.subtitle)}{' '} + + {url} + - - -
+
+
+ )} ) : null} @@ -305,47 +321,87 @@ export default class ConnectPage extends Component {
+ {isSelectingCashbackWallet && ( +
{intl.formatMessage(messages.cashbackApplyAll)}
+ )} +
    - {publicDerivers.map((wallet, idx) => ( -
  • - onSelectWallet(wallet, wallet.plate)}> - - - - } - /> - -
  • - ))} + {publicDerivers.map((wallet, idx) => { + const isTrezor = isSelectingCashbackWallet && wallet.type === 'trezor'; + const Btn = isTrezor ? DisabledWalletButton : WalletButton; + return ( +
  • + onSelectWallet(wallet, wallet.plate)}> + + + + } + /> + +
  • + ); + })}
) : null} )} - {hasWallets && !isAppAuth ? ( + {!isSelectingCashbackWallet && hasWallets && !isAppAuth ? (
{intl.formatMessage(messages.connectInfo)}
{intl.formatMessage(connectorMessages.messageReadOnly)}
) : null} + {isSelectingCashbackWallet && ( + + + + + )} ); } @@ -357,3 +413,6 @@ const WalletButton = styled('button')({ fontSize: '1rem', padding: '16px', }); +const DisabledWalletButton = styled(WalletButton)({ + cursor: 'default', +}); diff --git a/packages/yoroi-extension/app/connector/components/connect/ConnectPage.scss b/packages/yoroi-extension/app/connector/components/connect/ConnectPage.scss index 043799529d..a8703b2b00 100644 --- a/packages/yoroi-extension/app/connector/components/connect/ConnectPage.scss +++ b/packages/yoroi-extension/app/connector/components/connect/ConnectPage.scss @@ -92,6 +92,13 @@ } } +.cashbackApplyAll { + margin-left: 16px; + font-weight: 400; + font-size: 14px; + line-height: 16px; +} + .list { & .listItem { border: 1px solid #fff; @@ -105,12 +112,12 @@ width: 100%; color: var(--yoroi-palette-gray-900); } - + } + & .enabledWallet { &:hover { border-color: #c4cad7; } } - overflow: auto; } diff --git a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.js b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.js index 7ce9265b7c..36771356b6 100644 --- a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.js +++ b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.js @@ -10,6 +10,7 @@ import type { WalletState } from '../../../../chrome/extension/background/types' type Props = {| +publicDeriver: WalletState, +walletBalance?: Node, + +disabledForReason?: ?string, |}; function constructPlate( @@ -35,7 +36,7 @@ export default class WalletCard extends Component { }; render(): Node { - const { publicDeriver, walletBalance } = this.props; + const { publicDeriver, walletBalance, disabledForReason } = this.props; // eslint-disable-next-line no-unused-vars const [_, iconComponent] = publicDeriver.plate ? constructPlate(publicDeriver.plate, 0, styles.icon) @@ -44,25 +45,28 @@ export default class WalletCard extends Component { const checksum = this.props.publicDeriver.plate?.TextPart; return ( - -
-
{iconComponent}
-
- - {this.props.publicDeriver.name} - -
{checksum}
+ <> + +
+
{iconComponent}
+
+ + {this.props.publicDeriver.name} + +
{checksum}
+
+ {walletBalance != null && {walletBalance}}
- {walletBalance != null && {walletBalance}} -
- + + {disabledForReason && (
{disabledForReason}
)} + ); } } diff --git a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss index 9f846562d5..d6187b178f 100644 --- a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss +++ b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss @@ -50,3 +50,12 @@ font-size: 14px; } } + +.disabledReason { + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.20000000298023224px; + text-align: left; + color: var(--text-error, rgba(255, 19, 81, 1)); +} diff --git a/packages/yoroi-extension/app/connector/components/layout/Layout.js b/packages/yoroi-extension/app/connector/components/layout/Layout.js index f80423e5db..7d76c3c336 100644 --- a/packages/yoroi-extension/app/connector/components/layout/Layout.js +++ b/packages/yoroi-extension/app/connector/components/layout/Layout.js @@ -1,6 +1,7 @@ // @flow import { Component } from 'react'; -import type { Node } from 'react'; +import type { Node, ComponentType } from 'react'; +import { withRouter, type Location } from 'react-router-dom'; import { ReactComponent as YoroiLogo } from '../../assets/images/yoroi-logo.inline.svg'; import styles from './Layout.scss'; import { observer } from 'mobx-react'; @@ -9,25 +10,38 @@ import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import TestnetWarningBanner from '../../../components/topbar/banners/TestnetWarningBanner'; import { ReactComponent as DappConnectorIcon } from '../../../assets/images/dapp-connector/dapp-connector.inline.svg'; import environment from '../../../environment'; +import { ROUTES } from '../../routes-config'; type Props = {| children: Node, |}; +type LocationProp = {| + location: Location, +|}; -const messages = defineMessages({ +export const messages: Object = defineMessages({ yoroiDappConnector: { id: 'global.connector.yoroiDappConnector', defaultMessage: '!!!Yoroi Dapp Connector', }, + yoroiConnector: { + id: 'global.connector.yoroiConnector', + defaultMessage: '!!!Yoroi Connector', + }, }); + @observer -export default class Layout extends Component { +class Layout extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired, }; render(): Node { const { intl } = this.context; + const title = intl.formatMessage( + this.props.location.pathname === ROUTES.SELECT_CASHBACK_WALLET ? + messages.yoroiConnector : messages.yoroiDappConnector + ); return (
@@ -36,7 +50,7 @@ export default class Layout extends Component {
-

{intl.formatMessage(messages.yoroiDappConnector)}

+

{title}

@@ -48,3 +62,5 @@ export default class Layout extends Component { ); } } + +export default (withRouter(Layout): ComponentType); diff --git a/packages/yoroi-extension/app/connector/containers/SelectCashbackWalletContainer.js b/packages/yoroi-extension/app/connector/containers/SelectCashbackWalletContainer.js new file mode 100644 index 0000000000..21ac3f0956 --- /dev/null +++ b/packages/yoroi-extension/app/connector/containers/SelectCashbackWalletContainer.js @@ -0,0 +1,52 @@ +// @flow +import { Component, type Node } from 'react'; +import ConnectPage from '../components/connect/ConnectPage'; +import { observer } from 'mobx-react'; +import { genLookupOrFail } from '../../stores/stateless/tokenHelpers'; +import type { WalletState } from '../../../chrome/extension/background/types'; +import { setCashbackWallet } from '../../api/thunk'; +import type { ConnectorStoresProps } from '../stores'; + +@observer +export default class SelectCashbackWalletContainer extends Component { + componentDidMount() { + this.props.stores.connector.refreshWallets(); + } + + onCancel() { + window.close(); + } + + onSelectWallet(wallet: WalletState) { + setCashbackWallet(wallet.publicDeriverId); + // must delay or the message gets lost + setTimeout(window.close, 50); + } + + render(): Node { + const { stores } = this.props; + + return ( + {}} // placeholder + onCancel={this.onCancel} + isAppAuth={false} // na + hidePasswordForm={() => {}} // placeholder + loading={stores.connector.loadingWallets} + error={''} // na + message={null} // na + publicDerivers={stores.connector.wallets} + onSelectWallet={this.onSelectWallet} + network="Cardano" + getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} + shouldHideBalance={stores.profile.shouldHideBalance} + unitOfAccount={stores.profile.unitOfAccount} + getCurrentPrice={stores.coinPriceStore.getCurrentPrice} + onUpdateHideBalance={stores.profile.updateHideBalance} + isSelectingCashbackWallet + /> + ); + } +} + diff --git a/packages/yoroi-extension/app/connector/routes-config.js b/packages/yoroi-extension/app/connector/routes-config.js index f526f0ecc3..a602fb167e 100644 --- a/packages/yoroi-extension/app/connector/routes-config.js +++ b/packages/yoroi-extension/app/connector/routes-config.js @@ -2,4 +2,5 @@ export const ROUTES = { ROOT: '/', SIGNIN_TRANSACTION: '/signin-transaction', + SELECT_CASHBACK_WALLET: '/select-cashback-wallet', }; diff --git a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js index e70731630a..8c1fcf6a38 100644 --- a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js +++ b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js @@ -27,6 +27,7 @@ import { getCardanoHaskellBaseConfig, getNetworkById, isCardanoHaskell, + networks, } from '../../api/ada/lib/storage/database/prepackaged/networks'; import { MultiToken } from '../../api/common/lib/MultiToken'; import { RustModule } from '../../api/ada/lib/cardanoCrypto/rustLoader'; @@ -382,7 +383,9 @@ export default class ConnectorStore extends Store { }); try { - const wallets = await getWallets(); + const wallets = await getWallets( + this.stores.profile.currentNetworkId ?? networks.CardanoMainnet.NetworkId + ); runInAction(() => { this.loadingWallets = LoadingWalletStates.SUCCESS; diff --git a/packages/yoroi-extension/app/connector/stores/toplevel/ConnectorLoadingStore.js b/packages/yoroi-extension/app/connector/stores/toplevel/ConnectorLoadingStore.js index 6185aff515..49c289d01f 100644 --- a/packages/yoroi-extension/app/connector/stores/toplevel/ConnectorLoadingStore.js +++ b/packages/yoroi-extension/app/connector/stores/toplevel/ConnectorLoadingStore.js @@ -7,18 +7,13 @@ import { export default class ConnectorLoadingStore extends BaseLoadingStore { - async preLoadingScreenEnd(): Promise { - await super.preLoadingScreenEnd(); + async loadingEnd(): Promise { // fixme ? wait for wallets loading await this.stores.tokenInfoStore.refreshTokenInfo(); await this.stores.coinPriceStore.loadFromStorage(); } - postLoadingScreenEnd(): void { - super.postLoadingScreenEnd(); - } - getTabIdKey(): string { return TabIdKeys.YoroiConnector; } diff --git a/packages/yoroi-extension/app/containers/NavBarContainer.js b/packages/yoroi-extension/app/containers/NavBarContainer.js deleted file mode 100644 index e00a83b0c9..0000000000 --- a/packages/yoroi-extension/app/containers/NavBarContainer.js +++ /dev/null @@ -1,143 +0,0 @@ -// @flow -import moment from 'moment'; -import { Component } from 'react'; -import type { Node } from 'react'; -import { observer } from 'mobx-react'; -import { intlShape } from 'react-intl'; -import NavBar from '../components/topbar/NavBar'; -import NavPlate from '../components/topbar/NavPlate'; -import NavWalletDetails from '../components/topbar/NavWalletDetails'; -import NoWalletsDropdown from '../components/topbar/NoWalletsDropdown'; -import NavDropdown from '../components/topbar/NavDropdown'; -import NavDropdownRow from '../components/topbar/NavDropdownRow'; -import { ROUTES } from '../routes-config'; -import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; -import { genLookupOrFail } from '../stores/stateless/tokenHelpers'; -import BuySellDialog from '../components/buySell/BuySellDialog'; -import globalMessages from '../i18n/global-messages'; -import { MultiToken } from '../api/common/lib/MultiToken'; -import type { StoresProps } from '../stores'; - -type LocalProps = {| - title: Node, -|}; - -@observer -export default class NavBarContainer extends Component<{| ...StoresProps, ...LocalProps |}> { - static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { - intl: intlShape.isRequired, - }; - - updateHideBalance: void => Promise = async () => { - await this.props.stores.profile.updateHideBalance(); - }; - - switchToNewWallet: (number) => void = publicDeriverId => { - this.props.stores.app.goToRoute({ - route: this.props.stores.app.currentRoute, - publicDeriverId, - }); - }; - - openDialogWrapper: any => void = dialog => { - this.props.stores.app.goToRoute({ route: ROUTES.MY_WALLETS }); - this.props.stores.uiDialogs.open({ dialog }); - }; - - render(): Node { - const { intl } = this.context; - const { stores } = this.props; - const { profile, wallets: walletsStore } = stores; - const { wallets } = walletsStore; - - const walletComponents = wallets.map(wallet => { - const rewards: MultiToken = this.props.stores.delegation.getRewardBalanceOrZero(wallet); - const { lastSyncInfo } = wallet; - - return ( - } - onSelect={() => this.switchToNewWallet(wallet.publicDeriverId)} - isCurrentWallet={wallet === stores.wallets.selected} - syncTime={lastSyncInfo?.Time ? moment(lastSyncInfo.Time).fromNow() : null} - detailComponent={ - - } - /> - ); - }); - const dropdownContent = ( - <> - - {walletComponents} - - ); - - const dropdownComponent = (() => { - const getDropdownHead = () => { - const publicDeriver = walletsStore.selected; - if (publicDeriver == null) { - return ; - } - - const rewards: MultiToken = stores.delegation.getRewardBalanceOrZero( - publicDeriver - ); - - return ( - - ); - }; - - return ( - - stores.app.goToRoute({ route: ROUTES.WALLETS.ADD }) - } - openBuySellDialog={() => this.openDialogWrapper(BuySellDialog)} - /> - ); - })(); - - const getPlate = () => { - const { selected, selectedWalletName } = walletsStore; - if (selected == null || selectedWalletName == null) return null; - return ; - }; - - return ( - - ); - } -} diff --git a/packages/yoroi-extension/app/containers/NavBarContainerRevamp.js b/packages/yoroi-extension/app/containers/NavBarContainerRevamp.js index 1620e603bc..bfac4266d8 100644 --- a/packages/yoroi-extension/app/containers/NavBarContainerRevamp.js +++ b/packages/yoroi-extension/app/containers/NavBarContainerRevamp.js @@ -6,7 +6,7 @@ import { intlShape } from 'react-intl'; import { observer } from 'mobx-react'; import { ROUTES } from '../routes-config'; import { genLookupOrFail } from '../stores/stateless/tokenHelpers'; -import { getNetworkById } from '../api/ada/lib/storage/database/prepackaged/networks'; +import { networks, getNetworkById } from '../api/ada/lib/storage/database/prepackaged/networks'; import { addressToDisplayString } from '../api/ada/lib/storage/bridge/utils'; import BuySellDialog from '../components/buySell/BuySellDialog'; import NavBarRevamp from '../components/topbar/NavBarRevamp'; @@ -16,8 +16,24 @@ import BuySellAdaButton from '../components/topbar/BuySellAdaButton'; import { ampli } from '../../ampli/index'; import { MultiToken } from '../api/common/lib/MultiToken'; import LocalStorageApi from '../api/localStorage/index'; +import SwitchNetworkDialogContainer from './settings/categories/SwitchNetworkDialogContainer'; import type { StoresProps } from '../stores'; +const NETWORK_BADGES = Object.freeze({ + [networks.CardanoPreprodTestnet.NetworkId]: { + color: 'rgba(236, 186, 9, 1)', + text: 'preprod', + }, + [networks.CardanoPreviewTestnet.NetworkId]: { + color: 'rgba(143, 201, 246, 1)', + text: 'preview', + }, + [networks.CardanoSanchoTestnet.NetworkId]: { + color: 'rgba(147, 245, 225, 1)', + text: 'sancho', + }, +}); + type LocalProps = {| title: Node, menu?: Node, @@ -53,8 +69,9 @@ export default class NavBarContainerRevamp extends Component<{| ...StoresProps, const isRewardWallet = delegation.isRewardWallet(newWalletId); const isStakingPage = app.currentRoute === ROUTES.STAKING; await localStorage.unsetPortfolioFiatPair(); + this.props.stores.wallets.setActiveWallet({ publicDeriverId: newWalletId }); const route = !isRewardWallet && isStakingPage ? ROUTES.WALLETS.ROOT : app.currentRoute; - this.props.stores.app.goToRoute({ route, publicDeriverId: newWalletId }); + this.props.stores.app.goToRoute({ route }); }; checkAndResetGovRoutes: void => void = () => { @@ -102,11 +119,38 @@ export default class NavBarContainerRevamp extends Component<{| ...StoresProps, ); }; + let title; + if ( + this.props.stores.wallets.selected?.networkId === networks.CardanoMainnet.NetworkId || + !this.props.stores.wallets.selected + ) { + title = this.props.title; + } else { + const { color, text } = NETWORK_BADGES[this.props.stores.wallets.selected.networkId]; + title = ( +
+ {this.props.title} + +
+ ); + } + return ( <> {this.getDialog()} : null} buyButton={ @@ -127,28 +171,16 @@ export default class NavBarContainerRevamp extends Component<{| ...StoresProps, const shouldHideBalance = stores.profile.shouldHideBalance; if (stores.uiDialogs.isOpen(WalletListDialog)) { - const cardanoWallets = []; - - wallets.forEach(wallet => { - const rewards = stores.delegation.getRewardBalanceOrZero( - wallet - ); - - const walletMap = { - walletId: wallet.publicDeriverId, - rewards, - amount: wallet.balance, - plate: wallet.plate, - type: wallet.type, - name: wallet.name, - }; - - cardanoWallets.push(walletMap); - }); - return ( ({ + walletId: wallet.publicDeriverId, + rewards: this.props.stores.delegation.getRewardBalanceOrZero(wallet), + amount: wallet.balance, + plate: wallet.plate, + type: wallet.type, + name: wallet.name, + }))} onSelect={wallet => { this.checkAndResetGovRoutes(); this.onSelectWallet(wallet); @@ -163,8 +195,9 @@ export default class NavBarContainerRevamp extends Component<{| ...StoresProps, getTokenInfo={getTokenInfo} walletAmount={selected?.balance} onAddWallet={this.addNewWallet} - updateSortedWalletList={stores.profile.updateSortedWalletList} - walletsNavigation={stores.profile.walletsNavigation} + onUpdateWalletListOrder={async (from, to) => { + await this.props.stores.wallets.reorderWallets(from, to); + }} unitOfAccountSetting={stores.profile.unitOfAccount} getCurrentPrice={stores.coinPriceStore.getCurrentPrice} /> @@ -194,6 +227,11 @@ export default class NavBarContainerRevamp extends Component<{| ...StoresProps, ); } + if (this.props.stores.uiDialogs.isOpen(SwitchNetworkDialogContainer)) { + return ; + } + return null; }; + } diff --git a/packages/yoroi-extension/app/containers/cashback/CashbackPage.js b/packages/yoroi-extension/app/containers/cashback/CashbackPage.js new file mode 100644 index 0000000000..897d36f5ac --- /dev/null +++ b/packages/yoroi-extension/app/containers/cashback/CashbackPage.js @@ -0,0 +1,564 @@ +// @flow +import { useState, useEffect, useCallback, Suspense, useRef, useMemo } from 'react'; +import styles from './styles.module.css' +import { observer } from 'mobx-react'; +import type { StoresProps } from '../../stores'; +import TopBarLayout from '../../components/layout/TopBarLayout'; +import BannerContainer from '../banners/BannerContainer'; +import SidebarContainer from '../SidebarContainer'; +import environment from '../../environment'; +import { ROUTES } from '../../routes-config'; +import NavBarContainerRevamp from '../NavBarContainerRevamp'; +import NavBarTitle from '../../components/topbar/NavBarTitle'; +import globalMessages from '../../i18n/global-messages'; +import { CoreAddressTypes } from '../../api/ada/lib/storage/database/primitives/enums'; +import { walletSignData, encodeHardwareWalletSignResult } from '../../api/ada'; +import { getPublicDeriverById } from '../../../chrome/extension/background/handlers/yoroi/utils'; +import Dialog from '../../components/widgets/Dialog'; +import DialogCloseButton from '../../components/widgets/DialogCloseButton'; +import { RustModule } from '../../api/ada/lib/cardanoCrypto/rustLoader'; +import { + useTheme, + Box, + TextField, + Typography, + DialogContentText +} from '@mui/material'; +import { LedgerConnect } from '../../utils/hwConnectHandler'; +import { MessageAddressFieldType, AddressType } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { WrongPassphraseError } from '../../api/ada/lib/cardanoCrypto/cryptoErrors'; +import { IncorrectWalletPasswordError } from '../../api/common/errors'; +import { convertToLocalizableError } from '../../domain/LedgerLocalizedError'; +import LocalizableError from '../../i18n/LocalizableError'; +import type { $npm$ReactIntl$IntlShape } from 'react-intl'; +import { injectIntl, defineMessages, } from 'react-intl'; +import { getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks'; +import { forceNonNull } from '../../coreUtils'; +import { constructPlate32 } from '../../components/topbar/WalletCard'; +import LocalStorageApi from '../../api/localStorage'; +import DisclaimerDialog from '../../components/widgets/DisclaimerDialog'; +import type { BringConfigType, ConfigType } from '../../../config/config-types'; + +const messages = defineMessages({ + claim: { + id: 'cashback.claim.dialog.title', + defaultMessage: '!!!CLAIM CASHBACK', + }, + passwordClaimInstruction: { + id: 'cashback.claim.dialog.instruction.password', + defaultMessage: '!!!Enter your password to claim cashback rewards.', + }, + hardwardClaimInstruction: { + id: 'cashback.claim.dialog.instruction.hardware', + defaultMessage: '!!!Confirm on your hardware wallet to claim cashback rewards.', + }, + message: { + id: 'cashback.claim.dialog.message.label', + defaultMessage: '!!!Message', + }, + notCurrentText: { + id: 'cashback.not.current.warning.text', + defaultMessage: '!!!Your cashback rewards are currently linked to another wallet. To claim your ADA cashback, either switch to the rewards wallet or change the rewards wallet to the one you\'re using now. You can always access settings at anytime to change your cashback wallet and make claiming rewards relevant for you.', + }, + currentWalletLabel: { + id: 'cashback.not.current.warning.current', + defaultMessage: '!!!Current rewards wallet' + }, + warning: { + id: 'cashback.not.current.warning.title', + defaultMessage: '!!!WARNING', + }, + useThis: { + id: 'cashback.not.current.warning.use.this', + defaultMessage: '!!!use this wallet', + }, + keep: { + id: 'cashback.not.current.warning.keep', + defaultMessage: '!!!keep current wallet', + }, + chooseTitle: { + id: 'cashback.not.current.warning.title.choose', + defaultMessage: '!!!wrong wallet', + }, + setThis: { + id: 'cashback.not.current.warning.button.set.this', + defaultMessage: '!!!set this wallet', + }, + switch: { + id: 'cashback.not.current.warning.button.switch', + defaultMessage: '!!!switch wallet', + }, + chooseText1: { + id: 'cashback.not.current.warning.text.choose.1', + defaultMessage: '!!!Your cashback rewards are currently linked to another wallet.', + }, + chooseText2: { + id: 'cashback.not.current.warning.text.choose.2', + defaultMessage: '!!!Switch wallet to access your rewards or set this wallet as your cashback wallet.', + }, + setCurrentTitle: { + id: 'cashback.not.current.warning.title.set.current', + defaultMessage: '!!!Set my Current Wallet as my Cashback Wallet' + }, + no: { + id: 'cashback.not.current.warning.button.no', + defaultMessage: '!!!no' + }, + yes: { + id: 'cashback.not.current.warning.button.yes', + defaultMessage: '!!!yes' + }, + switchText: { + id: 'cashback.not.current.warning.text.switch', + defaultMessage: '!!!You will no longer be able to claim rewards linked to your previous cashback wallet until you link it again.' + }, +}); + +// populated by ConfigWebpackPlugin +declare var CONFIG: ConfigType; + +type NotCurrentWalletModalProps = {| + onSetCurrentAsCashbackWallet: () => void, + onSwitchToCashbackWallet: () => void, + shownCashbackWallet: { + plate: {| + ImagePart: string, + TextPart: string, + |}, + name: string, + ..., +}, +intl: $npm$ReactIntl$IntlShape, +|}; + +const NotCurrentWalletModal = injectIntl(observer((props: NotCurrentWalletModalProps) => { + const { intl } = props; + + const [state, setState] = useState < 'switchOrSet' | 'confirmSet' > ('switchOrSet'); + + if (state === 'switchOrSet') { + return ( + { + setState('confirmSet'); + } + }, + { + label: intl.formatMessage(messages.switch), + primary: true, + onClick: props.onSwitchToCashbackWallet, + } + ]} + > + + {intl.formatMessage(messages.chooseText1)} + + + {intl.formatMessage(messages.chooseText2)} + + + ); + } + + return ( + { + setState('switchOrSet'); + } + }, + { + label: intl.formatMessage(messages.yes), + primary: true, + onClick: props.onSetCurrentAsCashbackWallet, + } + ]} + > + + {intl.formatMessage(messages.switchText)} + + + {intl.formatMessage(messages.currentWalletLabel)} + + + {constructPlate32(props.shownCashbackWallet.plate)[1]} + + {props.shownCashbackWallet.plate.TextPart} + + + + ); +})); + +type AllProps = {| ...StoresProps, intl: $npm$ReactIntl$IntlShape, |}; + +type IframeMessageData = {| + action: string, + overlayBgColor ?: string, + messageToSign: string, + amount: number + |}; + +const canUseSandbox = environment.isDev() || environment.isNightly(); + +const CashbackPageContainer = observer((props: AllProps) => { + const { stores, intl } = props; + const wallet = stores.wallets.selected; + if (!wallet) throw Error('no publicDeriver'); + + const theme = useTheme(); + + const iframeRef = useRef < HTMLIFrameElement | null > (null); + const [iframeSrc, setIframeSrc] = useState(''); + const [popup, setPopup] = useState(false); + const [password, setPassword] = useState('') + const [errMsg, setErrMsg] = useState('') + const [message, setMessage] = useState('') + const [signaturePopup, setSignaturePopup] = useState(false); + const [overlayBgColor, setOverlayBgColor] = useState('#000000fa'); + + const bringSandboxRequest = stores.profile.getBringSandboxRequest; + const isBringSandbox: ?boolean = useMemo( + () => canUseSandbox && bringSandboxRequest.result, + [bringSandboxRequest.result], + ); + + + const fetchIframeUrl = useCallback(async () => { + + const bringConfig: BringConfigType = isBringSandbox ? CONFIG.bringSandbox : CONFIG.bring; + + try { + const publicDeriver = stores.wallets.selected; + if (!publicDeriver) throw Error('no publicDeriver'); + const walletAddress = RustModule.WalletV4.Address.from_hex( + publicDeriver.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0].address + ).to_bech32(); + + const response = await fetch(`${bringConfig.baseUrl}check/portal`, { + method: 'POST', + headers: { + 'x-api-key': bringConfig.identifier, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ walletAddress }), + }); + const data = await response.json(); + const url = new URL(data.iframeUrl); + url.searchParams.set('token', data.token); + url.searchParams.set('theme', theme.name.split('-')[0]); + + setIframeSrc(url.href); + } catch (error) { + console.error('Error fetching data:', error); + } + }, [stores.wallets.selected, isBringSandbox]); + + function stringToHex(str) { + return Array.from(str) + .map(char => char.charCodeAt(0).toString(16).padStart(2, '0')) + .join(''); + } + + const signMessage = useCallback(async (msg: string, pwd: string) => { + const { address, addressing } = wallet.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0]; + + try { + let res; + if (wallet.type === 'mnemonic') { + const publicDeriver = await getPublicDeriverById(wallet.publicDeriverId) + try { + res = await walletSignData(publicDeriver, pwd, address, stringToHex(msg)) + } catch (error) { + if (error instanceof WrongPassphraseError) { + throw new IncorrectWalletPasswordError(); + } + throw error; + } + } else if (wallet.type === 'ledger') { + const ledgerConnect = new LedgerConnect({ + locale: stores.profile.currentLocale, + }); + try { + const network = getNetworkById(wallet.networkId); + const config = network.BaseConfig[0]; + const messageHex = stringToHex(msg); + const hashPayload = true; + const { signatureHex, signingPublicKeyHex, addressFieldHex } = await ledgerConnect.signMessage({ + serial: null, + params: { + preferHexDisplay: false, + messageHex, + signingPath: addressing.path, + hashPayload, + addressFieldType: MessageAddressFieldType.ADDRESS, + address: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: addressing.path, + stakingPath: wallet.stakingAddressing.addressing.path, + }, + }, + network: { + protocolMagic: config.ByronNetworkId, + networkId: Number(config.ChainNetworkId), + }, + }, + }); + res = await encodeHardwareWalletSignResult( + addressFieldHex, + signatureHex, + messageHex, + signingPublicKeyHex, + hashPayload, + ); + } catch (error) { + throw new convertToLocalizableError(error); + } + } else { + throw new Error('unsupported wallet type'); + } + iframeRef.current?.contentWindow.postMessage( + { + to: 'bringweb3', + action: 'SIGNATURE', + ...res, + message: msg, + address + }, + '*' + ); + setSignaturePopup(false); + setPassword(''); + } catch (error) { + setErrMsg(error instanceof LocalizableError ? intl.formatMessage(error) : error.message) + // console.warn(error); + } + }, []); + + const handleMessage = useCallback(async (event: MessageEvent) => { + const iframeOrigin = new URL(iframeSrc).origin; + + if (event.origin !== iframeOrigin) { + return; + } + + const messageData: IframeMessageData = (event.data: any); + + if (messageData.action === 'SIGN_MESSAGE') { + setMessage(messageData.messageToSign) + setSignaturePopup(true) + } else if (messageData.action === 'POPUP_OPENED') { + setPopup(true); + setOverlayBgColor(messageData.overlayBgColor || overlayBgColor); + } else if (messageData.action === 'POPUP_CLOSED') { + setPopup(false); + } +}, [iframeSrc, overlayBgColor]); + +useEffect(() => { + if (environment.isLight) { + stores.app.goToRoute({ + route: ROUTES.WALLETS.ROOT, + }); + } + if (!iframeSrc) fetchIframeUrl(); + + window.addEventListener('message', handleMessage); + + return () => { + window.removeEventListener('message', handleMessage); + }; +}, [iframeSrc, fetchIframeUrl, handleMessage]); + +// If the current cashback wallet is not the current wallet, this value initially holds the current cashback +// wallet, to be shown in a warning dialog. +const [shownCashbackWallet, setShownCashbackWallet] = useState(null); + +useEffect(() => { + stores.wallets.getCashbackWalletRequest.execute().then((currentCashbackWallet) => { + if (currentCashbackWallet && currentCashbackWallet !== stores.wallets.selected) { + setShownCashbackWallet(currentCashbackWallet); + setPopup(true); + } + return 'nonsense'; + }).catch(console.error); +}, []); + +const [shouldShowDisclaimer, setShouldShowDisclaimer] = useState(false); + +useEffect(() => { + const localStorageApi = new LocalStorageApi(); + localStorageApi.isDisclaimerShown('cashback').then((result) => { + if (!result) { + setShouldShowDisclaimer(true); + setPopup(true); + } + return 'nonsense'; + }).catch(console.error); +}, []); + +const closePopup = useCallback(() => { + iframeRef.current?.contentWindow.postMessage({ to: 'bringweb3', action: 'CLOSE_POPUP' }, '*'); + setPopup(false); +}, []); + +const abortClaim = useCallback(() => { + iframeRef.current?.contentWindow.postMessage({ to: 'bringweb3', action: 'ABORT_SIGN_MESSAGE' }, '*'); + setSignaturePopup(false) + setPassword('') + setErrMsg('') +}, []); + +const sidebarContainer = ; + +return ( + } + sidebar={sidebarContainer} + navbar={ + } + /> + } + > + + {shouldShowDisclaimer && ( + { + setShouldShowDisclaimer(false); + if (!shownCashbackWallet) { + setPopup(false); + } + const localStorageApi = new LocalStorageApi(); + localStorageApi.setShownDisclaimer('cashback'); + }} + /> + )} + + {shownCashbackWallet && !shouldShowDisclaimer && ( + { + stores.wallets.setCashbackWallet(forceNonNull(stores.wallets.selected).publicDeriverId); + setShownCashbackWallet(null); + setPopup(false); + }} + onSwitchToCashbackWallet={() => { + stores.wallets.setActiveWallet( + { publicDeriverId: shownCashbackWallet.publicDeriverId } + ); + }} + /> + )} + + {signaturePopup ? + } + onClose={abortClaim} + dialogActions={[{ + label: intl.formatMessage(globalMessages.confirm), + primary: true, + disabled: wallet.type === 'mnemonic' && !password, + onClick: () => signMessage(message, password) + }]} + > + + + {intl.formatMessage(wallet.type === 'mnemonic' ? + messages.passwordClaimInstruction : messages.hardwardClaimInstruction + )} + + + {intl.formatMessage(messages.message)} + + + {message} + + {wallet.type === 'mnemonic' && ( + + // setShowPassword(!showPassword)} + // edge="end" + // > + // {!showPassword ? : } + // + // + // } + onChange={e => { + setPassword(e.target.value); + }} + error={!!errMsg} + disabled={false} + /> + )} + + {errMsg} + + + + : null} + + {popup ? ( + // eslint-disable-next-line +
+ ) : null} + + {iframeSrc && ( +