Skip to content

Commit

Permalink
feat: improve information when there is a pending tx
Browse files Browse the repository at this point in the history
  • Loading branch information
kelsos committed Nov 26, 2024
1 parent 606192f commit 0974516
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 24 deletions.
38 changes: 35 additions & 3 deletions components/account/home/SubscriptionTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { get, set, useIntervalFn } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { useMainStore } from '~/store';
import { PaymentMethod } from '~/types/payment';
import type { RouteLocationRaw } from 'vue-router';
import type {
ContextColorsType,
DataTableColumn,
DataTableSortColumn,
TablePaginationData,
} from '@rotki/ui-library';
import type { Subscription } from '~/types';
import type { PendingTx, Subscription } from '~/types';
const pagination = ref<TablePaginationData>();
const sort = ref<DataTableSortColumn<Subscription>[]>([]);
Expand All @@ -25,6 +26,7 @@ const store = useMainStore();
const { subscriptions } = storeToRefs(store);
const { cancelUserSubscription, resumeUserSubscription } = useSubscription();
const pendingTx = usePendingTx();
const { pause, resume, isActive } = useIntervalFn(
async () => await store.getAccount(),
60000,
Expand Down Expand Up @@ -111,6 +113,8 @@ const renewLink = computed<{ path: string; query: Record<string, string> }>(() =
return link;
});
const pendingPaymentLink: RouteLocationRaw = { path: '/checkout/pay/method' };
function isPending(sub: Subscription) {
return sub.status === 'Pending';
}
Expand Down Expand Up @@ -162,6 +166,12 @@ async function cancelSubscription(sub: Subscription) {
set(selectedSubscription, undefined);
}
function getBlockExplorerLink(pending: PendingTx): RouteLocationRaw {
return {
path: `${pending.blockExplorerUrl}/${pending.hash}`,
};
}
watch(pending, (pending) => {
if (pending.length === 0)
pause();
Expand Down Expand Up @@ -208,6 +218,11 @@ onUnmounted(() => pause());
</RuiTooltip>
<template v-else>
{{ row.status }}
<RuiProgress
v-if="pendingTx && row.identifier === pendingTx.subscriptionId"
thickness="2"
variant="indeterminate"
/>
</template>
</RuiChip>
</template>
Expand Down Expand Up @@ -235,10 +250,27 @@ onUnmounted(() => pause());
>
{{ t('actions.renew') }}
</ButtonLink>
<RuiTooltip v-if="pendingTx && row.identifier === pendingTx.subscriptionId">
<template #activator>
<ButtonLink
external
icon
color="primary"
:to="getBlockExplorerLink(pendingTx)"
>
<RuiIcon
name="links-line"
:size="18"
/>
</ButtonLink>
</template>
{{ t('account.subscriptions.pending_tx') }}
</RuiTooltip>
<!-- link will not work due to middleware if there is a transaction started -->
<ButtonLink
v-if="isPending(row)"
v-else-if="isPending(row)"
:disabled="cancelling"
:to="{ path: '/checkout/pay/method' }"
:to="pendingPaymentLink"
color="primary"
>
{{ t('account.subscriptions.payment_detail') }}
Expand Down
9 changes: 8 additions & 1 deletion components/checkout/pay/CryptoPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const { t } = useI18n();
const loading = ref(false);
const data = ref<CryptoPayment>();
const { cryptoPayment, switchCryptoPlan, deletePendingPayment, subscriptions } = useMainStore();
const {
cryptoPayment,
switchCryptoPlan,
deletePendingPayment,
subscriptions,
getAccount,
} = useMainStore();
const { plan } = usePlanParams();
const { currency } = useCurrencyParams();
Expand Down Expand Up @@ -110,6 +116,7 @@ onMounted(async () => {
set(loading, true);
const subId = get(subscriptionId);
const result = await cryptoPayment(selectedPlan, selectedCurrency, subId);
await getAccount();
if (result.isError) {
if (result.code === PaymentError.UNVERIFIED)
set(error, t('subscription.error.unverified_email'));
Expand Down
4 changes: 1 addition & 3 deletions components/checkout/pay/CryptoPaymentForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { get, set, useClipboard } from '@vueuse/core';
import { parseUnits } from 'ethers';
import { toCanvas } from 'qrcode';
import InputWithCopyButton from '~/components/common/InputWithCopyButton.vue';
import { getChainId } from '~/composables/crypto-payment';
import { toTitleCase, truncateAddress } from '~/utils/text';
import { useLogger } from '~/utils/use-logger';
import type { WatchHandle } from 'vue';
Expand Down Expand Up @@ -37,7 +36,6 @@ const showChangePaymentDialog = ref(false);
let stopWatcher: WatchHandle;
const { t } = useI18n();
const config = useRuntimeConfig();
const logger = useLogger('card-payment-form');
const appkitState = useAppKitState();
const { copy: copyToClipboard } = useClipboard({ source: qrText });
Expand Down Expand Up @@ -76,7 +74,7 @@ async function createPaymentQR(payment: CryptoPayment, canvas: HTMLCanvasElement
qrText = `bitcoin:${cryptoAddress}?amount=${finalPriceInCrypto}&label=Rotki`;
}
else {
const chainId = getChainId(config.public.testing, payment.chainId);
const chainId = payment.chainId;
const tokenAmount = parseUnits(finalPriceInCrypto, decimals);
if (!tokenAddress)
Expand Down
11 changes: 10 additions & 1 deletion components/common/ButtonLink.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { get } from '@vueuse/core';
import type { ContextColorsType } from '@rotki/ui-library';
import type { RouteLocationRaw } from 'vue-router';
defineOptions({
Expand All @@ -12,11 +13,17 @@ const props = withDefaults(defineProps<{
inline?: boolean;
highlightActive?: boolean;
highlightExactActive?: boolean;
color?: ContextColorsType;
icon?: boolean;
disabled?: boolean;
}>(), {
external: false,
inline: false,
highlightActive: false,
highlightExactActive: false,
color: undefined,
icon: undefined,
disabled: undefined,
});
const { highlightActive, highlightExactActive } = toRefs(props);
Expand All @@ -42,7 +49,9 @@ function getColor(active: boolean, exact: boolean) {
v-bind="{
variant: 'text',
type: 'button',
color: getColor(link?.isActive, link?.isExactActive),
color: color ?? getColor(link?.isActive, link?.isExactActive),
icon,
disabled,
...$attrs,
}"
:class="{ ['inline-flex py-0 !px-1 !text-[1em]']: inline }"
Expand Down
48 changes: 40 additions & 8 deletions composables/crypto-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BrowserProvider, Contract, type Signer, type TransactionResponse, parse
import { useMainStore } from '~/store';
import { assert } from '~/utils/assert';
import { useLogger } from '~/utils/use-logger';
import type { CryptoPayment, IdleStep, StepType } from '~/types';
import type { CryptoPayment, IdleStep, PendingTx, StepType } from '~/types';
import type { Ref } from 'vue';

const abi = [
Expand All @@ -29,19 +29,31 @@ const abi = [
'event Transfer(address indexed from, address indexed to, uint amount)',
];

export const getChainId = (testing: boolean, chainId?: string | number) => Number(chainId ?? (testing ? 11155111 : 1));

const testNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [sepolia, arbitrumSepolia, baseSepolia];
const productionNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, arbitrum, base];

export const usePendingTx = createSharedComposable(() => useLocalStorage<PendingTx>('rotki.pending_tx', null, {
serializer: {
read: (v: any): any => (v ? JSON.parse(v) : null),
write: (v: any): string => JSON.stringify(v),
},
}));

interface ExecutePaymentParams {
signer: Signer;
payment: CryptoPayment;
blockExplorerUrl: string;
}

export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
const { markTransactionStarted } = useMainStore();
const { getPendingSubscription, markTransactionStarted } = useMainStore();
const state = ref<StepType | IdleStep>('idle');
const error = ref('');

const logger = useLogger('web3-payment');
const { t } = useI18n();
const { public: { baseUrl, testing, walletConnect: { projectId } } } = useRuntimeConfig();
const pendingTx = usePendingTx();
const { start, stop } = useTimeoutFn(() => {
logger.info('change to done');
set(state, 'success');
Expand Down Expand Up @@ -76,7 +88,7 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
clearErrors();
});

async function executePayment(signer: Signer, payment: CryptoPayment): Promise<void> {
async function executePayment({ blockExplorerUrl, payment, signer }: ExecutePaymentParams): Promise<void> {
stop();
const {
cryptoAddress: to,
Expand Down Expand Up @@ -104,6 +116,19 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
}

logger.info(`transaction is pending: ${tx.hash}`);

const subscription = getPendingSubscription({
amount: payment.finalPriceInEur,
date: payment.startDate,
duration: payment.months,
});

set(pendingTx, {
blockExplorerUrl,
chainId: payment.chainId,
hash: tx.hash,
subscriptionId: subscription?.identifier,
});
await markTransactionStarted();
start();
}
Expand All @@ -128,14 +153,21 @@ export function useWeb3Payment(data: Ref<CryptoPayment | undefined>) {
const network = await browserProvider.getNetwork();

const { chainId, chainName } = payment;
const expectedChainId = getChainId(testing, chainId);
assert(chainId);

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

await executePayment(await browserProvider.getSigner(), payment);
const appKitNetwork = getNetwork(chainId);

const url = appKitNetwork.blockExplorers?.default.url;
await executePayment({
blockExplorerUrl: url ? `${url}/tx/` : '',
payment,
signer: await browserProvider.getSigner(),
});
}
catch (error_: any) {
logger.error(error_);
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
},
"no_subscriptions_found": "No subscriptions found",
"payment_detail": "Payment Details",
"pending_tx": "A transaction already exists for this subscription, please wait up to 7 minutes for our system to process the payment.",
"resume": {
"actions": {
"no": "No, don't resume",
Expand Down
2 changes: 2 additions & 0 deletions modules/ui-library/runtime/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
RiInformationLine,
RiLightbulbLine,
RiLink,
RiLinksLine,
RiLockLine,
RiLogoutBoxRLine,
RiMailSendLine,
Expand Down Expand Up @@ -100,6 +101,7 @@ export default defineNuxtPlugin((nuxtApp) => {
RiSearchLine,
RiLink,
RiLockLine,
RiLinksLine,
],
mode: 'light',
},
Expand Down
29 changes: 21 additions & 8 deletions store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { get, isClient, set, useTimeoutFn } from '@vueuse/core';
import { FetchError } from 'ofetch';
import { acceptHMRUpdate, defineStore } from 'pinia';
import { useLogger } from '~/utils/use-logger';
import {
Account,
ActionResultResponse,
Expand All @@ -28,13 +27,10 @@ import { PaymentError } from '~/types/codes';
import { fetchWithCsrf } from '~/utils/api';
import { assert } from '~/utils/assert';
import { formatSeconds } from '~/utils/text';
import { useLogger } from '~/utils/use-logger';
import type { LoginCredentials } from '~/types/login';
import type { ActionResult } from '~/types/common';
import type {
DeleteAccountPayload,
PasswordChangePayload,
ProfilePayload,
} from '~/types/account';
import type { DeleteAccountPayload, PasswordChangePayload, ProfilePayload } from '~/types/account';
import type { ComposerTranslation } from 'vue-i18n';

const SESSION_TIMEOUT = 3600000;
Expand Down Expand Up @@ -446,10 +442,26 @@ export const useMainStore = defineStore('main', () => {
}
};

function getPendingSubscription({ amount, date, duration }: {
amount: string;
duration: number;
date: number;
}): Subscription | undefined {
const subDate = new Date(date * 1000);
return get(subscriptions).find((subscription) => {
const [day, month, year] = subscription.createdDate.split('/').map(Number);
const createdDate = new Date(year, month - 1, day);
return subscription.status === 'Pending'
&& subscription.durationInMonths === duration
&& subscription.nextBillingAmount === amount
&& createdDate.toDateString() === subDate.toDateString();
});
}

const markTransactionStarted = async (): Promise<Result<boolean>> => {
try {
const response = await fetchWithCsrf<ActionResultResponse>(
'webapi/payment/pending',
'webapi/payment/pending/',
{
method: 'PATCH',
},
Expand Down Expand Up @@ -480,7 +492,7 @@ export const useMainStore = defineStore('main', () => {
const deletePendingPayment = async (): Promise<Result<boolean>> => {
try {
const response = await fetchWithCsrf<ActionResultResponse>(
'webapi/payment/pending',
'webapi/payment/pending/',
{
method: 'DELETE',
},
Expand Down Expand Up @@ -580,6 +592,7 @@ export const useMainStore = defineStore('main', () => {
deleteAccount,
deletePendingPayment,
getAccount,
getPendingSubscription,
getPlans,
login,
logout,
Expand Down
7 changes: 7 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,10 @@ export const ResendVerificationResponse = z.object({
});

export type ResendVerificationResponse = z.infer<typeof ResendVerificationResponse>;

export interface PendingTx {
hash: string;
subscriptionId: string;
chainId: number;
blockExplorerUrl: string;
}

0 comments on commit 0974516

Please sign in to comment.