diff --git a/.env.example b/.env.example index fb227a93..3a25abe7 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ NUXT_PUBLIC_MAINTENANCE=false NUXT_PUBLIC_TESTING=true #PROXY_DOMAIN=rotki.com #PROXY_INSECURE=true #When set it will proxy to http instead of https +NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= diff --git a/components/account/home/AccountAddress.vue b/components/account/home/AccountAddress.vue index 2e019095..a64a2e88 100644 --- a/components/account/home/AccountAddress.vue +++ b/components/account/home/AccountAddress.vue @@ -4,6 +4,7 @@ import { required } from '@vuelidate/validators'; import { get, objectOmit, set } from '@vueuse/core'; import { storeToRefs } from 'pinia'; import { useMainStore } from '~/store'; +import { toMessages } from '~/utils/validation'; const store = useMainStore(); const state = reactive({ diff --git a/components/account/home/AccountInformation.vue b/components/account/home/AccountInformation.vue index 790f074e..1d3e6050 100644 --- a/components/account/home/AccountInformation.vue +++ b/components/account/home/AccountInformation.vue @@ -4,6 +4,7 @@ import { required } from '@vuelidate/validators'; import { get, objectOmit, set } from '@vueuse/core'; import { storeToRefs } from 'pinia'; import { useMainStore } from '~/store'; +import { toMessages } from '~/utils/validation'; const store = useMainStore(); const state = reactive({ diff --git a/components/account/home/ChangePassword.vue b/components/account/home/ChangePassword.vue index 053fe915..b0b6ae25 100644 --- a/components/account/home/ChangePassword.vue +++ b/components/account/home/ChangePassword.vue @@ -3,6 +3,7 @@ import { useVuelidate } from '@vuelidate/core'; import { minLength, required, sameAs } from '@vuelidate/validators'; import { get, set } from '@vueuse/core'; import { useMainStore } from '~/store'; +import { toMessages } from '~/utils/validation'; import type { ActionResult } from '~/types/common'; const loading = ref(false); diff --git a/components/checkout/pay/CryptoPage.vue b/components/checkout/pay/CryptoPage.vue index f8c9bb83..2eec3a2c 100644 --- a/components/checkout/pay/CryptoPage.vue +++ b/components/checkout/pay/CryptoPage.vue @@ -1,75 +1,26 @@ @@ -182,9 +160,10 @@ async function changePaymentMethod() { :pending="pending || currentState === 'pending'" v-bind="{ success, failure, status }" :loading="loading" - :metamask-support="metamaskSupport" :plan="plan" - @pay="payWithMetamask()" + :connected="account.isConnected" + @pay="pay()" + @connect="open()" @change="changePaymentMethod()" @clear:errors="clearErrors()" /> diff --git a/components/checkout/pay/CryptoPaymentForm.vue b/components/checkout/pay/CryptoPaymentForm.vue index 47bbb042..53d102e3 100644 --- a/components/checkout/pay/CryptoPaymentForm.vue +++ b/components/checkout/pay/CryptoPaymentForm.vue @@ -15,13 +15,14 @@ const props = defineProps<{ success: boolean; failure: boolean; loading: boolean; - metamaskSupport: boolean; + connected: boolean; status: PaymentStep; }>(); const emit = defineEmits<{ (e: 'change'): void; (e: 'pay'): void; + (e: 'connect'): void; (e: 'clear:errors'): void; }>(); @@ -96,7 +97,8 @@ const stopWatcher = watchEffect(() => { const { copy: copyToClipboard } = useClipboard({ source: qrText }); const isBtc = computed(() => get(data).chainName === 'bitcoin'); -const payWithMetamask = () => emit('pay'); +const pay = () => emit('pay'); +const connect = () => emit('connect'); const changePaymentMethod = () => emit('change'); const clearErrors = () => emit('clear:errors'); const css = useCssModule(); @@ -173,14 +175,14 @@ const showChangePaymentDialog = ref(false); warning /> - {{ t('home.plans.tiers.step_3.metamask.notice') }} + {{ t('home.plans.tiers.step_3.wallet.notice') }} - {{ t('home.plans.tiers.step_3.metamask.paid_notice_1') }} + {{ t('home.plans.tiers.step_3.wallet.paid_notice_1') }} - {{ t('home.plans.tiers.step_3.metamask.paid_notice_2') }} + {{ t('home.plans.tiers.step_3.wallet.paid_notice_2') }} @@ -197,21 +199,43 @@ const showChangePaymentDialog = ref(false); - - - - {{ t('home.plans.tiers.step_3.metamask.action') }} + {{ t('home.plans.tiers.step_3.wallet.connect_wallet') }} + + + {{ t('home.plans.tiers.step_3.wallet.pay_with_wallet') }} + + + + + + diff --git a/components/icons/MetamaskIcon.vue b/components/icons/MetamaskIcon.vue deleted file mode 100644 index ef5a749a..00000000 --- a/components/icons/MetamaskIcon.vue +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composables/crypto-payment.ts b/composables/crypto-payment.ts index f1079509..06de5760 100644 --- a/composables/crypto-payment.ts +++ b/composables/crypto-payment.ts @@ -1,60 +1,83 @@ +import { EthersAdapter } from '@reown/appkit-adapter-ethers'; import { - BrowserProvider, - Contract, - type Signer, - type TransactionResponse, - parseUnits, -} from 'ethers'; + type AppKitNetwork, + arbitrum, + arbitrumSepolia, + base, + baseSepolia, + mainnet, + sepolia, +} from '@reown/appkit/networks'; +import { createAppKit, useAppKitAccount, useAppKitProvider } from '@reown/appkit/vue'; import { get, set, useTimeoutFn } from '@vueuse/core'; -import { useLogger } from '~/utils/use-logger'; -import { assert } from '~/utils/assert'; +import { BrowserProvider, Contract, type Signer, type TransactionResponse, parseUnits } from 'ethers'; import { useMainStore } from '~/store'; -import type { - CryptoPayment, - IdleStep, - Provider, - StepType, -} from '~/types'; +import { assert } from '~/utils/assert'; +import { useLogger } from '~/utils/use-logger'; +import type { CryptoPayment, IdleStep, StepType } from '~/types'; import type { Ref } from 'vue'; const abi = [ // Some details about the token 'function name() view returns (string)', 'function symbol() view returns (string)', - // Get the account balance 'function balanceOf(address) view returns (uint)', - // Send some of your tokens to someone else 'function transfer(address to, uint amount)', - // An event triggered whenever anyone transfers to someone else 'event Transfer(address indexed from, address indexed to, uint amount)', ]; -export const getChainId = (testing: boolean, chainId?: string | number) => BigInt(chainId ?? (testing ? 11155111 : 1)); +export const getChainId = (testing: boolean, chainId?: string | number) => Number(chainId ?? (testing ? 11155111 : 1)); -export function useWeb3Payment(data: Ref, getProvider: () => Provider, testing: boolean) { +const testNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [sepolia, arbitrumSepolia, baseSepolia]; +const productionNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, arbitrum, base]; + +export function useWeb3Payment(data: Ref) { const { markTransactionStarted } = useMainStore(); const state = ref('idle'); const error = ref(''); const logger = useLogger('web3-payment'); - - const { start, stop } = useTimeoutFn( - () => { - logger.info('change to done'); - set(state, 'success'); + const { t } = useI18n(); + const { public: { baseUrl, testing, walletConnect: { projectId } } } = useRuntimeConfig(); + const { start, stop } = useTimeoutFn(() => { + logger.info('change to done'); + set(state, 'success'); + }, 5000, { immediate: false }); + + const account = useAppKitAccount(); + const defaultNetwork = getNetwork(get(data)?.chainId); + + const appKit = createAppKit({ + adapters: [new EthersAdapter()], + allowUnsupportedChain: false, + defaultNetwork, + features: { + analytics: true, + email: false, + onramp: false, + socials: false, + swaps: false, + }, + metadata: { + description: 'rotki is an open source portfolio tracker, accounting and analytics tool that protects your privacy.', + icons: ['https://raw.githubusercontent.com/rotki/data/main/assets/icons/app_logo.png'], + name: 'Rotki', + url: baseUrl, }, - 5000, - { immediate: false }, - ); + networks: testing ? testNetworks : productionNetworks, + projectId, + themeMode: 'light', + }); - async function executePayment(signer: Signer): Promise { - stop(); - const payment = get(data); - assert(payment); + appKit.subscribeAccount(() => { + clearErrors(); + }); + async function executePayment(signer: Signer, payment: CryptoPayment): Promise { + stop(); const { cryptoAddress: to, cryptocurrency, @@ -72,19 +95,12 @@ export function useWeb3Payment(data: Ref, getProvider: () // Pay with native token if (!tokenAddress) { - tx = await signer.sendTransaction({ - to, - value, - }); + tx = await signer.sendTransaction({ to, value }); } // Pay with non-native token else { const contract = new Contract(tokenAddress, abi, signer); - - tx = await (contract.transfer( - to, - value, - ) as Promise); + tx = await (contract.transfer(to, value) as Promise); } logger.info(`transaction is pending: ${tx.hash}`); @@ -92,44 +108,34 @@ export function useWeb3Payment(data: Ref, getProvider: () start(); } - const payWithMetamask = async () => { + const pay = async (): Promise => { if (get(state) === 'pending') return; set(state, 'pending'); + try { - const payment = get(data); - assert(payment); - const provider = getProvider(); - const accounts = await provider.request({ - method: 'eth_requestAccounts', - params: [], - }); - - if (!accounts || accounts.length === 0) { - logger.info('missing permission'); + if (!get(account, 'isConnected')) { + set(error, t('subscription.crypto_payment.not_connected')); return; } - const browserProvider = new BrowserProvider(provider); - const network = await browserProvider.getNetwork(); - const { - chainId, - chainName, - } = payment; + const payment = get(data); + assert(payment); + + const { walletProvider } = useAppKitProvider('eip155'); + const browserProvider = new BrowserProvider(walletProvider as any); + const network = await browserProvider.getNetwork(); + const { chainId, chainName } = payment; const expectedChainId = getChainId(testing, chainId); - if (network.chainId !== expectedChainId) { - set( - error, - `We are expecting payments on ${chainName} but found ${network.name}. Change the network and try again.`, - ); + if (network.chainId !== BigInt(expectedChainId)) { + set(error, t('subscription.crypto_payment.invalid_chain', { actualName: network.name, chainName })); return; } - const signer = await browserProvider.getSigner(); - await executePayment(signer); + await executePayment(await browserProvider.getSigner(), payment); } catch (error_: any) { logger.error(error_); @@ -148,10 +154,23 @@ export function useWeb3Payment(data: Ref, getProvider: () set(state, 'idle'); } + function getNetwork(chainId?: number): AppKitNetwork { + const networks = testing ? testNetworks : productionNetworks; + const network = networks.find(network => network.id === chainId); + if (!network) { + return testing ? testNetworks[0] : productionNetworks[0]; + } + return network; + } + + onUnmounted(async () => { + await appKit.disconnect(); + }); + return { clearErrors, error, - payWithMetamask, + pay, state, }; } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index e69c8376..c790f797 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -568,9 +568,10 @@ "warning": "Changing the payment method after sending a payment can lead to problems with the activation of your subscription. Please only switch a plan if no payment has been sent.", "switch_agree": "I confirm that no payment has been sent. {separator}Allow me to change the payment method." }, - "metamask": { - "action": "Pay with Metamask", - "notice": "You can pay with metamask, your mobile wallet or manually send the exact amount to the following address above. Once the whole amount is sent and processed by our system, then a receipt will be sent to your email and your subscription will be activated.", + "wallet": { + "connect_wallet": "Connect wallet", + "pay_with_wallet": "Pay with wallet", + "notice": "You can pay your wallet wallet or manually send the exact amount to the following address above. Once the whole amount is sent and processed by our system, then a receipt will be sent to your email and your subscription will be activated.", "paid_notice_1": "If you already have made a transaction you don't need to do anything more.", "paid_notice_2": "You will be notified about your subscription via e-mail as soon as your transaction is confirmed." }, @@ -587,6 +588,9 @@ "description": "Are you sure you want to remove this card?" }, "expiry": "Expiry {expiresAt}" + }, + "wallets": { + "pay": "Pay with browser wallet" } } } @@ -629,6 +633,10 @@ "success": { "title": "Payment Success", "message": "Your payment was processed successfully. Visit the account management page to manage your account." + }, + "crypto_payment": { + "invalid_chain": "We are expecting payments on {chainName} but found ${actualName}. Change the network and try again.", + "not_connected": "No account is connected" } }, "selected_plan_overview": { diff --git a/modules/ui-library/runtime/plugin.ts b/modules/ui-library/runtime/plugin.ts index 536afcb0..cd224467 100644 --- a/modules/ui-library/runtime/plugin.ts +++ b/modules/ui-library/runtime/plugin.ts @@ -28,6 +28,7 @@ import { RiHandCoinLine, RiInformationLine, RiLightbulbLine, + RiLink, RiLockLine, RiLogoutBoxRLine, RiMailSendLine, @@ -97,6 +98,7 @@ export default defineNuxtPlugin((nuxtApp) => { RiCoinLine, RiPaypalLine, RiSearchLine, + RiLink, RiLockLine, ], mode: 'light', diff --git a/nuxt.config.ts b/nuxt.config.ts index 2a465e07..81df3c90 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -185,6 +185,9 @@ export default defineNuxtConfig({ siteKey: '', }, testing: false, + walletConnect: { + projectId: '', + }, }, }, diff --git a/package.json b/package.json index 4ee4cc2f..15865107 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "rotki.com", "version": "1.24.0", "private": true, - "packageManager": "pnpm@9.13.2", + "packageManager": "pnpm@9.14.2", "type": "module", "scripts": { "dev": "nuxi dev", @@ -25,20 +25,22 @@ "release": "bumpp -r --no-push" }, "optionalDependencies": { - "cypress": "13.15.2" + "cypress": "13.16.0" }, "dependencies": { "@metamask/detect-provider": "2.0.0", "@nuxtjs/robots": "4.1.11", "@nuxtjs/tailwindcss": "6.11.4", "@pinia/nuxt": "0.7.0", + "@reown/appkit": "1.5.0", + "@reown/appkit-adapter-ethers": "1.5.0", "@vuelidate/core": "2.0.3", "@vuelidate/validators": "2.0.4", - "@vueuse/core": "11.2.0", - "@vueuse/math": "11.2.0", - "@vueuse/nuxt": "11.2.0", - "@vueuse/shared": "11.2.0", - "braintree-web": "3.112.0", + "@vueuse/core": "11.3.0", + "@vueuse/math": "11.3.0", + "@vueuse/nuxt": "11.3.0", + "@vueuse/shared": "11.3.0", + "braintree-web": "3.112.1", "ethers": "6.13.4", "pinia": "2.2.6", "qrcode": "1.5.4", @@ -52,14 +54,14 @@ "@commitlint/config-conventional": "19.6.0", "@fontsource/roboto": "5.1.0", "@nuxt/content": "2.13.4", - "@nuxt/devtools": "1.6.0", + "@nuxt/devtools": "1.6.1", "@nuxt/image": "1.8.1", "@nuxt/test-utils": "3.14.4", "@nuxtjs/i18n": "9.1.0", "@nuxtjs/sitemap": "6.1.5", "@rotki/eslint-config": "3.5.0", "@rotki/eslint-plugin": "0.5.0", - "@rotki/ui-library": "1.6.0", + "@rotki/ui-library": "1.7.1", "@types/braintree-web": "3.96.15", "@types/paypal-checkout-components": "4.0.8", "@types/qrcode": "1.5.5", @@ -73,7 +75,7 @@ "husky": "9.1.7", "lint-staged": "15.2.10", "msw": "2.6.5", - "nuxt": "3.14.159", + "nuxt": "3.14.1592", "postcss": "8.4.49", "postcss-html": "1.7.0", "postcss-import": "16.1.0", @@ -86,7 +88,7 @@ "stylelint-config-standard": "36.0.1", "stylelint-config-standard-scss": "13.1.0", "stylelint-order": "6.0.4", - "stylelint-scss": "6.9.0", + "stylelint-scss": "6.10.0", "tailwindcss": "3.4.15", "typescript": "5.6.3", "vite": "5.4.11", diff --git a/pages/checkout/success.vue b/pages/checkout/success.vue index 5590af76..99e7701a 100644 --- a/pages/checkout/success.vue +++ b/pages/checkout/success.vue @@ -1,5 +1,6 @@
- {{ t('home.plans.tiers.step_3.metamask.paid_notice_1') }} + {{ t('home.plans.tiers.step_3.wallet.paid_notice_1') }}
- {{ t('home.plans.tiers.step_3.metamask.paid_notice_2') }} + {{ t('home.plans.tiers.step_3.wallet.paid_notice_2') }}