Skip to content

Commit

Permalink
fix: improve the wallet connection experience
Browse files Browse the repository at this point in the history
  • Loading branch information
kelsos committed Nov 27, 2024
1 parent 12dd9a9 commit 3823661
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 62 deletions.
27 changes: 11 additions & 16 deletions components/checkout/pay/CryptoPage.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script lang="ts" setup>
import { useAppKit, useAppKitAccount } from '@reown/appkit/vue';
import { get, set } from '@vueuse/core';
import { useMainStore } from '~/store';
import { PaymentError } from '~/types/codes';
import { PaymentMethod } from '~/types/payment';
import { assert } from '~/utils/assert';
import type { CryptoPayment, PaymentStep } from '~/types';
import type { CryptoPayment, IdleStep, PaymentStep, StepType } from '~/types';
const { t } = useI18n();
const loading = ref(false);
const loading = ref<boolean>(false);
const data = ref<CryptoPayment>();
const error = ref<string>('');
const paymentState = ref<StepType | IdleStep>('idle');
const {
cryptoPayment,
Expand All @@ -24,18 +25,15 @@ const { plan } = usePlanParams();
const { currency } = useCurrencyParams();
const { subscriptionId } = useSubscriptionIdParam();
const route = useRoute();
const { pay, state: currentState, error, clearErrors } = useWeb3Payment(data);
const account = useAppKitAccount();
const { open } = useAppKit();
const step = computed<PaymentStep>(() => {
const errorMessage = get(error);
const state = get(currentState);
if (errorMessage) {
const message = get(error);
const state = get(paymentState);
if (message) {
return {
type: 'failure',
title: t('subscription.error.payment_failure'),
message: errorMessage,
message,
closeable: true,
};
}
Expand Down Expand Up @@ -159,16 +157,13 @@ onMounted(async () => {
</div>
<CryptoPaymentForm
v-else-if="data && plan"
v-bind="{ success, failure, status, pending }"
v-model:error="error"
v-model:state="paymentState"
:data="data"
:pending="pending || currentState === 'pending'"
v-bind="{ success, failure, status }"
:loading="loading"
:plan="plan"
:connected="account.isConnected"
@pay="pay()"
@connect="open()"
@change="changePaymentMethod()"
@clear:errors="clearErrors()"
/>

<div
Expand Down
33 changes: 21 additions & 12 deletions components/checkout/pay/CryptoPaymentForm.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useAppKitState } from '@reown/appkit/vue';
import { get, set, useClipboard } from '@vueuse/core';
import { parseUnits } from 'ethers';
import { toCanvas } from 'qrcode';
import InputWithCopyButton from '~/components/common/InputWithCopyButton.vue';
import { toTitleCase, truncateAddress } from '~/utils/text';
import { useLogger } from '~/utils/use-logger';
import type { WatchHandle } from 'vue';
import type { CryptoPayment, PaymentStep } from '~/types';
import type { CryptoPayment, IdleStep, PaymentStep, StepType } from '~/types';
const error = defineModel<string>('error', { required: true });
const state = defineModel<StepType | IdleStep>('state', { required: true });
const props = defineProps<{
data: CryptoPayment;
Expand All @@ -16,15 +19,11 @@ const props = defineProps<{
success: boolean;
failure: boolean;
loading: boolean;
connected: boolean;
status: PaymentStep;
}>();
const emit = defineEmits<{
(e: 'change'): void;
(e: 'pay'): void;
(e: 'connect'): void;
(e: 'clear:errors'): void;
}>();
const { data, pending, loading, success } = toRefs(props);
Expand All @@ -37,8 +36,8 @@ let stopWatcher: WatchHandle;
const { t } = useI18n();
const logger = useLogger('card-payment-form');
const appkitState = useAppKitState();
const { copy: copyToClipboard } = useClipboard({ source: qrText });
const { connected, pay, isOpen, open, isExpectedChain, switchNetwork } = useWeb3Payment(data, state, error);
const isBtc = computed<boolean>(() => get(data).chainName === 'bitcoin');
Expand Down Expand Up @@ -105,7 +104,7 @@ watch(canvas, async (canvas) => {
<div :class="$style.wrapper">
<div :class="$style.qrcode">
<canvas
v-if="!appkitState.open"
v-if="!isOpen"
ref="canvas"
@click="copyToClipboard(qrText)"
/>
Expand Down Expand Up @@ -207,27 +206,37 @@ watch(canvas, async (canvas) => {
:disabled="processing"
size="lg"
class="w-full"
@click="emit('connect')"
@click="open()"
>
{{ t('home.plans.tiers.step_3.wallet.connect_wallet') }}
</RuiButton>
<template v-else>
<RuiButton
v-if="isExpectedChain"
:loading="processing"
:disabled="processing"
color="primary"
size="lg"
class="w-full"
@click="emit('pay')"
@click="pay()"
>
{{ t('home.plans.tiers.step_3.wallet.pay_with_wallet') }}
</RuiButton>
<RuiButton
v-else
color="primary"
size="lg"
class="w-full"
@click="switchNetwork()"
>
{{ t('home.plans.tiers.step_3.wallet.switch_network') }}
</RuiButton>

<RuiButton
size="lg"
color="secondary"
class="!px-3"
@click="emit('connect')"
@click="open()"
>
<RuiIcon
name="link"
Expand All @@ -243,7 +252,7 @@ watch(canvas, async (canvas) => {
:timeout="10000"
closeable
:visible="failure"
@dismiss="emit('clear:errors')"
@dismiss="state = 'idle'"
>
<template #title>
{{ status?.title }}
Expand Down
84 changes: 60 additions & 24 deletions composables/crypto-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
optimism,
sepolia,
} from '@reown/appkit/networks';
import { createAppKit, useAppKitAccount, useAppKitProvider } from '@reown/appkit/vue';
import { createAppKit, useAppKitProvider } from '@reown/appkit/vue';
import { ChainController } from '@reown/appkit-core';
import { get, set, useTimeoutFn } from '@vueuse/core';
import { BrowserProvider, Contract, type Signer, type TransactionResponse, parseUnits } from 'ethers';
import { useMainStore } from '~/store';
Expand All @@ -19,6 +20,11 @@ import { useLogger } from '~/utils/use-logger';
import type { CryptoPayment, IdleStep, PendingTx, StepType } from '~/types';
import type { Ref } from 'vue';

// Patch the showUnsupportedChainUI method to no-op
ChainController.showUnsupportedChainUI = function () {
// No operation
};

const abi = [
// Some details about the token
'function name() view returns (string)',
Expand Down Expand Up @@ -47,10 +53,11 @@ interface ExecutePaymentParams {
blockExplorerUrl: string;
}

export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
export function useWeb3Payment(data: Ref<CryptoPayment>, state: Ref<StepType | IdleStep>, errorMessage: Ref<string>) {
const { getPendingSubscription, markTransactionStarted } = useMainStore();
const state = ref<StepType | IdleStep>('idle');
const error = ref('');
const connected = ref<boolean>(false);
const isOpen = ref<boolean>(false);
const connectedChainId = ref<bigint>();

const logger = useLogger('web3-payment');
const { t } = useI18n();
Expand All @@ -61,13 +68,11 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
set(state, 'success');
}, 5000, { immediate: false });

const account = useAppKitAccount();
const defaultNetwork = getNetwork(get(data)?.chainId);
const defaultNetwork = getNetwork(get(data).chainId);

const appKit = createAppKit({
adapters: [new EthersAdapter()],
allowUnsupportedChain: false,
defaultNetwork,
allowUnsupportedChain: true,
features: {
analytics: true,
email: false,
Expand All @@ -81,13 +86,36 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
name: 'Rotki',
url: baseUrl,
},
networks: testing ? testNetworks : productionNetworks,
networks: [defaultNetwork],
projectId,
themeMode: 'light',
});

appKit.subscribeAccount(() => {
clearErrors();
appKit.subscribeAccount((account) => {
set(state, 'idle');
set(connected, account.isConnected);

if (account.isConnected) {
const { walletProvider } = useAppKitProvider('eip155');
const browserProvider = new BrowserProvider(walletProvider as any);
browserProvider.getNetwork()
.then(network => set(connectedChainId, network.chainId))
.catch(logger.error);
}
else {
set(connectedChainId, undefined);
}
});

appKit.subscribeState((state) => {
set(isOpen, state.open);
});

const isExpectedChain = computed<boolean>(() => {
const paymentChainId = get(data).chainId;
if (!isDefined(connectedChainId) || !paymentChainId)
return false;
return get(connectedChainId) === BigInt(paymentChainId);
});

async function executePayment({ blockExplorerUrl, payment, signer }: ExecutePaymentParams): Promise<void> {
Expand Down Expand Up @@ -142,8 +170,8 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
set(state, 'pending');

try {
if (!get(account, 'isConnected')) {
set(error, t('subscription.crypto_payment.not_connected'));
if (!get(connected)) {
set(errorMessage, t('subscription.crypto_payment.not_connected'));
return;
}

Expand All @@ -158,7 +186,7 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
assert(chainId);

if (network.chainId !== BigInt(chainId)) {
set(error, t('subscription.crypto_payment.invalid_chain', { actualName: network.name, chainName }));
set(errorMessage, t('subscription.crypto_payment.invalid_chain', { actualName: network.name, chainName }));
return;
}

Expand All @@ -176,18 +204,13 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
set(state, 'idle');

if ('reason' in error_ && error_.reason)
set(error, error_.reason);
set(errorMessage, error_.reason);

else
set(error, error_.message);
set(errorMessage, error_.message);
}
};

function clearErrors() {
set(error, null);
set(state, 'idle');
}

function getNetwork(chainId?: number): AppKitNetwork {
const networks = testing ? testNetworks : productionNetworks;
const network = networks.find(network => network.id === chainId);
Expand All @@ -197,14 +220,27 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
return network;
}

function switchNetwork(): void {
const network = getNetwork(get(data).chainId);
appKit.switchNetwork(network);
}

watch(state, (state) => {
if (state === 'idle') {
set(errorMessage, '');
}
});

onUnmounted(async () => {
await appKit.disconnect();
});

return {
clearErrors,
error,
connected: readonly(connected),
isExpectedChain,
isOpen: readonly(isOpen),
open: async () => appKit.open(),
pay,
state,
switchNetwork,
};
}
21 changes: 11 additions & 10 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,9 @@
"most_popular": "Most popular",
"names": {
"for": "{name} for",
"monthly": "Monthly",
"numeric": "{months} Months",
"plan": "{name} Plan.",
"monthly": "Monthly",
"yearly": "Yearly"
},
"per_month": "per month",
Expand All @@ -429,14 +429,14 @@
"crypto_hint": "* When paying with crypto the monthly price is {cryptoPrice} €",
"description": "Choose the different plans we offer",
"maybe_vat": "VAT may apply depending on your jurisdiction",
"title": "Premium Plans",
"vat": "The prices include a +{vat}% VAT tax",
"notes": {
"line_1": "The selected payment method will be billed the total amount for the subscription immediately.",
"line_2": "New billing cycle starts at UTC midnight after the subscription has ran out.",
"line_3": "An invoice is generated for each payment and you can access it from your account page.",
"line_4": "Subscriptions can be canceled from the account page at any point in time."
}
},
"title": "Premium Plans",
"vat": "The prices include a +{vat}% VAT tax"
},
"step_2": {
"description": "Select one of the different methods",
Expand All @@ -457,6 +457,11 @@
"token": "Token",
"token_contract": "Token contract: "
},
"notes": {
"line_1": "Please submit a payment request. Once that is done an email will be sent to your account with a payment request of the equivalent € amount of that cryptocurrency based on the rates given by CryptoCompare",
"line_2": "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.",
"line_3": "Finally, when the end of the subscription period approaches an email with a prompt to generate another payment request will be sent."
},
"payment_description": "Payments are safely processed with Braintree a PayPal Service",
"saved_card": {
"cannot_use_card": "Something went wrong. Please refresh the page or use a different card.",
Expand All @@ -475,12 +480,8 @@
"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.",
"pay_with_wallet": "Pay with wallet"
},
"notes": {
"line_1": "Please submit a payment request. Once that is done an email will be sent to your account with a payment request of the equivalent € amount of that cryptocurrency based on the rates given by CryptoCompare",
"line_2": "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.",
"line_3": "Finally, when the end of the subscription period approaches an email with a prompt to generate another payment request will be sent."
"pay_with_wallet": "Pay with wallet",
"switch_network": "Switch Network"
}
}
},
Expand Down

0 comments on commit 3823661

Please sign in to comment.