This commit is contained in:
Nadar
2026-03-17 13:24:22 +03:00
commit 82e5ac9d81
554 changed files with 29637 additions and 0 deletions

24
apps/pay/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

12
apps/pay/app.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
useHead({
title: 'Indefiti',
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
})
</script>

View 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),
},
],
}

View File

@@ -0,0 +1,7 @@
body, html {
height: 100%;
}
#__nuxt {
height: 100%;
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View File

@@ -0,0 +1,5 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
globalInjection: true,
}))

5
apps/pay/lang/en.js Normal file
View 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
View File

@@ -0,0 +1 @@
export default {}

View 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
View 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
View 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
View 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>

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View 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
View File

@@ -0,0 +1,5 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"module": "esnext"
}