initial
This commit is contained in:
24
apps/pay/.gitignore
vendored
Normal file
24
apps/pay/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
2
apps/pay/.npmrc
Normal file
2
apps/pay/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
12
apps/pay/app.vue
Normal file
12
apps/pay/app.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useHead({
|
||||
title: 'Indefiti',
|
||||
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
|
||||
})
|
||||
</script>
|
||||
11
apps/pay/app/router.options.ts
Normal file
11
apps/pay/app/router.options.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
|
||||
export default <RouterConfig> {
|
||||
routes: _routes => [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/:invoiceId',
|
||||
component: () => import('~/pages/index.vue').then(r => r.default || r),
|
||||
},
|
||||
],
|
||||
}
|
||||
7
apps/pay/assets/styles.scss
Normal file
7
apps/pay/assets/styles.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#__nuxt {
|
||||
height: 100%;
|
||||
}
|
||||
88
apps/pay/components/invoice-form/amount.vue
Normal file
88
apps/pay/components/invoice-form/amount.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div :class="cn.b()">
|
||||
<p :class="cn.e('label')">
|
||||
To pay:
|
||||
</p>
|
||||
|
||||
<div :class="cn.e('left')">
|
||||
<span :class="cn.e('amount')">{{ amount }}</span>
|
||||
<UiCopyButton
|
||||
:class="cn.e('copy')"
|
||||
:title="null"
|
||||
:pressed-title="null"
|
||||
:value="amount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="cn.e('right')">
|
||||
<UiCoin
|
||||
:class="cn.e('coin')"
|
||||
:code="invoice.currencyCode"
|
||||
/>
|
||||
<span :class="cn.e('code')">{{ invoice.currencyCode }}</span>
|
||||
<span :class="cn.e('network')">TRC20</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const amount = computed(() => $money.format(invoice.value.amount, invoice.value.code))
|
||||
|
||||
const cn = useClassname('invoice-form-amount')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form-amount {
|
||||
display: grid;
|
||||
grid-template-areas: 'label label' 'left right';
|
||||
text-align: left;
|
||||
padding-inline: 16px;
|
||||
|
||||
&__label {
|
||||
@include txt-r-m;
|
||||
|
||||
grid-area: label;
|
||||
margin-bottom: 3px;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
@include h2;
|
||||
}
|
||||
|
||||
&__copy {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-area: right;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
&__code {
|
||||
@include font(16px, 600, 20px);
|
||||
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__network {
|
||||
@include txt-i-m;
|
||||
|
||||
padding: 3px 5px;
|
||||
border-radius: 6px;
|
||||
background-color: $clr-cyan-500;
|
||||
color: $clr-white;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
apps/pay/components/invoice-form/details.vue
Normal file
122
apps/pay/components/invoice-form/details.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<UiAccordion
|
||||
active-icon="disclosure-up"
|
||||
inactive-icon="disclosure-down"
|
||||
:model-value="accordion ? undefined : 'details'"
|
||||
:disabled="!accordion"
|
||||
>
|
||||
<UiAccordionItem id="details" :class="cn.b()">
|
||||
<template #title>
|
||||
<p :class="cn.e('title')">
|
||||
Payment details
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<dl :class="cn.e('list')">
|
||||
<!-- <dt>Date</dt> -->
|
||||
<!-- <dd>22 сент. 2022г., 11:27</dd> -->
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd :class="[cn.e('status-text'), invoice.status]">
|
||||
{{ statusText }}
|
||||
</dd>
|
||||
|
||||
<dt>Blockchain</dt>
|
||||
<dd>
|
||||
TRON
|
||||
</dd>
|
||||
|
||||
<dt>Sender</dt>
|
||||
<dd>
|
||||
<TextShortener :text="invoice.transfers?.[0]?.sender" />
|
||||
</dd>
|
||||
|
||||
<dt>Recipient</dt>
|
||||
<dd>
|
||||
<TextShortener :text="invoice.transfers?.[0]?.recipient" />
|
||||
</dd>
|
||||
|
||||
<dt>TXID</dt>
|
||||
<dd>
|
||||
<TextShortener :text="invoice.transfers?.[0]?.txID" />
|
||||
</dd>
|
||||
</dl>
|
||||
</template>
|
||||
</UiAccordionItem>
|
||||
</UiAccordion>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
accordion: Boolean,
|
||||
})
|
||||
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form-details')
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (invoice.value.status) {
|
||||
case 'completed':
|
||||
return 'Payment success'
|
||||
case 'underpaid':
|
||||
return 'Invoice has been partially paid'
|
||||
case 'overpaid':
|
||||
return 'Invoice has been over paid'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form-details {
|
||||
$self: &;
|
||||
|
||||
--accordion-head-padding: 16px;
|
||||
--accordion-head-active-padding: 16px;
|
||||
--accordion-content-padding: 0 16px 16px;
|
||||
|
||||
text-align: left;
|
||||
outline: none !important;
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background-color: $clr-grey-100;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include txt-l-m;
|
||||
}
|
||||
|
||||
&__list {
|
||||
@include txt-r-m;
|
||||
@include txt-r-m('text-shortener', true);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
row-gap: 16px;
|
||||
margin: 0;
|
||||
color: $clr-grey-500;
|
||||
|
||||
dt, dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: $clr-grey-600;
|
||||
}
|
||||
|
||||
dd {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&__status-text {
|
||||
color: $clr-warn-500;
|
||||
|
||||
&.completed {
|
||||
color: $clr-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
apps/pay/components/invoice-form/instructions.vue
Normal file
61
apps/pay/components/invoice-form/instructions.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div :class="cn.b()">
|
||||
<div :class="cn.e('header')">
|
||||
<p :class="cn.e('title')">
|
||||
Инструкция для оплаты
|
||||
</p>
|
||||
|
||||
<UiIconExclamationFilled :class="cn.e('icon')" />
|
||||
</div>
|
||||
|
||||
<p :class="cn.e('text')">
|
||||
Для оплаты скопируйте адрес в Ваш кошелек <br>
|
||||
или отсканируйте QR-код в приложении.
|
||||
<br><br>
|
||||
Далее введите в кошельке необходимую сумму <br>
|
||||
и подтвердите в этой форме зачисление.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const cn = useClassname('invoice-form-instructions')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form-instructions {
|
||||
$self: &;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: $clr-grey-100;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 16px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include txt-i-b;
|
||||
|
||||
color: $clr-black;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-r-m;
|
||||
|
||||
color: $clr-grey-600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
apps/pay/components/invoice-form/separator.vue
Normal file
35
apps/pay/components/invoice-form/separator.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="invoice-form-separator" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form-separator {
|
||||
position: relative;
|
||||
margin: 40px 20px;
|
||||
border: 0 dashed $clr-grey-300;
|
||||
border-top-width: 2px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: $clr-grey-100;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -60px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
apps/pay/components/invoice-form/step/expired.vue
Normal file
51
apps/pay/components/invoice-form/step/expired.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div :class="cn.m('expired')">
|
||||
<div class="p-16">
|
||||
<img
|
||||
src="/expired.svg"
|
||||
alt="Expired"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-grey-600">
|
||||
Payment has expired
|
||||
</h2>
|
||||
|
||||
<h4 class="text-clr-grey-400 mt-16">
|
||||
Your invoice has expired. If you want to make a payment, <br> please return to the site and create the invoice again.
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
|
||||
<InvoiceFormAmount />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('back-to-store')"
|
||||
size="large"
|
||||
:href="invoice.redirectUrl"
|
||||
>
|
||||
Back to store
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--expired {
|
||||
#{$self}__back-to-store {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
apps/pay/components/invoice-form/step/overpaid.vue
Normal file
95
apps/pay/components/invoice-form/step/overpaid.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div :class="cn.m('success')">
|
||||
<img
|
||||
src="/partially.svg"
|
||||
alt="Success"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-green-500">
|
||||
Invoice has been over paid
|
||||
</h2>
|
||||
|
||||
<div class="mt-16">
|
||||
<UiCoin :code="invoice.currencyCode" />
|
||||
|
||||
<MoneyAmount
|
||||
:class="cn.e('amount')"
|
||||
:value="invoice.amount"
|
||||
:currency="invoice.currencyCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InvoiceFormDetails :class="cn.e('details')" accordion />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('back-to-store')"
|
||||
size="large"
|
||||
:href="invoice.redirectUrl"
|
||||
>
|
||||
Back to store
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('explorer')"
|
||||
type="link"
|
||||
color="secondary"
|
||||
:href="explorerLink"
|
||||
target="_blank"
|
||||
>
|
||||
Просмотреть в обозревателе блоков
|
||||
</UiButton>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form')
|
||||
|
||||
const explorerLink = computed(() => {
|
||||
return `https://nile.tronscan.org/#/address/${invoice.value.walletAddress}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--success {
|
||||
#{$self}__amount {
|
||||
@include h2('money-amount-value', true);
|
||||
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#{$self}__transaction-id {
|
||||
text-align: center;
|
||||
color: $clr-grey-400;
|
||||
margin-top: 16px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
#{$self}__details {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__back-to-store {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__explorer {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
apps/pay/components/invoice-form/step/pending.vue
Normal file
49
apps/pay/components/invoice-form/step/pending.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="cn.m('pending')">
|
||||
<div class="p-16">
|
||||
<img
|
||||
src="/pending.svg"
|
||||
alt="Pending"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-grey-600">
|
||||
Waiting
|
||||
</h2>
|
||||
|
||||
<h4 class="text-clr-grey-400 mt-16">
|
||||
Searching for transaction in the Blockchain network…
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
|
||||
<InvoiceFormAmount />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('action')"
|
||||
size="large"
|
||||
disabled
|
||||
>
|
||||
I have paid
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const cn = useClassname('invoice-form')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--pending {
|
||||
#{$self}__action {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
apps/pay/components/invoice-form/step/requisites.vue
Normal file
67
apps/pay/components/invoice-form/step/requisites.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :class="cn.m('requisites')">
|
||||
<div
|
||||
class="d-flex"
|
||||
style="gap: 16px"
|
||||
>
|
||||
<InvoiceFormInstructions />
|
||||
|
||||
<UiQrCode
|
||||
:class="cn.e('qr')"
|
||||
:value="invoice.walletAddress"
|
||||
:size="139"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UiInput
|
||||
id="address"
|
||||
:class="cn.e('address')"
|
||||
label="Адрес кошелька для оплаты"
|
||||
:model-value="invoice.walletAddress"
|
||||
readonly
|
||||
copyable
|
||||
/>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
|
||||
<InvoiceFormAmount />
|
||||
|
||||
<UiButton
|
||||
class="action"
|
||||
size="large"
|
||||
:class="cn.e('action')"
|
||||
:disabled="payedFlag"
|
||||
@click="payedFlag = true"
|
||||
>
|
||||
I have paid
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { invoice, payedFlag } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--requisites {
|
||||
#{$self}__qr {
|
||||
width: 139px;
|
||||
height: 139px;
|
||||
}
|
||||
|
||||
#{$self}__address {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#{$self}__action {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
apps/pay/components/invoice-form/step/success.vue
Normal file
84
apps/pay/components/invoice-form/step/success.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div :class="cn.m('success')">
|
||||
<img
|
||||
src="/success.svg"
|
||||
alt="Success"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-green-500">
|
||||
Payment success
|
||||
</h2>
|
||||
|
||||
<div class="mt-16">
|
||||
<UiCoin :code="invoice.currencyCode" />
|
||||
|
||||
<MoneyAmount
|
||||
:class="cn.e('amount')"
|
||||
:value="invoice.amount"
|
||||
:currency="invoice.currencyCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InvoiceFormDetails :class="cn.e('details')" />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('back-to-store')"
|
||||
size="large"
|
||||
:href="invoice.redirectUrl"
|
||||
>
|
||||
Back to store
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('explorer')"
|
||||
type="link"
|
||||
color="secondary"
|
||||
:href="explorerLink"
|
||||
target="_blank"
|
||||
>
|
||||
Просмотреть в обозревателе блоков
|
||||
</UiButton>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form')
|
||||
|
||||
const explorerLink = computed(() => {
|
||||
return `https://nile.tronscan.org/#/address/${invoice.value.walletAddress}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--success {
|
||||
#{$self}__amount {
|
||||
@include h2('money-amount-value', true);
|
||||
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#{$self}__details {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__back-to-store {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__explorer {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
126
apps/pay/components/invoice-form/step/underpaid.vue
Normal file
126
apps/pay/components/invoice-form/step/underpaid.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div :class="cn.m('underpaid')">
|
||||
<img
|
||||
src="/partially.svg"
|
||||
alt="Success"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-grey-600">
|
||||
Invoice has been partially paid
|
||||
</h2>
|
||||
|
||||
<p :class="cn.e('total')">
|
||||
Total amount to be paid - <strong>{{ total }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="mt-32">
|
||||
<UiCoin :code="invoice.currencyCode" />
|
||||
|
||||
<div :class="cn.e('collected-amount')">
|
||||
<span>Funds received</span>
|
||||
|
||||
<MoneyAmount
|
||||
:value="invoice.collectedAmount"
|
||||
:currency="invoice.currencyCode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiAlert type="warning" :class="cn.e('alert')">
|
||||
You need to deposit the missing part of the funds - <span style="white-space: nowrap">{{ missingAmount }}</span>, otherwise the bill will not be paid
|
||||
</UiAlert>
|
||||
|
||||
<InvoiceFormDetails :class="cn.e('details')" accordion />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('pay-additional')"
|
||||
size="large"
|
||||
@click="notify({ type: 'warning', id: 'wip', text: 'Work in progress' })"
|
||||
>
|
||||
Pay in addition
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('explorer')"
|
||||
type="link"
|
||||
color="secondary"
|
||||
:href="explorerLink"
|
||||
target="_blank"
|
||||
>
|
||||
Просмотреть в обозревателе блоков
|
||||
</UiButton>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
const { invoice } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form')
|
||||
const notify = useNotify()
|
||||
|
||||
const total = computed(() => $money.fullFormat(invoice.value.amount, invoice.value.currencyCode))
|
||||
const missingAmount = computed(() => $money.fullFormat(Decimal.sub(invoice.value.amount, invoice.value.collectedAmount), invoice.value.currencyCode))
|
||||
const explorerLink = computed(() => {
|
||||
return `https://nile.tronscan.org/#/address/${invoice.value.walletAddress}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--underpaid {
|
||||
#{$self}__total {
|
||||
@include h4;
|
||||
|
||||
color: $clr-grey-400;
|
||||
margin-top: 4px;
|
||||
|
||||
strong {
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
#{$self}__collected-amount {
|
||||
@include h3('money-amount-value', true);
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
|
||||
> span {
|
||||
@include txt-r-m;
|
||||
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
#{$self}__alert {
|
||||
margin-top: 32px;
|
||||
padding-block: 16px;
|
||||
}
|
||||
|
||||
#{$self}__details {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__pay-additional {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
#{$self}__explorer {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
apps/pay/components/invoice-form/step/waiting-requisites.vue
Normal file
49
apps/pay/components/invoice-form/step/waiting-requisites.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="cn.m('waiting-requisites')">
|
||||
<div class="p-16">
|
||||
<img
|
||||
src="/pending.svg"
|
||||
alt="Waiting requisites"
|
||||
class="mb-16"
|
||||
data-status-illustration
|
||||
>
|
||||
|
||||
<h2 class="text-clr-grey-600">
|
||||
Waiting
|
||||
</h2>
|
||||
|
||||
<h4 class="text-clr-grey-400 mt-16">
|
||||
It may take several minutes to receive payment details
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<InvoiceFormSeparator />
|
||||
|
||||
<InvoiceFormAmount />
|
||||
|
||||
<UiButton
|
||||
:class="cn.e('action')"
|
||||
size="large"
|
||||
disabled
|
||||
>
|
||||
I have paid
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const cn = useClassname('invoice-form')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
$self: &;
|
||||
|
||||
&--waiting-requisites {
|
||||
#{$self}__action {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
apps/pay/components/invoice-form/timer.vue
Normal file
53
apps/pay/components/invoice-form/timer.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div :class="[cn.b(), cn.is('expired', isPassed)]">
|
||||
<UiIconClock :class="cn.e('icon')" />
|
||||
|
||||
<ClientOnly>
|
||||
<span :class="cn.e('time')">{{ formattedTime }}</span>
|
||||
|
||||
<template #fallback>
|
||||
<span :class="cn.e('time')">--:--</span>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { expirationTime } = inject('public-invoice')
|
||||
|
||||
const cn = useClassname('invoice-form-timer')
|
||||
|
||||
const { isPassed, remainingTime } = useTimer(expirationTime)
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
return new Date(remainingTime.value * 1000).toISOString().slice(14, 19)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form-timer {
|
||||
$self: &;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__icon {
|
||||
color: $clr-grey-400;
|
||||
margin-right: 8px;
|
||||
|
||||
#{$self}.is-expired & {
|
||||
color: $clr-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
@include txt-m-sb;
|
||||
|
||||
color: $clr-grey-600;
|
||||
|
||||
#{$self}.is-expired & {
|
||||
color: $clr-red-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
17
apps/pay/composables/api.ts
Normal file
17
apps/pay/composables/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { NitroFetchRequest } from 'nitropack'
|
||||
|
||||
export function $api<
|
||||
T = unknown,
|
||||
R extends NitroFetchRequest = NitroFetchRequest,
|
||||
>(
|
||||
request: Parameters<typeof $fetch<T, R>>[0],
|
||||
options?: Partial<Parameters<typeof $fetch<T, R>>[1]>,
|
||||
) {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
return $fetch<T, R>(request, {
|
||||
...options,
|
||||
retry: false,
|
||||
baseURL: runtimeConfig.public.apiHost as string,
|
||||
})
|
||||
}
|
||||
11
apps/pay/eslint.config.js
Normal file
11
apps/pay/eslint.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default await antfu({
|
||||
overrides: {
|
||||
vue: {
|
||||
'vue/block-order': ['error', {
|
||||
order: ['template', 'script', 'style'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
5
apps/pay/i18n.config.ts
Normal file
5
apps/pay/i18n.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
globalInjection: true,
|
||||
}))
|
||||
5
apps/pay/lang/en.js
Normal file
5
apps/pay/lang/en.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
privacy_policy: 'Privacy Policy',
|
||||
copyright: '© {year}, Indefiti - the best crypto processing',
|
||||
support: 'Support',
|
||||
}
|
||||
1
apps/pay/lang/ru.js
Normal file
1
apps/pay/lang/ru.js
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
121
apps/pay/layouts/default.vue
Normal file
121
apps/pay/layouts/default.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="default-layout">
|
||||
<header class="default-header">
|
||||
<NuxtLink to="/projects">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
>
|
||||
</NuxtLink>
|
||||
|
||||
<LangSwitcher />
|
||||
</header>
|
||||
|
||||
<main class="default-main">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="default-footer">
|
||||
<div class="default-footer__left">
|
||||
<UiButton
|
||||
type="link"
|
||||
color="secondary"
|
||||
href="#"
|
||||
>
|
||||
{{ $t('privacy_policy') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div class="default-footer__middle">
|
||||
<span class="default-footer__copyright">
|
||||
{{ $t('copyright', { year }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="default-footer__right">
|
||||
<UiButton
|
||||
icon="circle-question"
|
||||
type="link"
|
||||
color="secondary"
|
||||
href="#"
|
||||
>
|
||||
{{ $t('support') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
const year = computed(() => new Date().getFullYear())
|
||||
|
||||
const headerAction = computed(() => {
|
||||
switch (route.name) {
|
||||
case 'login':
|
||||
return {
|
||||
name: t('register'),
|
||||
link: '/register',
|
||||
}
|
||||
case 'register':
|
||||
return {
|
||||
name: t('login'),
|
||||
link: '/login',
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.default-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.default-header {
|
||||
display: flex;
|
||||
padding: 32px 56px 0 56px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.default-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-block: 30px;
|
||||
}
|
||||
|
||||
.default-footer {
|
||||
padding: 24px 56px 32px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__left {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__right {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
apps/pay/nuxt.config.ts
Normal file
45
apps/pay/nuxt.config.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
extends: ['../../layers/shared', '../../layers/ui'],
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
[
|
||||
'@nuxtjs/i18n',
|
||||
{
|
||||
vueI18n: './i18n.config.ts',
|
||||
lazy: true,
|
||||
langDir: 'lang',
|
||||
compilation: {
|
||||
strictMessage: false,
|
||||
},
|
||||
locales: [
|
||||
// {
|
||||
// code: 'ru',
|
||||
// name: 'Русский',
|
||||
// file: 'ru.js',
|
||||
// },
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
file: 'en.js',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
strategy: 'no_prefix',
|
||||
detectBrowserLanguage: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
css: ['~/assets/styles.scss', 'vue-final-modal/style.css'],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
host: process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3001'
|
||||
: 'https://pay.prgms.io',
|
||||
apiHost:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '/api'
|
||||
: 'https://api.prgms.io/api/v1',
|
||||
},
|
||||
},
|
||||
})
|
||||
35
apps/pay/package.json
Normal file
35
apps/pay/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "pay",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev --port 3001",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.4.11",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"decimal.js": "^10.4.3",
|
||||
"defu": "^6.1.2",
|
||||
"ufo": "^1.3.2",
|
||||
"ui-layer": "*",
|
||||
"shared-layer": "*",
|
||||
"uuid": "^9.0.1",
|
||||
"vue-final-modal": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.1.2",
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxtjs/i18n": "^8.0.0-rc.5",
|
||||
"eslint": "^8.54.0",
|
||||
"nuxt": "latest",
|
||||
"sass": "^1.69.0",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "^4.2.5"
|
||||
}
|
||||
}
|
||||
210
apps/pay/pages/index.vue
Normal file
210
apps/pay/pages/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="invoice-form">
|
||||
<div class="invoice-form__header">
|
||||
<div>
|
||||
<h2>Payment details</h2>
|
||||
|
||||
<p class="invoice-form__order-id">
|
||||
# {{ invoice.orderId }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InvoiceFormTimer
|
||||
:expiration-time="expirationTime"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="invoice-form__body">
|
||||
<Transition
|
||||
name="shift"
|
||||
mode="out-in"
|
||||
>
|
||||
<Component :is="stepComponent" />
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<p class="invoice-form__network-alert">
|
||||
Адрес работает только для получения Tether (USDT) в сети TRC20
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSessionStorage } from '@vueuse/core'
|
||||
import {
|
||||
InvoiceFormStepExpired,
|
||||
InvoiceFormStepPending,
|
||||
InvoiceFormStepRequisites,
|
||||
InvoiceFormStepSuccess,
|
||||
InvoiceFormStepUnderpaid,
|
||||
InvoiceFormStepWaitingRequisites,
|
||||
} from '#components'
|
||||
|
||||
definePageMeta({
|
||||
validate: async (route) => {
|
||||
try {
|
||||
await $api(`/payments/${route.params.invoiceId}`, {
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const payedFlag = useSessionStorage(`invoice-${route.params.invoiceId}`, false, { initOnMounted: true })
|
||||
|
||||
interface InvoiceTransfer {
|
||||
txID: string
|
||||
recipient: string
|
||||
sender: string
|
||||
blockchainCode: string
|
||||
}
|
||||
|
||||
interface PublicInvoice {
|
||||
id: string
|
||||
orderId: string
|
||||
amount: number | string
|
||||
collectedAmount: number | string
|
||||
currencyCode: string
|
||||
expiresAt: string
|
||||
blockchainCode?: string
|
||||
availableBlockchains: string[]
|
||||
walletAddress?: string
|
||||
transfers?: InvoiceTransfer[]
|
||||
}
|
||||
|
||||
const { data: invoice, refresh } = await useAsyncData<PublicInvoice>('public-invoice', async () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const invoice = await $api<PublicInvoice>(`/payments/${route.params.invoiceId}`, {
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
if (new Date(invoice.expiresAt) < new Date() || invoice.walletAddress)
|
||||
return invoice
|
||||
|
||||
try {
|
||||
const selectedBlockchain = await nuxtApp.runWithContext(async () => {
|
||||
return $api<Pick<PublicInvoice, 'expiresAt' | 'walletAddress'>>(`/payments/${route.params.invoiceId}`, {
|
||||
method: 'put',
|
||||
body: {
|
||||
blockchainCode: invoice!.availableBlockchains[0],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...invoice,
|
||||
...selectedBlockchain,
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
|
||||
return invoice
|
||||
}
|
||||
})
|
||||
|
||||
const expirationTime = computed(() => {
|
||||
return new Date(invoice.value?.expiresAt ?? 0).getTime()
|
||||
})
|
||||
|
||||
const { isPassed } = useTimer(expirationTime)
|
||||
|
||||
const stepComponent = computed(() => {
|
||||
const status = invoice.value?.status
|
||||
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return InvoiceFormStepWaitingRequisites
|
||||
|
||||
case 'awaiting_payment':
|
||||
if (!isPassed.value)
|
||||
return InvoiceFormStepRequisites
|
||||
else
|
||||
return InvoiceFormStepPending
|
||||
|
||||
case 'completed':
|
||||
return InvoiceFormStepSuccess
|
||||
|
||||
case 'underpaid':
|
||||
return InvoiceFormStepUnderpaid
|
||||
|
||||
case 'overpaid':
|
||||
return InvoiceFormStepSuccess
|
||||
// return InvoiceFormStepOverpaid
|
||||
|
||||
default:
|
||||
return InvoiceFormStepExpired
|
||||
}
|
||||
})
|
||||
|
||||
const intervalId = setInterval(refresh, 5000)
|
||||
|
||||
watch(payedFlag, () => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
|
||||
provide('public-invoice', { invoice, expirationTime, payedFlag })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-form {
|
||||
width: 532px;
|
||||
margin: 0 auto;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__order-id {
|
||||
@include txt-r-sb;
|
||||
|
||||
color: $clr-grey-500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
background: $clr-white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__network-alert {
|
||||
@include txt-i-m;
|
||||
|
||||
text-align: center;
|
||||
color: $clr-grey-400;
|
||||
padding: 8px 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
[data-status-illustration] {
|
||||
animation: rotate .8s cubic-bezier(.25,.75,.5,1.25);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotateZ(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2
apps/pay/public/robots.txt
Normal file
2
apps/pay/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
21
apps/pay/server/api/[...path].ts
Normal file
21
apps/pay/server/api/[...path].ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default defineEventHandler((event) => {
|
||||
if (process.env.NODE_ENV !== 'development')
|
||||
return
|
||||
|
||||
const url = getRequestURL(event)
|
||||
const path = event.path.replace('/api/', '')
|
||||
|
||||
return proxyRequest(event, `https://api.prgms.io/api/v1/${path}`, {
|
||||
cookieDomainRewrite: {
|
||||
'prgms.io': url.hostname,
|
||||
},
|
||||
onResponse: (event, response) => {
|
||||
const cookies = event.node.res.getHeader('set-cookie')
|
||||
|
||||
if (!cookies)
|
||||
return
|
||||
|
||||
event.node.res.setHeader('set-cookie', cookies.map(cookie => cookie.replace(' Secure;', '')))
|
||||
},
|
||||
})
|
||||
})
|
||||
5
apps/pay/tsconfig.json
Normal file
5
apps/pay/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"module": "esnext"
|
||||
}
|
||||
Reference in New Issue
Block a user