Skip to content

Commit

Permalink
feat: add TransactionStatus component and use it for cart
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Sep 2, 2021
1 parent 3be655a commit b3eb05d
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 12 deletions.
114 changes: 114 additions & 0 deletions app/src/components/TransactionStatus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<!-- Transaction hash -->
<div :class="[...rowClasses, 'border-t', 'border-b', 'border-grey-100']">
<div class="mr-4 w-28">Hash:</div>
<a :href="etherscanUrl" class="link text-grey-400">{{ hash }}</a>
</div>
<!-- Status -->
<div :class="[...rowClasses, 'border-b', 'border-grey-100']">
<div class="mr-4 w-28">Status:</div>
<div v-if="status === 'pending'" class="text-grey-500">
<span class="border-2 p-4 mr-8">Pending</span>
Pending for {{ timeString }}
</div>
<div v-else-if="status === 'success'" class="text-grey-500">
<span class="text-teal border-teal border-2 p-4 mr-8">Success</span>
Confirmed in {{ timeString }}
</div>
<div v-else-if="status === 'failed'" class="text-grey-500">
<span class="text-pink border-pink border-2 p-4 mr-8 px-8">Fail</span>
Failed after {{ timeString }}
</div>
</div>
<!-- Copy + button -->
<div :class="[...rowClasses, 'justify-between']">
<div v-if="status === 'pending'" class="text-grey-500">Your transaction is pending.</div>
<div v-else-if="status === 'success'" class="text-grey-500">Transaction successful!</div>
<div v-else-if="status === 'failed'" class="text-grey-500">
Something went wrong. Please check the transaction and try again.
</div>
<button
v-if="label"
@click="action ? action : () => ({})"
class="btn"
:class="{ disabled: status === 'pending' }"
:disabled="status === 'pending'"
>
{{ label }}
</button>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, ref, SetupContext, watch } from 'vue';
import { getEtherscanUrl } from 'src/utils/utils';
import useWalletStore from 'src/store/wallet';
const emittedEventName = 'onReceipt'; // emitted once we receive the transaction receipt
const rowClasses = [
// styles applied to all rows in the HTML template
'flex',
'justify-start',
'items-center',
'text-grey-400',
'text-left',
'py-10',
'px-4',
'sm:px-6',
'lg:px-8',
];
function useTransactionStatus(hash: string, context: SetupContext<'onReceipt'[]>) {
// Transaction status management
const status = ref<'pending' | 'success' | 'failed'>('pending'); // available states
const emitTxReceipt = (success: boolean) => context.emit(emittedEventName, success); // emit event when we get receipt
// UI timer management
const timer = ref(1); // number of seconds transaction has been pending for
const timeString = computed(() => (timer.value === 1 ? '1 second' : `${timer.value} seconds`));
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // for updating timer each second
watch(
() => timer.value,
async () => {
// Update timer every second when transaction is pending
if (status.value !== 'pending') return;
await sleep(1000);
timer.value += 1;
},
{ immediate: true }
);
// Etherscan URL helpers
const chainId = ref(1); // default chainId is mainnet
const etherscanUrl = computed(() => getEtherscanUrl(hash, chainId.value));
// On mount, fetch receipt and wait for it to be mined, and emit event with receipt status once mined
onMounted(async () => {
const { provider } = useWalletStore();
chainId.value = (await provider.value.getNetwork()).chainId;
const receipt = await provider.value.waitForTransaction(hash);
status.value = receipt.status === 1 ? 'success' : 'failed';
emitTxReceipt(Boolean(receipt.status));
});
return { etherscanUrl, status, timeString };
}
export default defineComponent({
name: 'TransactionStatus',
emits: [emittedEventName],
props: {
hash: { type: String, required: true }, // transaction hash
buttonLabel: { type: String, required: false, default: undefined }, // if true, show button with this label
buttonAction: { type: Function, required: false, default: undefined }, // if buttonLabel is present, execute this method when button is clicked
},
setup(props, context) {
return {
action: props.buttonAction,
label: props.buttonLabel,
rowClasses,
...useTransactionStatus(props.hash, context),
};
},
});
</script>
48 changes: 36 additions & 12 deletions app/src/views/Cart.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
<template>
<!-- Header -->
<BaseHeader :name="`My Cart (${cart.length})`" />

<!-- Empty cart -->
<div v-if="cart.length === 0">
<div>Your cart is empty</div>
<button @click="pushRoute({ name: 'dgrants' })" class="btn btn-primary mt-6">Browse Grants</button>
<div v-if="!txHash && cart.length === 0">
<div class="mt-10">Your cart is empty</div>
<button @click="pushRoute({ name: 'dgrants' })" class="btn btn-primary mx-auto mt-6">Browse Grants</button>
</div>

<!-- Cart has items -->
<div v-else>
<!-- Cart has items and no checkout transaction -->
<div v-else-if="!txHash">
<BaseHeader :name="`My Cart (${cart.length})`" />
<!-- Cart toolbar -->
<div
class="flex justify-between max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 border-b border-grey-100 text-grey-400"
Expand Down Expand Up @@ -97,20 +95,33 @@
<span class="text-grey-300">Estimated matching value:</span> TODO USD
</div>
<div class="py-8 flex justify-end">
<button @click="checkout" class="btn">Checkout</button>
<button @click="executeCheckout" class="btn">Checkout</button>
</div>
</div>
</div>

<!-- Checkout transaction is pending -->
<div v-else-if="txHash">
<!-- We use a -1px bottom margin so overlapping borders of BaseHeader and TransactionStatus don't make the border thicker -->
<BaseHeader name="Checkout Transaction Status" style="margin-bottom: -1px" />
<TransactionStatus
@onReceipt="completeCheckout"
:hash="txHash"
buttonLabel="Home"
:buttonAction="() => pushRoute({ name: 'Home' })"
/>
</div>
</template>

<script lang="ts">
// --- External Imports ---
import { defineComponent, onMounted } from 'vue';
import { defineComponent, onMounted, ref } from 'vue';
import { ArrowToprightIcon, CloseIcon } from '@fusion-icons/vue/interface';
// --- App Imports ---
import BaseHeader from 'src/components/BaseHeader.vue';
import BaseInput from 'src/components/BaseInput.vue';
import BaseSelect from 'src/components/BaseSelect.vue';
import TransactionStatus from 'src/components/TransactionStatus.vue';
// --- Store ---
import useCartStore from 'src/store/cart';
import useDataStore from 'src/store/data';
Expand All @@ -120,13 +131,26 @@ import { pushRoute } from 'src/utils/utils';
function useCart() {
const { cart, cartSummaryString, removeFromCart, clearCart, initializeCart, updateCart, checkout } = useCartStore(); // prettier-ignore
onMounted(() => initializeCart()); // make sure cart store is initialized
return { cart, updateCart, clearCart, removeFromCart, cartSummaryString, checkout };
const txHash = ref<string>();
const status = ref<'not started' | 'pending' | 'success' | 'failure'>('pending');
async function executeCheckout() {
const tx = await checkout();
txHash.value = tx.hash;
}
function completeCheckout(success: boolean) {
if (success) clearCart();
}
return { txHash, status, cart, updateCart, clearCart, removeFromCart, cartSummaryString, executeCheckout, completeCheckout }; // prettier-ignore
}
export default defineComponent({
name: 'Cart',
components: { BaseHeader, BaseInput, BaseSelect, ArrowToprightIcon, CloseIcon },
components: { BaseHeader, BaseInput, BaseSelect, TransactionStatus, ArrowToprightIcon, CloseIcon },
setup() {
const { grantMetadata } = useDataStore();
const NOT_IMPLEMENTED = (msg: string) => window.alert(`NOT IMPLEMENTED: ${msg}`);
Expand Down

0 comments on commit b3eb05d

Please sign in to comment.