Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5921 payment flow modification #1113

Merged
merged 10 commits into from
Dec 20, 2023
572 changes: 126 additions & 446 deletions frontend/marketplace/package-lock.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions frontend/marketplace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
"@apollo/client": "^3.8.7",
"@empower-plastic/empowerjs": "0.0.18",
"@keplr-wallet/types": "^0.12.44",
"@logto/vue": "^2.1.0",
"@logto/vue": "^2.1.1",
"@vue/apollo-composable": "^4.0.0-beta.5",
"@vue/apollo-option": "^4.0.0-beta.9",
"@vuepic/vue-datepicker": "^7.2.2",
"daisyui": "^4.4.4",
"daisyui": "^4.4.20",
"graphql": "^16.8.1",
"graphql-tag": "^2.12.6",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.7.1",
"qrcode": "^1.5.3",
"rollbar": "^2.26.2",
"vue": "^3.3.2",
"vue": "^3.3.12",
"vue-awesome-paginate": "^1.1.46",
"vue-multiselect": "^3.0.0-beta.2",
"vue-router": "^4.2.5",
Expand Down Expand Up @@ -57,13 +57,16 @@
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"tailwindcss": "^3.3.5",
"typescript": "~5.3.2",
"vite": "^5.0.5",
"typescript": "~5.3.3",
"vite": "^5.0.10",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
"vue-tsc": "^1.8.25"
},
"engines": {
"npm": ">=10.2.3",
"node": ">=20.10.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.9.1"
}
}
22 changes: 16 additions & 6 deletions frontend/marketplace/src/components/BuyButton.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref } from "vue";
import { useAuth } from "@/stores/auth";
import { isValidCreditAmount } from "@/utils/utils";
import CertificateHolderModal from "@/components/CertificateHolderModal.vue";
export interface BuyButtonProps {
showButtonSpinner: boolean;
insufficientBalance: boolean;
coinFormatted: string;
handleCardPayment: () => void;
handleBuyCredits: () => void;
handleCardPayment: (name: string) => void;
handleBuyCredits: (name: string) => void;
isWalletConnected: boolean;
availableCredits: number;
buyingCreditAmount: number;
}
const props = defineProps<BuyButtonProps>();

const { isAuthenticated, handleSignIn } = useAuth();
const modalEl = ref<HTMLDialogElement | null>(null);
const continueHandler = ref<((name: string) => void) | undefined>(undefined);

enum BuyButtonState {
DISABLED = "disabled",
Expand Down Expand Up @@ -68,12 +70,19 @@ const buttonText = computed(() => {
}
});

const addModalToHandler = (newContinueHandler: (name: string) => void) => {
return () => {
continueHandler.value = newContinueHandler;
modalEl.value?.show();
};
};

const buttonHandler = computed<(() => void) | undefined>(() => {
switch (buttonState.value) {
case BuyButtonState.ENABLED_CARD:
return props.handleCardPayment;
return addModalToHandler(props.handleCardPayment);
case BuyButtonState.ENABLED_WALLET:
return props.handleBuyCredits;
return addModalToHandler(props.handleBuyCredits);
case BuyButtonState.ENABLED_UNAUTHORIZED:
return handleSignIn;
default:
Expand Down Expand Up @@ -114,4 +123,5 @@ const buttonsCssClasses = `
<span v-if="showButtonSpinner" class="loading loading-spinner"></span>
{{ buttonText }}
</button>
<CertificateHolderModal ref="modalEl" @continue="continueHandler" />
</template>
29 changes: 25 additions & 4 deletions frontend/marketplace/src/components/BuyCredits.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useFetcher, authHeader } from "@/utils/fetcher";
import { useAuth } from "@/stores/auth";
import { useWallet } from "@/stores/wallet";
import { useNotifyer } from "@/utils/notifyer";
import { useRetireCredits } from "@/utils/plastic-credits";

export interface BuyCreditsProps {
availableCredits: number;
Expand All @@ -29,7 +30,7 @@ export interface BuyCreditsProps {
}

const { isAuthenticated, getAccessToken } = useAuth();
const { isWalletConnected } = useWallet();
const { isWalletConnected, address: walletAddress } = useWallet();
const { notifyer } = useNotifyer();
const amount = ref<number>(1);
const props = defineProps<BuyCreditsProps>();
Expand All @@ -40,6 +41,7 @@ const currentBalance = ref(Number.MAX_SAFE_INTEGER);
const availableCreditsString = computed<string>(() => {
return `${props.availableCredits}/${props.initialCredits}`;
});
const retireCredits = useRetireCredits();

watch(isWalletConnected, async (newVal) => {
if (newVal === true) {
Expand Down Expand Up @@ -89,8 +91,8 @@ const checkBalanceForPurchase = (amount: number) => {
}
};

const handleBuyCredits = async () => {
if (!isWalletConnected.value) {
const handleBuyCredits = async (retirererName: string) => {
if (!isWalletConnected.value || !walletAddress.value) {
notifyer.error("Please connect to wallet");
return;
}
Expand Down Expand Up @@ -143,6 +145,24 @@ const handleBuyCredits = async () => {
} finally {
showButtonSpinner.value = false;
}

try {
await retireCredits.handleRetireCredits(
amount.value,
walletAddress.value,
props.denom,
retirererName,
() => {
notifyer.success(
"Retired credits successfully and generated a certificate",
);
},
);
notifyer.success("Retire credits successfully");
} catch (error) {
notifyer.error("Retire credits failed: " + resolveSdkError(error));
throw new Error("Error retiring credits, " + error);
}
};

const checkIfCreditsAvailable = () => {
Expand All @@ -153,7 +173,7 @@ const checkIfCreditsAvailable = () => {
return true;
};

const handleCardPayment = async () => {
const handleCardPayment = async (retirererName: string) => {
if (!checkIfCreditsAvailable()) {
return;
}
Expand All @@ -171,6 +191,7 @@ const handleCardPayment = async () => {
amount: amount.value,
denom: props.denom,
listingOwner: props.owner,
retirererName: retirererName,
};
try {
const accessToken = await getAccessToken(PC_BACKEND_ENDPOINT);
Expand Down
60 changes: 60 additions & 0 deletions frontend/marketplace/src/components/CertificateHolderModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import Modal from "@/components/ui/Modal.vue";
import PrimaryButton from "@/components/ui/PrimaryButton.vue";

const emit = defineEmits(["continue"]);
const baseModal = ref<HTMLDialogElement | null>(null);
const certificateHolder = ref<string>("");
const isCertificateHolderValid = computed<boolean>(() => {
return (
certificateHolder.value.length > 0 && certificateHolder.value.length < 31
);
});
const isCertificateHolderTooLong = computed<boolean>(() => {
return certificateHolder.value.length > 30;
});
const handleOpenModal = () => {
baseModal.value?.show();
};

const handleContinue = () => {
emit("continue", certificateHolder.value);
baseModal.value?.close();
};

defineExpose({
show: handleOpenModal,
});
</script>
<template>
<Modal ref="baseModal" title="Certificate holder">
<template v-slot:body>
<p class="py-4">
Provide an individual or company name that is going to be used for the
Plastic Credit Offset Certificate.
<b>Mind that this cannot be changed later.</b>
</p>
<div>
<input
type="text"
placeholder="Certificate holder name"
class="input input-bordered w-full text-black"
v-model="certificateHolder"
/>
<div class="label" v-if="isCertificateHolderTooLong">
<span class="label-text-alt text-white"
>Name can not be more than 30 characters.</span
>
</div>
</div>
</template>
<template v-slot:actions>
<PrimaryButton
@click.prevent="handleContinue"
:disabled="!isCertificateHolderValid"
>Continue</PrimaryButton
>
</template>
</Modal>
</template>
41 changes: 41 additions & 0 deletions frontend/marketplace/src/components/ui/Modal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref, defineExpose } from "vue";
interface Props {
title?: string;
}
const props = defineProps<Props>();
const isOpen = ref(false);
const dialog = ref<HTMLDialogElement>();

const show = () => {
isOpen.value = true;
dialog.value?.showModal();
};

const close = () => {
isOpen.value = false;
dialog.value?.close();
};

defineExpose({
show,
close,
});
</script>

<template>
<dialog ref="dialog" id="modal_dialog" class="modal">
<div class="modal-box bg-modalBackground">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<h3 v-if="props.title" class="font-bold text-lg">{{ props.title }}</h3>
<slot name="body" />
<div class="modal-action">
<slot name="actions"></slot>
</div>
</div>
</dialog>
</template>
16 changes: 16 additions & 0 deletions frontend/marketplace/src/components/ui/PrimaryButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { ButtonHTMLAttributes, ReservedProps } from "vue";
interface Props
extends /* @vue-ignore */ ButtonHTMLAttributes,
/* @vue-ignore */ ReservedProps {}

const props = defineProps<Props>();
</script>
<template>
<button
v-bind="props"
class="btn btn-ghost text-white btn-block normal-case bg-greenPrimary hover:bg-greenDark text-title24 lg:text-title32 lg:btn-lg p-0 px-12 font-normal disabled:bg-lightGray disabled:text-white"
>
<slot />
</button>
</template>
63 changes: 63 additions & 0 deletions frontend/marketplace/src/utils/plastic-credits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ref } from "vue";
import {
empowerchain,
getSigningTM37EmpowerchainClient,
} from "@empower-plastic/empowerjs";
import { getWallet } from "@/utils/wallet-utils";
import { CHAIN_ID, RPC_ENDPOINT } from "@/config/config";

const { retireCredits } =
empowerchain.plasticcredit.MessageComposer.withTypeUrl;

export const useRetireCredits = () => {
const loading = ref(false);

const handleRetireCredits = async (
amount: any, // Typing, Johannes?!
address: string,
denom: string,
retirererName: string,
onSuccess: () => void,
additionalInfo?: string,
) => {
try {
loading.value = true;
const retireCreditsMsg = retireCredits({
owner: address,
denom: denom,
amount: amount as bigint,
retiringEntityName: retirererName,
retiringEntityAdditionalData: additionalInfo ?? "",
});
const wallet = getWallet();
const offlineSigner = wallet.getOfflineSigner(CHAIN_ID);
const chainClient = await getSigningTM37EmpowerchainClient({
rpcEndpoint: RPC_ENDPOINT,
signer: offlineSigner,
});
const fee = {
amount: [{ amount: "100000", denom: "umpwr" }],
gas: "200000",
};
const response = await chainClient.signAndBroadcast(
address,
[retireCreditsMsg],
fee,
);
if (response && !response.code) {
loading.value = false;
onSuccess();
} else {
throw new Error("Retire credits failed " + response.rawLog);
}
} catch (error) {
loading.value = false;
throw error;
}
};

return {
handleRetireCredits,
loading,
};
};