From ff67f3dffa44098ced54fe8035280d9b18fc2ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 4 Jun 2025 13:46:51 -0300 Subject: [PATCH 01/36] feat: add proUntil column --- prisma/migrations/20250604164641_pro_until_column/migration.sql | 2 ++ prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 prisma/migrations/20250604164641_pro_until_column/migration.sql diff --git a/prisma/migrations/20250604164641_pro_until_column/migration.sql b/prisma/migrations/20250604164641_pro_until_column/migration.sql new file mode 100644 index 000000000..16e745b7a --- /dev/null +++ b/prisma/migrations/20250604164641_pro_until_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `UserProfile` ADD COLUMN `proUntil` DATETIME(3) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d102eb0e1..fe39d7a4f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -168,6 +168,7 @@ model UserProfile { addresses AddressesOnUserProfiles[] preferredCurrencyId Int @default(1) preferredTimezone String @db.VarChar(255)@default("") + proUntil DateTime? organizationId String? organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) From 0aba51aec1ac6c8d9a85dd21c3feaa3b323328f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 4 Jun 2025 13:54:59 -0300 Subject: [PATCH 02/36] feat: add config options for pro --- config/example-config.json | 9 ++++++++- config/index.ts | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/config/example-config.json b/config/example-config.json index f77182ff3..7deb468ec 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -29,5 +29,12 @@ "bitcoincash": true }, "triggerPOSTTimeout": 3000, - "sideshiftAffiliateId": "JPk84U3xN" + "sideshiftAffiliateId": "JPk84U3xN", + "proEnabled": true, + "proMonthsCost": { + "1": "10", + "3": "20", + "6": "30", + "12": "50" + } } diff --git a/config/index.ts b/config/index.ts index b67055e95..6b209e713 100644 --- a/config/index.ts +++ b/config/index.ts @@ -21,6 +21,8 @@ interface Config { sideshiftAffiliateId: string smtpHost: string smtpPort: number + proEnabled: boolean + proMonthsCost: Record } const readConfig = (): Config => { From 01f431a4a3372c903eb7625667772ad8a8f0cc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 4 Jun 2025 13:58:18 -0300 Subject: [PATCH 03/36] refactor: organization title --- components/Organization/ViewOrganization.tsx | 5 +++-- pages/account/index.tsx | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Organization/ViewOrganization.tsx b/components/Organization/ViewOrganization.tsx index 4fe902b28..3897007cc 100644 --- a/components/Organization/ViewOrganization.tsx +++ b/components/Organization/ViewOrganization.tsx @@ -22,7 +22,8 @@ const ViewOrganization = ({ user, orgMembers, setOrgMembers, organization }: IPr const [orgEdit, setOrgEdit] = useState('') const [loading, setLoading] = useState(false) - return ( + return <> +

Organization

{org !== null && org.creatorId === user.userProfile.id ? ( @@ -129,7 +130,7 @@ const ViewOrganization = ({ user, orgMembers, setOrgMembers, organization }: IPr )} {error !== '' &&
{error}
}
- ) + } export default ViewOrganization diff --git a/pages/account/index.tsx b/pages/account/index.tsx index 546c44e91..4a2be1262 100644 --- a/pages/account/index.tsx +++ b/pages/account/index.tsx @@ -189,7 +189,6 @@ export default function Account ({ user, userPublicKey, organization, orgMembers
-

Organization

) From 8a50bfd386bce788588c81b8759d743aaa307aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 4 Jun 2025 14:20:16 -0300 Subject: [PATCH 04/36] feat: component, service & endpoint to get pro status --- components/Account/ProConfig.tsx | 28 ++++++++++++++++++++++++ pages/api/user/remainingProTime/index.ts | 15 +++++++++++++ services/userService.ts | 14 ++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 components/Account/ProConfig.tsx create mode 100644 pages/api/user/remainingProTime/index.ts diff --git a/components/Account/ProConfig.tsx b/components/Account/ProConfig.tsx new file mode 100644 index 000000000..4f637155f --- /dev/null +++ b/components/Account/ProConfig.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react' + +const ProConfig = (): JSX.Element => { + const [text, setText] = useState('') + + useEffect(() => { + void (async () => { + const res = await fetch('/api/user/remainingProTime') + if (res.status === 200) { + const remainingMs: number | null = await res.json() + if (remainingMs === null) { + setText('You are not PRO.') + } else if (remainingMs <= 0) { + setText('Your PRO has expired.') + } else { + const futureDate = new Date(Date.now() + remainingMs) + setText(`You are PRO until ${futureDate.toLocaleDateString()}.`) + } + } else { + setText('Failed to fetch PRO status') + } + })() + }, []) + + return <>{text} +} + +export default ProConfig diff --git a/pages/api/user/remainingProTime/index.ts b/pages/api/user/remainingProTime/index.ts new file mode 100644 index 000000000..0117bb658 --- /dev/null +++ b/pages/api/user/remainingProTime/index.ts @@ -0,0 +1,15 @@ +import { setSession } from 'utils/setSession' +import * as userService from 'services/userService' + +export default async ( + req: any, + res: any +): Promise => { + await setSession(req, res, true) + + if (req.method === 'GET') { + const session = req.session + const remainingTime = await userService.userRemainingProTime(session.userId) + res.status(200).json(remainingTime) + } +} diff --git a/services/userService.ts b/services/userService.ts index 671ac4eec..7d5d1425e 100644 --- a/services/userService.ts +++ b/services/userService.ts @@ -149,3 +149,17 @@ export async function updatePreferredTimezone (id: string, preferredTimezone: st } }) } + +export async function userRemainingProTime (id: string): Promise { + const today = new Date() + const proUntil = (await prisma.userProfile.findUniqueOrThrow({ + where: { id }, + select: { + proUntil: true + } + })).proUntil + if (proUntil === null) { + return null + } + return proUntil.getTime() - today.getTime() +} From e8ecc500fe0a41775cd5220ee516da76aa60a293 Mon Sep 17 00:00:00 2001 From: chedieck Date: Wed, 4 Jun 2025 15:31:21 -0300 Subject: [PATCH 05/36] fix: mocked objects --- tests/mockedObjects.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/mockedObjects.ts b/tests/mockedObjects.ts index a989a800e..eae8728c3 100644 --- a/tests/mockedObjects.ts +++ b/tests/mockedObjects.ts @@ -526,7 +526,8 @@ export const mockedUserProfile: UserProfile = { lastSentVerificationEmailAt: null, preferredCurrencyId: 1, preferredTimezone: '', - emailCredits: 15 + emailCredits: 15, + proUntil: null } export const mockedUserProfileWithPublicKey: UserProfile = { @@ -539,7 +540,8 @@ export const mockedUserProfileWithPublicKey: UserProfile = { lastSentVerificationEmailAt: null, preferredCurrencyId: 1, preferredTimezone: '', - emailCredits: 15 + emailCredits: 15, + proUntil: null } export const mockedAddressesOnButtons: AddressesOnButtons[] = [ From 4080778c6d05f953eb01a767f9f44457668ba90d Mon Sep 17 00:00:00 2001 From: chedieck Date: Wed, 4 Jun 2025 15:37:14 -0300 Subject: [PATCH 06/36] refactor: rename ProConfig -> ProDisplay --- components/Account/{ProConfig.tsx => ProDisplay.tsx} | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename components/Account/{ProConfig.tsx => ProDisplay.tsx} (77%) diff --git a/components/Account/ProConfig.tsx b/components/Account/ProDisplay.tsx similarity index 77% rename from components/Account/ProConfig.tsx rename to components/Account/ProDisplay.tsx index 4f637155f..da9bb3814 100644 --- a/components/Account/ProConfig.tsx +++ b/components/Account/ProDisplay.tsx @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react' +import ProPurchase from './ProPurchase' const ProConfig = (): JSX.Element => { const [text, setText] = useState('') + const [isPro, setIsPro] = useState() useEffect(() => { void (async () => { @@ -14,6 +16,7 @@ const ProConfig = (): JSX.Element => { setText('Your PRO has expired.') } else { const futureDate = new Date(Date.now() + remainingMs) + setIsPro(true) setText(`You are PRO until ${futureDate.toLocaleDateString()}.`) } } else { @@ -22,7 +25,10 @@ const ProConfig = (): JSX.Element => { })() }, []) - return <>{text} + return
+ {text} + {isPro === false && } +
} export default ProConfig From d8631d7d1fe3808db029fb8a0bafa2472073f7a7 Mon Sep 17 00:00:00 2001 From: chedieck Date: Wed, 4 Jun 2025 15:37:57 -0300 Subject: [PATCH 07/36] feat: show ProDisplay and ProPurchase --- components/Account/ProPurchase.tsx | 9 +++++++++ pages/account/index.tsx | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 components/Account/ProPurchase.tsx diff --git a/components/Account/ProPurchase.tsx b/components/Account/ProPurchase.tsx new file mode 100644 index 000000000..51b63adc2 --- /dev/null +++ b/components/Account/ProPurchase.tsx @@ -0,0 +1,9 @@ +// import { useEffect, useState } from 'react' + +const ProPurchase = (): JSX.Element => { + return
+ WIP PURCHASE +
+} + +export default ProPurchase diff --git a/pages/account/index.tsx b/pages/account/index.tsx index 4a2be1262..f178649a0 100644 --- a/pages/account/index.tsx +++ b/pages/account/index.tsx @@ -17,6 +17,8 @@ import { removeDateFields, removeUnserializableFields } from 'utils/index' import TopBar from 'components/TopBar' import TimezoneSelector from 'components/Timezone Selector' import moment from 'moment-timezone' +import config from 'config' +import ProDisplay from 'components/Account/ProDisplay' export const getServerSideProps: GetServerSideProps = async (context) => { supertokensNode.init(SuperTokensConfig.backendConfig()) @@ -189,6 +191,7 @@ export default function Account ({ user, userPublicKey, organization, orgMembers
+ {config.proEnabled && }
) From d68460f0689e5a516f09b54994ed7ff8a9e4ec7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 5 Jun 2025 17:05:21 -0300 Subject: [PATCH 08/36] feat: pro settings skeleton --- components/Account/ProDisplay.tsx | 12 +++- components/Account/ProPurchase.tsx | 4 +- components/Account/account.module.css | 15 +++++ config/example-config.json | 3 +- config/index.ts | 1 + pages/pro/index.tsx | 92 +++++++++++++++++++++++++++ pages/pro/pro.module.css | 52 +++++++++++++++ 7 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 pages/pro/index.tsx create mode 100644 pages/pro/pro.module.css diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index da9bb3814..24c3373f1 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import ProPurchase from './ProPurchase' +import style from './account.module.css' const ProConfig = (): JSX.Element => { const [text, setText] = useState('') @@ -11,8 +12,10 @@ const ProConfig = (): JSX.Element => { if (res.status === 200) { const remainingMs: number | null = await res.json() if (remainingMs === null) { + setIsPro(false) setText('You are not PRO.') } else if (remainingMs <= 0) { + setIsPro(false) setText('Your PRO has expired.') } else { const futureDate = new Date(Date.now() + remainingMs) @@ -25,10 +28,13 @@ const ProConfig = (): JSX.Element => { })() }, []) - return
- {text} - {isPro === false && } + return <> +

PayButton Pro

+
+ {text} + {isPro === false && }
+ } export default ProConfig diff --git a/components/Account/ProPurchase.tsx b/components/Account/ProPurchase.tsx index 51b63adc2..2190aaa78 100644 --- a/components/Account/ProPurchase.tsx +++ b/components/Account/ProPurchase.tsx @@ -1,8 +1,8 @@ -// import { useEffect, useState } from 'react' +import Link from 'next/link' const ProPurchase = (): JSX.Element => { return
- WIP PURCHASE + Buy Pro
} diff --git a/components/Account/account.module.css b/components/Account/account.module.css index b67731025..4fa21e95f 100644 --- a/components/Account/account.module.css +++ b/components/Account/account.module.css @@ -44,3 +44,18 @@ body[data-theme='dark'] .changepw_ctn input { } } +.pro_ctn { + width: 100%; + background-color: #fff; + max-width: 700px; + padding: 20px; + border-radius: 10px; +} + +body[data-theme="dark"] .pro_ctn { + background-color: var(--secondary-bg-color); +} + +body[data-theme="dark"] .pro_ctn input { + border-color: var(--gray) +} diff --git a/config/example-config.json b/config/example-config.json index 7deb468ec..380c0b301 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -36,5 +36,6 @@ "3": "20", "6": "30", "12": "50" - } + }, + "proPayoutAddress": "ecash:qpg35jhcdzue9wv8teds7ku0vnp5dzs5vqss7p6ph6" } diff --git a/config/index.ts b/config/index.ts index 6b209e713..7d376956d 100644 --- a/config/index.ts +++ b/config/index.ts @@ -23,6 +23,7 @@ interface Config { smtpPort: number proEnabled: boolean proMonthsCost: Record + proPayoutAddress: string } const readConfig = (): Config => { diff --git a/pages/pro/index.tsx b/pages/pro/index.tsx new file mode 100644 index 000000000..449afb1dd --- /dev/null +++ b/pages/pro/index.tsx @@ -0,0 +1,92 @@ +import { PayButton } from '@paybutton/react' +import config from 'config/index' +import React, { useState } from 'react' +import style from './pro.module.css' +import { UserProfile } from '@prisma/client' +import Page from 'components/Page' +import { GetServerSideProps } from 'next' +import supertokensNode from 'supertokens-node' +import * as SuperTokensConfig from '../../config/backendConfig' +import Session from 'supertokens-node/recipe/session' +import { fetchUserProfileFromId } from 'services/userService' +import { removeUnserializableFields } from 'utils' + +interface IProps { + user: UserProfile +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + supertokensNode.init(SuperTokensConfig.backendConfig()) + let session + try { + session = await Session.getSession(context.req, context.res) + } catch (err: any) { + if (err.type === Session.Error.TRY_REFRESH_TOKEN) { + return { props: { fromSupertokens: 'needs-refresh' } } + } else if (err.type === Session.Error.UNAUTHORISED) { + return { props: {} } + } else { + throw err + } + } + + const user = await fetchUserProfileFromId(session.getUserId()) + + removeUnserializableFields(user) + + return { + props: { + user + } + } +} + +export default function Pro ({ user }: IProps): React.ReactElement { + const offeredMonths = Object.keys(config.proMonthsCost) + const [selectedMonths, setSelectedMonths] = useState(null) + + const handleSelect = (months: string): void => { + setSelectedMonths(months) + } + + const renderLabel = (m: string): string => { + if (m === '1') return '1 month' + if (m === '12') return '1 year' + return `${m} months` + } + console.log('oia', user) + + return user === undefined + ? + : ( +
+

Choose your PRO plan

+
+ {offeredMonths.map((m) => { + const selected = selectedMonths === m + return ( +
handleSelect(m)} + className={`${style.card} ${selected ? style.selected : ''}`} + > +
{renderLabel(m)}
+
${config.proMonthsCost[m]}
+
+ ) + })} +
+ + {selectedMonths !== null && ( +
+ +
+ )} +
+ ) +} diff --git a/pages/pro/pro.module.css b/pages/pro/pro.module.css new file mode 100644 index 000000000..d9390a90b --- /dev/null +++ b/pages/pro/pro.module.css @@ -0,0 +1,52 @@ +.container { + padding: 1rem; +} + +.heading { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + text-align: center; +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.card { + border: 2px solid #ccc; + border-radius: 12px; + padding: 1rem; + text-align: center; + cursor: pointer; + transition: all 0.2s ease-in-out; +} + +.card:hover { + border-color: #fff; + background-color: #333; +} + +.selected { + border-color: #0070f3; + background-color: #8888f3; + transform: scale(1.05); +} + +.label { + font-size: 1.1rem; + font-weight: 600; +} + +.price { + font-size: 0.9rem; + margin-top: 0.5rem; +} + +.payButtonWrapper { + text-align: center; +} + From b3a35ae21d9b44bd6936b9b3cd68f70071641752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Mon, 9 Jun 2025 13:55:32 -0300 Subject: [PATCH 09/36] chore: update default address --- config/example-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/example-config.json b/config/example-config.json index 380c0b301..4ada32d76 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -37,5 +37,5 @@ "6": "30", "12": "50" }, - "proPayoutAddress": "ecash:qpg35jhcdzue9wv8teds7ku0vnp5dzs5vqss7p6ph6" + "proPayoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" } From 390c8e37f01300bba2da1ba7a96f7b22b5d5fe5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 10 Jun 2025 08:01:18 -0300 Subject: [PATCH 10/36] feat: make link button --- components/Account/ProPurchase.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Account/ProPurchase.tsx b/components/Account/ProPurchase.tsx index 2190aaa78..7067b624e 100644 --- a/components/Account/ProPurchase.tsx +++ b/components/Account/ProPurchase.tsx @@ -2,7 +2,9 @@ import Link from 'next/link' const ProPurchase = (): JSX.Element => { return
+
} From 24bdc53f11d82de2d2ec36ef3f80eef2a43970a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 10 Jun 2025 08:40:27 -0300 Subject: [PATCH 11/36] fix: type on new config parameter --- config/example-config.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/example-config.json b/config/example-config.json index 4ada32d76..cc6c9f40f 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -32,10 +32,10 @@ "sideshiftAffiliateId": "JPk84U3xN", "proEnabled": true, "proMonthsCost": { - "1": "10", - "3": "20", - "6": "30", - "12": "50" + "1": 10, + "3": 20, + "6": 30, + "12": 50 }, "proPayoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" } From 47561ab40837fbba103c17260b1e6946f73a3624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 10 Jun 2025 08:40:41 -0300 Subject: [PATCH 12/36] chore: add new config parameters to README --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 93fd14a6a..d9ccca679 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,13 @@ default: "/api/auth" > Base API endpoint for authentication. +#### websiteBasePath +``` +type: string +default: "/auth", +``` +> Base API endpoint for authentication through SuperTokens. + #### websiteDomain ``` type: string @@ -203,6 +210,34 @@ default: N/A ``` > Necessary only for paybutton client to interact with sideshift through the server. +#### proEnabled +``` +type: boolean +default: true +``` +> If the PayButton Pro features should be enabled. + +#### proMonthsCost +``` +type: { +[key: string]: number +} +default: { +"1": 10, +"3": 20, +"6": 30, +"12": 50 +} +``` +> The pricing model for PayButton Pro subscription — [value] USD for [key] months. + +#### proPayoutAddress +``` +type: string +default: "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" +``` +> The payout address for PayButton Pro subscriptions. + --- - For production, set `ENVIRONMENT=production` in `.env.local.` This optimizes the build for performance and skips the setup of various dev tools (like LiveReload). From e1325fa9e5efd87a27e33b7d081b4ca6a718bbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 12 Jun 2025 17:08:22 -0300 Subject: [PATCH 13/36] fix: Upgrade to Pro text & css --- components/Account/ProDisplay.tsx | 2 +- components/Account/ProPurchase.tsx | 9 ++++----- components/Account/account.module.css | 2 ++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index 24c3373f1..75c61935d 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -31,7 +31,7 @@ const ProConfig = (): JSX.Element => { return <>

PayButton Pro

- {text} +

{text}

{isPro === false && }
diff --git a/components/Account/ProPurchase.tsx b/components/Account/ProPurchase.tsx index 7067b624e..75e7f7d56 100644 --- a/components/Account/ProPurchase.tsx +++ b/components/Account/ProPurchase.tsx @@ -1,11 +1,10 @@ +import Button from 'components/Button' import Link from 'next/link' const ProPurchase = (): JSX.Element => { - return
- -
+ return } export default ProPurchase diff --git a/components/Account/account.module.css b/components/Account/account.module.css index 4fa21e95f..031482bf0 100644 --- a/components/Account/account.module.css +++ b/components/Account/account.module.css @@ -50,6 +50,8 @@ body[data-theme='dark'] .changepw_ctn input { max-width: 700px; padding: 20px; border-radius: 10px; + display: flex; + flex-direction: column; } body[data-theme="dark"] .pro_ctn { From a6943eb13485c8a656802c2e4e1eafa5f5d4f664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 12 Jun 2025 17:23:06 -0300 Subject: [PATCH 14/36] feat: add hint --- components/Account/ProDisplay.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index 75c61935d..55f19dd55 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -1,10 +1,12 @@ import { useEffect, useState } from 'react' import ProPurchase from './ProPurchase' import style from './account.module.css' +import stylep from '../../pages/account/account.module.css' const ProConfig = (): JSX.Element => { const [text, setText] = useState('') const [isPro, setIsPro] = useState() + const [infoModal, setInfoModal] = useState(false) useEffect(() => { void (async () => { @@ -31,8 +33,23 @@ const ProConfig = (): JSX.Element => { return <>

PayButton Pro

-

{text}

+
+
+ {text} +
setInfoModal(!infoModal)} + className={stylep.whats_this_btn} + > + {infoModal ? 'Close' : 'What is this?'} +
+
+
{isPro === false && } + {infoModal && ( +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt risus libero, venenatis commodo nibh commodo eget. Aenean turpis tellus, consectetur vel consequat nec, vehicula quis odio. Sed vitae venenatis orci. Vestibulum a facilisis tellus. Nunc at hendrerit +
+ )}
} From 35508b17e0c9b4269d2823e4cf44c4cbd075e5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 12 Jun 2025 17:23:54 -0300 Subject: [PATCH 15/36] fix: default export name --- components/Button/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Button/index.tsx b/components/Button/index.tsx index 367d981a8..cabd8cf6d 100644 --- a/components/Button/index.tsx +++ b/components/Button/index.tsx @@ -19,7 +19,7 @@ interface ButtonProps { loading?: boolean } -export default function TopBar ({ +export default function Button ({ children, disabled, type = 'button', From 95167d659cec3ce0833b6a01f951a9a57ec02591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 12 Jun 2025 17:44:06 -0300 Subject: [PATCH 16/36] fix: button link css --- components/Account/ProDisplay.tsx | 2 -- components/Account/ProPurchase.tsx | 9 ++++++--- components/Account/account.module.css | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index 55f19dd55..b9035579e 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -33,7 +33,6 @@ const ProConfig = (): JSX.Element => { return <>

PayButton Pro

-
{text}
{ {infoModal ? 'Close' : 'What is this?'}
-
{isPro === false && } {infoModal && (
diff --git a/components/Account/ProPurchase.tsx b/components/Account/ProPurchase.tsx index 75e7f7d56..16f0ade02 100644 --- a/components/Account/ProPurchase.tsx +++ b/components/Account/ProPurchase.tsx @@ -1,10 +1,13 @@ import Button from 'components/Button' import Link from 'next/link' +import style from './account.module.css' const ProPurchase = (): JSX.Element => { - return + return
+ + + +
} export default ProPurchase diff --git a/components/Account/account.module.css b/components/Account/account.module.css index 031482bf0..e37aabf5c 100644 --- a/components/Account/account.module.css +++ b/components/Account/account.module.css @@ -52,6 +52,8 @@ body[data-theme='dark'] .changepw_ctn input { border-radius: 10px; display: flex; flex-direction: column; + align-items: center; + justify-content: space-around; } body[data-theme="dark"] .pro_ctn { @@ -61,3 +63,14 @@ body[data-theme="dark"] .pro_ctn { body[data-theme="dark"] .pro_ctn input { border-color: var(--gray) } + +.upgrade_btn { + margin-top: 2rem; + width: 100% +} +.upgrade_btn a { + width: 100% +} +.upgrade_btn a button { + width: 100% +} From 69f173e4012830a3f4f50bc329065ccc3bd8cb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 12 Jun 2025 18:06:30 -0300 Subject: [PATCH 17/36] fix latest to be 4.1.0 --- yarn.lock | 326 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 171 insertions(+), 155 deletions(-) diff --git a/yarn.lock b/yarn.lock index d4ddc8cbe..3b5d3cade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1550,21 +1550,26 @@ fastq "^1.6.0" "@paybutton/react@latest": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@paybutton/react/-/react-2.0.0.tgz#38ab65cc55393a2cbee977e07cd982c9e9c6f1af" - integrity sha512-aoCHZag5QF7yhtBp3PBLkBQ6bw7Kc9kcZGeohs0M0s9HEE+kOPAS4BN9X7M3dn13C/w1yFHz3WaS+iwYmqumHA== + version "4.1.0" + resolved "https://registry.yarnpkg.com/@paybutton/react/-/react-4.1.0.tgz#f7ca742458fe6ef54fb0c9bc4234572083e2157e" + integrity sha512-qpN2N7QjV9BVUnPnj3OPV50B/AwRLfH40dJxnSFK+f0s+jrTKjiikjsI+7IVw2CfJ4z6wdKR2Ag1HCXDHepc6g== dependencies: "@material-ui/core" "4.12.4" "@material-ui/lab" "4.0.0-alpha.61" "@material-ui/styles" "4.11.5" - axios "0.21.4" + "@types/crypto-js" "^4.2.1" + "@types/jest" "^29.5.11" + axios "1.6.5" bignumber.js "9.0.2" - copy-to-clipboard "3.3.1" + copy-to-clipboard "3.3.3" + crypto-js "^4.2.0" + jest "^29.7.0" lodash "4.17.21" notistack "1.0.10" qrcode.react "1.0.1" - react-jss "10.9.0" - socket.io-client "^4.7.1" + react-jss "10.10.0" + socket.io-client "4.7.4" + ts-jest "^29.1.1" xecaddrjs "^0.0.1" "@prisma/client@^6.3.1": @@ -1786,6 +1791,11 @@ dependencies: "@types/node" "*" +"@types/crypto-js@^4.2.1": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" + integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== + "@types/engine.io@*": version "3.1.7" resolved "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz" @@ -1859,6 +1869,14 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/jest@^29.5.11": + version "29.5.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -2399,12 +2417,14 @@ available-typed-arrays@^1.0.5: resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axios@0.21.4: - version "0.21.4" - resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== +axios@1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8" + integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" axios@^0.26.1: version "0.26.1" @@ -2801,7 +2821,7 @@ chronik-client-cashtokens@^3.1.1-rc0: dependencies: "@types/ws" "^8.2.1" axios "^1.6.3" - ecashaddrjs "file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs" + ecashaddrjs "file:../../../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs" isomorphic-ws "^4.0.1" protobufjs "^6.8.8" ws "^8.3.0" @@ -2984,10 +3004,10 @@ cookie@~0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -copy-to-clipboard@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" - integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== +copy-to-clipboard@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== dependencies: toggle-selection "^1.0.6" @@ -3069,14 +3089,19 @@ crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== -css-jss@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/css-jss/-/css-jss-10.9.0.tgz#1595c67bacf651100984a05763c1170d1bfa7127" - integrity sha512-CpYclti5ZQ18PfAeXaHQ2bEw4DEUfjC0lTS9sQcUlTRF8hC/Va0h3DIowlRm6AH/Ka/O/+tp41Q5zn9MJQoRsA== +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + +css-jss@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/css-jss/-/css-jss-10.10.0.tgz#bd51fbd255cc24597ac0f0f32368394794d37ef3" + integrity sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" - jss-preset-default "10.9.0" + jss "^10.10.0" + jss-preset-default "^10.10.0" css-vendor@^2.0.8: version "2.0.8" @@ -3268,7 +3293,7 @@ ecashaddrjs@^1.0.7: dependencies: big-integer "1.6.36" -ecashaddrjs@^2.0.0, "ecashaddrjs@file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": +ecashaddrjs@^2.0.0, "ecashaddrjs@file:../../../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": version "2.0.0" resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874" integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw== @@ -3333,6 +3358,17 @@ engine.io-client@~6.5.1: ws "~8.11.0" xmlhttprequest-ssl "~2.0.0" +engine.io-client@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.4.tgz#b8bc71ed3f25d0d51d587729262486b4b33bd0d0" + integrity sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.0.0" + engine.io-parser@~5.0.3: version "5.0.6" resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz" @@ -3343,6 +3379,11 @@ engine.io-parser@~5.1.0: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.1.0.tgz#d593d6372d7f79212df48f807b8cace1ea1cb1b8" integrity sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w== +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + engine.io@~6.4.1: version "6.4.1" resolved "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz" @@ -3724,7 +3765,7 @@ exit@^0.1.2: resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.7.0: +expect@^29.0.0, expect@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== @@ -3880,11 +3921,16 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.0, follow-redirects@^1.14.8: +follow-redirects@^1.14.8: version "1.15.2" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -5032,16 +5078,7 @@ jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.3.8" -jss-plugin-camel-case@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" - integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== - dependencies: - "@babel/runtime" "^7.3.1" - hyphenate-style-name "^1.0.3" - jss "10.9.0" - -jss-plugin-camel-case@^10.5.1: +jss-plugin-camel-case@10.10.0, jss-plugin-camel-case@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c" integrity sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw== @@ -5050,24 +5087,16 @@ jss-plugin-camel-case@^10.5.1: hyphenate-style-name "^1.0.3" jss "10.10.0" -jss-plugin-compose@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-compose/-/jss-plugin-compose-10.9.0.tgz#4954583227db9b49bd2e29cd055dcc65b12cb19d" - integrity sha512-Q/0FEZhDwGUpf3/b7+PspmMi6MVSlN3YlTDmvrft7I6N346jUpd8MYkYP/6qM1ZMuVj4v8ky/XYqr1v2ganLLg== +jss-plugin-compose@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-plugin-compose/-/jss-plugin-compose-10.10.0.tgz#00d7a79adf7fcfe4927a792febdf0deceb0a7cd2" + integrity sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.10.0" tiny-warning "^1.0.2" -jss-plugin-default-unit@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" - integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-default-unit@^10.5.1: +jss-plugin-default-unit@10.10.0, jss-plugin-default-unit@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz#db3925cf6a07f8e1dd459549d9c8aadff9804293" integrity sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ== @@ -5075,32 +5104,24 @@ jss-plugin-default-unit@^10.5.1: "@babel/runtime" "^7.3.1" jss "10.10.0" -jss-plugin-expand@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-expand/-/jss-plugin-expand-10.9.0.tgz#96c902f5759fe189184f8418997659d61e927ffe" - integrity sha512-QfZ9jld0HpF1OiYU7cGWQ4q+f6+Wu93mV4X+cA1iVRssiUbSbygwdfZkUwX23UOhS1WWRJeQlLK1aJC94K8/0A== +jss-plugin-expand@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-plugin-expand/-/jss-plugin-expand-10.10.0.tgz#5debd80554174ca2d9b9e38d85d4cb6f3e0393ab" + integrity sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.10.0" -jss-plugin-extend@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-extend/-/jss-plugin-extend-10.9.0.tgz#b1163ceb25d908888b326e5b5fa780aaaed4884d" - integrity sha512-xvmosUh3RsKVsm9L14ml6PL3i0Ejj5gB6eo/jTMkGW1kIy42gNXV1EthR8cD5xiowWstnvugQ3JF0pI5+QkPMg== +jss-plugin-extend@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-plugin-extend/-/jss-plugin-extend-10.10.0.tgz#94eb450847a8941777e77ea4533a579c1c578430" + integrity sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.10.0" tiny-warning "^1.0.2" -jss-plugin-global@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" - integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-global@^10.5.1: +jss-plugin-global@10.10.0, jss-plugin-global@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz#1c55d3c35821fab67a538a38918292fc9c567efd" integrity sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A== @@ -5108,16 +5129,7 @@ jss-plugin-global@^10.5.1: "@babel/runtime" "^7.3.1" jss "10.10.0" -jss-plugin-nested@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" - integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - tiny-warning "^1.0.2" - -jss-plugin-nested@^10.5.1: +jss-plugin-nested@10.10.0, jss-plugin-nested@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz#db872ed8925688806e77f1fc87f6e62264513219" integrity sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA== @@ -5126,15 +5138,7 @@ jss-plugin-nested@^10.5.1: jss "10.10.0" tiny-warning "^1.0.2" -jss-plugin-props-sort@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" - integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-props-sort@^10.5.1: +jss-plugin-props-sort@10.10.0, jss-plugin-props-sort@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz#67f4dd4c70830c126f4ec49b4b37ccddb680a5d7" integrity sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg== @@ -5142,16 +5146,7 @@ jss-plugin-props-sort@^10.5.1: "@babel/runtime" "^7.3.1" jss "10.10.0" -jss-plugin-rule-value-function@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" - integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - tiny-warning "^1.0.2" - -jss-plugin-rule-value-function@^10.5.1: +jss-plugin-rule-value-function@10.10.0, jss-plugin-rule-value-function@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz#7d99e3229e78a3712f78ba50ab342e881d26a24b" integrity sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g== @@ -5160,34 +5155,25 @@ jss-plugin-rule-value-function@^10.5.1: jss "10.10.0" tiny-warning "^1.0.2" -jss-plugin-rule-value-observable@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.9.0.tgz#fc48b70f6915a913618fdadb44dedce23982b32d" - integrity sha512-/MWVPJVEn41+ofzQdsvH1GR4wusDqFqNnchh/98HVc580MxPy4NVkmUa2SAEpbHhnJ93sCoETZccW3HJKuvH4A== +jss-plugin-rule-value-observable@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.10.0.tgz#d17b28c4401156bbe4cd0c4a73a80aad70613e8b" + integrity sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.10.0" symbol-observable "^1.2.0" -jss-plugin-template@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-template/-/jss-plugin-template-10.9.0.tgz#27547e093e04b9dc9e900f35146e874bb196f575" - integrity sha512-lxThUvdt0drCi7xhuJWxADWTgLLy1IWCeFO5k+dtba900xJsNg0IGZplpP9w9UpaJsYS3WUwWMXw8Sxn1dobfQ== +jss-plugin-template@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-plugin-template/-/jss-plugin-template-10.10.0.tgz#072cda74a94c91b02d3a895d9e2408fd978ce033" + integrity sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.10.0" tiny-warning "^1.0.2" -jss-plugin-vendor-prefixer@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" - integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== - dependencies: - "@babel/runtime" "^7.3.1" - css-vendor "^2.0.8" - jss "10.9.0" - -jss-plugin-vendor-prefixer@^10.5.1: +jss-plugin-vendor-prefixer@10.10.0, jss-plugin-vendor-prefixer@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz#c01428ef5a89f2b128ec0af87a314d0c767931c7" integrity sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg== @@ -5196,27 +5182,27 @@ jss-plugin-vendor-prefixer@^10.5.1: css-vendor "^2.0.8" jss "10.10.0" -jss-preset-default@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-preset-default/-/jss-preset-default-10.9.0.tgz#20919ee04e543b3502086001c4179ab15c012153" - integrity sha512-Zdsj+R+UTn7OOJ1TFQi+l8PfEL7APSAM6vRPaU8mJywT8OrMjgslMKckFLrgq1k+qk1hJR1ePAMesvZ5aAXGOQ== +jss-preset-default@10.10.0, jss-preset-default@^10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/jss-preset-default/-/jss-preset-default-10.10.0.tgz#c8209449a0f6d232526c2ba3a3a6ec69ee97e023" + integrity sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" - jss-plugin-camel-case "10.9.0" - jss-plugin-compose "10.9.0" - jss-plugin-default-unit "10.9.0" - jss-plugin-expand "10.9.0" - jss-plugin-extend "10.9.0" - jss-plugin-global "10.9.0" - jss-plugin-nested "10.9.0" - jss-plugin-props-sort "10.9.0" - jss-plugin-rule-value-function "10.9.0" - jss-plugin-rule-value-observable "10.9.0" - jss-plugin-template "10.9.0" - jss-plugin-vendor-prefixer "10.9.0" - -jss@10.10.0, jss@^10.5.1: + jss "10.10.0" + jss-plugin-camel-case "10.10.0" + jss-plugin-compose "10.10.0" + jss-plugin-default-unit "10.10.0" + jss-plugin-expand "10.10.0" + jss-plugin-extend "10.10.0" + jss-plugin-global "10.10.0" + jss-plugin-nested "10.10.0" + jss-plugin-props-sort "10.10.0" + jss-plugin-rule-value-function "10.10.0" + jss-plugin-rule-value-observable "10.10.0" + jss-plugin-template "10.10.0" + jss-plugin-vendor-prefixer "10.10.0" + +jss@10.10.0, jss@^10.10.0, jss@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss/-/jss-10.10.0.tgz#a75cc85b0108c7ac8c7b7d296c520a3e4fbc6ccc" integrity sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw== @@ -5226,16 +5212,6 @@ jss@10.10.0, jss@^10.5.1: is-in-browser "^1.1.3" tiny-warning "^1.0.2" -jss@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" - integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^3.0.2" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -6004,7 +5980,7 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^29.7.0: +pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== @@ -6218,18 +6194,18 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-jss@10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-10.9.0.tgz#102332a109d98510f0f812dd090d02f6045b5229" - integrity sha512-xKXTEejrSkzINF+dutFtLllIfYSN6tOA1XmnpiZGjsWZqy7Hum6fjjgAE2TbBmV9h2CW62ekmGj/Mx27ZuMjuw== +react-jss@10.10.0: + version "10.10.0" + resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-10.10.0.tgz#d08ab3257b0eed01e15d6d8275840055c279b0da" + integrity sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ== dependencies: "@babel/runtime" "^7.3.1" "@emotion/is-prop-valid" "^0.7.3" - css-jss "10.9.0" + css-jss "10.10.0" hoist-non-react-statics "^3.2.0" is-in-browser "^1.1.3" - jss "10.9.0" - jss-preset-default "10.9.0" + jss "10.10.0" + jss-preset-default "10.10.0" prop-types "^15.6.0" shallow-equal "^1.2.0" theming "^3.3.0" @@ -6507,6 +6483,11 @@ semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.7.2: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + semver@~7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" @@ -6655,6 +6636,16 @@ socket.io-adapter@~2.5.2: dependencies: ws "~8.11.0" +socket.io-client@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8" + integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + socket.io-client@^4.7.1: version "4.7.1" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.1.tgz#48e5f703abe4fb0402182bcf9c06b7820fb3453b" @@ -7059,6 +7050,21 @@ ts-essentials@^7.0.3: resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== +ts-jest@^29.1.1: + version "29.4.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.0.tgz#bef0ee98d94c83670af7462a1617bf2367a83740" + integrity sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q== + dependencies: + bs-logger "^0.2.6" + ejs "^3.1.10" + fast-json-stable-stringify "^2.1.0" + json5 "^2.2.3" + lodash.memoize "^4.1.2" + make-error "^1.3.6" + semver "^7.7.2" + type-fest "^4.41.0" + yargs-parser "^21.1.1" + ts-jest@^29.2.5: version "29.2.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" @@ -7159,6 +7165,11 @@ type-fest@^0.21.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + type-is@^1.6.18, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -7396,6 +7407,11 @@ ws@~8.11.0: resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + xecaddrjs@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/xecaddrjs/-/xecaddrjs-0.0.1.tgz" From 4d0d746bd493115070581386a4951b2e5f50b382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 20 Jun 2025 10:29:36 -0300 Subject: [PATCH 18/36] refactor: update config and README --- README.md | 67 +++++++++++++++++++++++++++++++++++--- config/example-config.json | 14 +++++--- config/index.ts | 16 +++++++-- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d9ccca679..4f6afe2de 100644 --- a/README.md +++ b/README.md @@ -210,14 +210,45 @@ default: N/A ``` > Necessary only for paybutton client to interact with sideshift through the server. -#### proEnabled +#### proSettings + "": { +``` +type: { + enabled: boolean, + monthsCost: { + [key: string]: number + }, + payoutAddress: string, + standardDailyEmailLimit: number | "Inf", + proDailyEmailLimit: number | "Inf", + standardAddressesPerButtonLimit: number | "Inf", + proAddressesPerButtonLimit: number | "Inf" +} +default: { + "enabled": true, + "monthsCost": { + "1": 10, + "3": 20, + "6": 30, + "12": 50 + }, + "payoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" + "standardDailyEmailLimit": 5, + "proDailyEmailLimit": 100, + "standardAddressesPerButtonLimit": 20, + "proAddressesPerButtonLimit": "Inf" +} +``` +> General configuration for PayButton Pro. Each parameter is described below. + +##### proSettings.enabled ``` type: boolean default: true ``` -> If the PayButton Pro features should be enabled. +> If the Pro feature should be enabled or hidden. -#### proMonthsCost +##### proSettings.monthsCost ``` type: { [key: string]: number @@ -231,13 +262,41 @@ default: { ``` > The pricing model for PayButton Pro subscription — [value] USD for [key] months. -#### proPayoutAddress +##### proSettings.payoutAddress ``` type: string default: "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" ``` > The payout address for PayButton Pro subscriptions. +##### proSettings.standardDailyEmailLimit +``` +type: number | "Inf" +default: true +``` +> How many emails can a standard user send daily. + +##### proSettings.proDailyEmailLimit +``` +type: number | "Inf" +default: true +``` +> How many emails can a PayButton Pro user send daily. + +##### proSettings.standardAddressesPerButtonLimit +``` +type: number | "Inf" +default: true +``` +> How many addresses can a standard Pro user add for a single button. + +##### proSettings.proAddressesPerButtonLimit +``` +type: number | "Inf" +default: true +``` +> How many addresses can a PayButton Pro user add for a single button + --- - For production, set `ENVIRONMENT=production` in `.env.local.` This optimizes the build for performance and skips the setup of various dev tools (like LiveReload). diff --git a/config/example-config.json b/config/example-config.json index cc6c9f40f..12f24a115 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -30,12 +30,18 @@ }, "triggerPOSTTimeout": 3000, "sideshiftAffiliateId": "JPk84U3xN", - "proEnabled": true, - "proMonthsCost": { + "proSettings": { + "enabled": true, + "monthsCost": { "1": 10, "3": 20, "6": 30, "12": 50 - }, - "proPayoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" + }, + "payoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" + "standardDailyEmailLimit": 5, + "proDailyEmailLimit": 100, + "standardAddressesPerButtonLimit": 20, + "proAddressesPerButtonLimit": "Inf" + } } diff --git a/config/index.ts b/config/index.ts index 7d376956d..b6b67d590 100644 --- a/config/index.ts +++ b/config/index.ts @@ -3,6 +3,18 @@ import localConfig from '../paybutton-config.json' export type BlockchainClientOptions = 'grpc' | 'chronik' +interface ProSettings { + enabled: boolean + monthsCost: { + [key: string]: number + } + payoutAddress: string + standardDailyEmailLimit: number | 'Inf' + proDailyEmailLimit: number | 'Inf' + standardAddressesPerButtonLimit: number | 'Inf' + proAddressesPerButtonLimit: number | 'Inf' +} + interface Config { appName: string apiDomain: string @@ -21,9 +33,7 @@ interface Config { sideshiftAffiliateId: string smtpHost: string smtpPort: number - proEnabled: boolean - proMonthsCost: Record - proPayoutAddress: string + proSettings: ProSettings } const readConfig = (): Config => { From 968a5b2efded5f667c31dd4eae01120f787ceff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 20 Jun 2025 10:34:27 -0300 Subject: [PATCH 19/36] fix: update code to use new config --- pages/account/index.tsx | 2 +- pages/pro/index.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pages/account/index.tsx b/pages/account/index.tsx index f178649a0..915d6d492 100644 --- a/pages/account/index.tsx +++ b/pages/account/index.tsx @@ -191,7 +191,7 @@ export default function Account ({ user, userPublicKey, organization, orgMembers
- {config.proEnabled && } + {config.proSettings.enabled && }
) diff --git a/pages/pro/index.tsx b/pages/pro/index.tsx index 449afb1dd..f38758b88 100644 --- a/pages/pro/index.tsx +++ b/pages/pro/index.tsx @@ -42,7 +42,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { } export default function Pro ({ user }: IProps): React.ReactElement { - const offeredMonths = Object.keys(config.proMonthsCost) + const offeredMonths = Object.keys(config.proSettings.monthsCost) const [selectedMonths, setSelectedMonths] = useState(null) const handleSelect = (months: string): void => { @@ -71,7 +71,7 @@ export default function Pro ({ user }: IProps): React.ReactElement { className={`${style.card} ${selected ? style.selected : ''}`} >
{renderLabel(m)}
-
${config.proMonthsCost[m]}
+
${config.proSettings.monthsCost[m]}
) })} @@ -81,8 +81,8 @@ export default function Pro ({ user }: IProps): React.ReactElement {
From 0336536572cf9ab5e5e2bb05789319ed260d9e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 20 Jun 2025 11:04:21 -0300 Subject: [PATCH 20/36] feat: informative table with limits --- components/Account/ProDisplay.tsx | 29 ++- pages/account/account.module.css | 294 ++++++++++++++++-------------- styles/global.css | 2 +- 3 files changed, 190 insertions(+), 135 deletions(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index b9035579e..b99d7c4f4 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -2,12 +2,17 @@ import { useEffect, useState } from 'react' import ProPurchase from './ProPurchase' import style from './account.module.css' import stylep from '../../pages/account/account.module.css' +import config from 'config/index' const ProConfig = (): JSX.Element => { const [text, setText] = useState('') const [isPro, setIsPro] = useState() const [infoModal, setInfoModal] = useState(false) + const showLimit = (configLimit: number | 'Inf'): string => { + return configLimit === 'Inf' ? 'Unlimited' : configLimit.toString() + } + useEffect(() => { void (async () => { const res = await fetch('/api/user/remainingProTime') @@ -45,7 +50,29 @@ const ProConfig = (): JSX.Element => { {isPro === false && } {infoModal && (
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt risus libero, venenatis commodo nibh commodo eget. Aenean turpis tellus, consectetur vel consequat nec, vehicula quis odio. Sed vitae venenatis orci. Vestibulum a facilisis tellus. Nunc at hendrerit +

PayButton Pro users get higher limits:

+ + + + + + + + + + + + + + + + + + + + +
StandardPro
Daily Emails{showLimit(config.proSettings.standardDailyEmailLimit)}{showLimit(config.proSettings.proDailyEmailLimit)}
Addresses Per Button{showLimit(config.proSettings.standardAddressesPerButtonLimit)}{showLimit(config.proSettings.proAddressesPerButtonLimit)}
+
)} diff --git a/pages/account/account.module.css b/pages/account/account.module.css index 4a8ee6678..9a99a6345 100644 --- a/pages/account/account.module.css +++ b/pages/account/account.module.css @@ -53,166 +53,166 @@ body[data-theme='dark'] .account_row select { } /* body[data-theme='dark'] .account_row select { - background-color: #434343 !important; - border-color: #898EA4 !important; +background-color: #434343 !important; +border-color: #898EA4 !important; } body[data-theme='dark'] .pk_card, body[data-theme='dark'] .pk_copy { - background-color: #434343; +background-color: #434343; } */ -.label { - width: 100%; - position: relative; - font-size: 15px; -} + .label { + width: 100%; + position: relative; + font-size: 15px; + } -.value { - font-weight: 600; - width: 100%; -} + .value { + font-weight: 600; + width: 100%; + } -.pk_ctn { - width: 100%; - display: flex; - align-items: center; - gap: 1px; - margin-top: 10px; -} + .pk_ctn { + width: 100%; + display: flex; + align-items: center; + gap: 1px; + margin-top: 10px; + } -.pk_card { - flex-grow: 2; - background-color: var(--primary-bg-color); - padding: 20px; - border-radius: 10px 0 0 10px; - display: inline-block; - font-weight: 600; - word-break: break-all; - position: relative; - font-weight: 400; - font-size: 16px; -} + .pk_card { + flex-grow: 2; + background-color: var(--primary-bg-color); + padding: 20px; + border-radius: 10px 0 0 10px; + display: inline-block; + font-weight: 600; + word-break: break-all; + position: relative; + font-weight: 400; + font-size: 16px; + } -.pk_copy { - width: 50px; - align-self: stretch; - display: flex; - align-items: center; - cursor: pointer; - background-color: var(--primary-bg-color); - border-radius: 0 10px 10px 0; - justify-content: center; - transition: all ease-in-out 200ms; - flex-shrink: 0; -} + .pk_copy { + width: 50px; + align-self: stretch; + display: flex; + align-items: center; + cursor: pointer; + background-color: var(--primary-bg-color); + border-radius: 0 10px 10px 0; + justify-content: center; + transition: all ease-in-out 200ms; + flex-shrink: 0; + } -body[data-theme='dark'] .pk_copy img { - filter: invert(1); -} + body[data-theme='dark'] .pk_copy img { + filter: invert(1); + } -.copied_confirmation { - width: 100%; - background-color: var(--accent-color); - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 10px 0 0 10px; - display: flex; - align-items: center; - justify-content: center; -} + .copied_confirmation { + width: 100%; + background-color: var(--accent-color); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 10px 0 0 10px; + display: flex; + align-items: center; + justify-content: center; + } -.pk_copy:hover { - background-color: var(--accent-color) !important; -} + .pk_copy:hover { + background-color: var(--accent-color) !important; + } -.updatebtn { - font-size: 14px; - cursor: pointer; - display: inline-block; - align-self: flex-start; - margin-top: 10px; - position: relative; - cursor: pointer; - padding: 5px 40px; - box-shadow: none; - font-size: 14px; -} + .updatebtn { + font-size: 14px; + cursor: pointer; + display: inline-block; + align-self: flex-start; + margin-top: 10px; + position: relative; + cursor: pointer; + padding: 5px 40px; + box-shadow: none; + font-size: 14px; + } -.public_key_info_btn { - cursor: pointer; - color: var(--accent-color); - font-size: 12px; - position: absolute; - right: 0; - user-select: none; - top: 3px; -} + .public_key_info_btn { + cursor: pointer; + color: var(--accent-color); + font-size: 12px; + position: absolute; + right: 0; + user-select: none; + top: 3px; + } -.public_key_info_ctn { - width: 100%; - margin-top: 10px; - font-size: 16px; - background-color: var(--primary-bg-color); - padding: 20px; - border-radius: 10px; -} + .public_key_info_ctn { + width: 100%; + margin-top: 10px; + font-size: 16px; + background-color: var(--primary-bg-color); + padding: 20px; + border-radius: 10px; + } -.updatebtn:hover { - background-color: var(--accent-color); -} + .updatebtn:hover { + background-color: var(--accent-color); + } -body[data-theme='dark'] .updatebtn:hover { - color: #231f20; -} + body[data-theme='dark'] .updatebtn:hover { + color: #231f20; + } -body[data-theme='dark'] .copy_btn img { - filter: grayscale(1) invert(1); -} + body[data-theme='dark'] .copy_btn img { + filter: grayscale(1) invert(1); + } -body[data-theme='dark'] .copy_btn:hover img { - filter: none; -} + body[data-theme='dark'] .copy_btn:hover img { + filter: none; + } -.change_pw_btn { - position: absolute; - right: 0; - top: 40px; - font-size: 12px; - background-color: var(--primary-bg-color); - border-radius: 100px; - padding: 3px 15px; - cursor: pointer; - user-select: none; -} + .change_pw_btn { + position: absolute; + right: 0; + top: 40px; + font-size: 12px; + background-color: var(--primary-bg-color); + border-radius: 100px; + padding: 3px 15px; + cursor: pointer; + user-select: none; + } -.change_pw_btn:hover { - background-color: var(--accent-color); -} + .change_pw_btn:hover { + background-color: var(--accent-color); + } -.whats_this_btn { - font-size: 12px; - background-color: var(--primary-bg-color); - border-radius: 100px; - padding: 3px 15px; - cursor: pointer; - user-select: none; - float: right; -} + .whats_this_btn { + font-size: 12px; + background-color: var(--primary-bg-color); + border-radius: 100px; + padding: 3px 15px; + cursor: pointer; + user-select: none; + float: right; + } -.whats_this_btn:hover { - background-color: var(--accent-color); -} + .whats_this_btn:hover { + background-color: var(--accent-color); + } -@media (max-width: 960px) { + @media (max-width: 960px) { .account_card, .public_key_card_ctn, .public_key_info_ctn, .label { - max-width: unset; + max-width: unset; } .copy_btn { @@ -225,7 +225,35 @@ body[data-theme='dark'] .copy_btn:hover img { } .updatebtn { - width: 100%; - text-align: center; + width: 100%; + text-align: center; } } + + +.public_key_info_ctn { + width: 100%; +} + +.public_key_info_ctn table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + margin: 0; + border-radius: 6px; +} + +.public_key_info_ctn th, +.public_key_info_ctn td { + border: none; + width: 100%; +} + +.public_key_info_ctn th { + background-color: black; + color: white; +} + +.public_key_info_ctn tr:nth-child(even) { + background-color: var(--secondary-bg-color); +} diff --git a/styles/global.css b/styles/global.css index b5012b77f..16028a18f 100644 --- a/styles/global.css +++ b/styles/global.css @@ -137,7 +137,7 @@ button:enabled:hover { } .paybutton-table-ctn tr:nth-child(even) { - background: none; + background-color: var(--primary-bg-color); } .paybutton-table-ctn thead th:first-child, From 8a91569598268024e280bdfd1f6d2e0d461b7042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 20 Jun 2025 14:16:16 -0300 Subject: [PATCH 21/36] fix: readme --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4f6afe2de..07aa81737 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,6 @@ default: N/A > Necessary only for paybutton client to interact with sideshift through the server. #### proSettings - "": { ``` type: { enabled: boolean, @@ -272,30 +271,30 @@ default: "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" ##### proSettings.standardDailyEmailLimit ``` type: number | "Inf" -default: true +default: 5 ``` > How many emails can a standard user send daily. ##### proSettings.proDailyEmailLimit ``` type: number | "Inf" -default: true +default: 20 ``` > How many emails can a PayButton Pro user send daily. ##### proSettings.standardAddressesPerButtonLimit ``` type: number | "Inf" -default: true +default: 5 ``` > How many addresses can a standard Pro user add for a single button. ##### proSettings.proAddressesPerButtonLimit ``` type: number | "Inf" -default: true +default: "Inf" ``` -> How many addresses can a PayButton Pro user add for a single button +> How many addresses can a PayButton Pro user add for a single button. --- From 26e1e0ba800e8c76cad5ad5cabee1124a0923f3a Mon Sep 17 00:00:00 2001 From: lissavxo Date: Mon, 26 May 2025 17:42:52 -0300 Subject: [PATCH 22/36] feat: invoices api --- constants/index.ts | 3 +- pages/api/invoices/index.ts | 32 +++++ pages/api/invoices/invoiceNumber/index.ts | 18 +++ .../invoices/transaction/[transactionId].ts | 26 ++++ .../20250515150215_invoices/migration.sql | 25 ++++ prisma/schema.prisma | 25 ++++ services/invoiceService.ts | 113 ++++++++++++++++++ services/transactionService.ts | 6 +- utils/validators.ts | 19 +++ 9 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 pages/api/invoices/index.ts create mode 100644 pages/api/invoices/invoiceNumber/index.ts create mode 100644 pages/api/invoices/transaction/[transactionId].ts create mode 100644 prisma/migrations/20250515150215_invoices/migration.sql create mode 100644 services/invoiceService.ts diff --git a/constants/index.ts b/constants/index.ts index 1a97bab4d..a35dccbf9 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -101,7 +101,8 @@ export const RESPONSE_MESSAGES = { ORGANIZATION_NAME_NOT_PROVIDED_400: { statusCode: 400, message: "'organizationName' not provided." }, INVITE_EXPIRED_400: { statusCode: 400, message: 'Invite expired.' }, INVALID_EMAIL_400: { statusCode: 400, message: 'Invalid email.' }, - USER_OUT_OF_EMAIL_CREDITS_400: { statusCode: 400, message: 'User out of email credits.' } + USER_OUT_OF_EMAIL_CREDITS_400: { statusCode: 400, message: 'User out of email credits.' }, + NO_INVOICE_FOUND_404: { statusCode: 404, message: 'No invoice found.' } } export const SOCKET_MESSAGES = { diff --git a/pages/api/invoices/index.ts b/pages/api/invoices/index.ts new file mode 100644 index 000000000..64c2f9743 --- /dev/null +++ b/pages/api/invoices/index.ts @@ -0,0 +1,32 @@ +import { setSession } from 'utils/setSession' +import { parseCreateInvoicePOSTRequest } from 'utils/validators' +import { RESPONSE_MESSAGES } from 'constants/index' +import { createInvoice } from 'services/invoiceService' + +export default async ( + req: any, + res: any +): Promise => { + await setSession(req, res, true) + const session = req.session + if (req.method === 'POST') { + try { + const parsedValues = parseCreateInvoicePOSTRequest({ + ...req.body, + userId: session.userId + }) + const invoice = await createInvoice(parsedValues) + res.status(200).json({ + invoice + }) + } catch (err: any) { + switch (err.message) { + case RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400.message: + res.status(400).json(RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400) + break + default: + res.status(500).json({ statusCode: 500, message: err.message }) + } + } + } +} diff --git a/pages/api/invoices/invoiceNumber/index.ts b/pages/api/invoices/invoiceNumber/index.ts new file mode 100644 index 000000000..be2a0056d --- /dev/null +++ b/pages/api/invoices/invoiceNumber/index.ts @@ -0,0 +1,18 @@ +import * as invoiceService from 'services/invoiceService' +import { setSession } from 'utils/setSession' + +export default async ( + req: any, + res: any +): Promise => { + await setSession(req, res) + const userId = req.session.userId + if (req.method === 'GET') { + try { + const invoiceNumber = await invoiceService.getNewInvoiceNumber(userId) + res.status(200).json({ invoiceNumber }) + } catch (err: any) { + res.status(500).json({ statusCode: 500, message: err.message }) + } + } +} diff --git a/pages/api/invoices/transaction/[transactionId].ts b/pages/api/invoices/transaction/[transactionId].ts new file mode 100644 index 000000000..5c115bf63 --- /dev/null +++ b/pages/api/invoices/transaction/[transactionId].ts @@ -0,0 +1,26 @@ +import * as invoiceService from 'services/invoiceService' +import { RESPONSE_MESSAGES } from 'constants/index' +import { setSession } from 'utils/setSession' + +export default async ( + req: any, + res: any +): Promise => { + await setSession(req, res) + const userId = req.session.userId + const transactionId = req.query.transactionId as string + if (req.method === 'GET') { + try { + const invoice = await invoiceService.getInvoiceByTransactionId(transactionId, userId) + res.status(200).json(invoice) + } catch (err: any) { + switch (err.message) { + case RESPONSE_MESSAGES.NO_INVOICE_FOUND_404.message: + res.status(404).json(RESPONSE_MESSAGES.NO_INVOICE_FOUND_404) + break + default: + res.status(500).json({ statusCode: 500, message: err.message }) + } + } + } +} diff --git a/prisma/migrations/20250515150215_invoices/migration.sql b/prisma/migrations/20250515150215_invoices/migration.sql new file mode 100644 index 000000000..87c6e0f3d --- /dev/null +++ b/prisma/migrations/20250515150215_invoices/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE `Invoice` ( + `id` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `invoiceNumber` VARCHAR(191) NOT NULL, + `transactionId` VARCHAR(191) NOT NULL, + `amount` DECIMAL(65, 30) NOT NULL, + `description` VARCHAR(191) NOT NULL, + `recipientName` VARCHAR(191) NOT NULL, + `recipientAddress` VARCHAR(191) NOT NULL, + `customerName` VARCHAR(191) NOT NULL, + `customerAddress` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Invoice_invoiceNumber_userId_unique_constraint`(`invoiceNumber`, `userId`), + UNIQUE INDEX `Invoice_transactionId_userId_unique_constraint`(`transactionId`, `userId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Invoice` ADD CONSTRAINT `Invoice_transactionId_fkey` FOREIGN KEY (`transactionId`) REFERENCES `Transaction`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Invoice` ADD CONSTRAINT `Invoice_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `UserProfile`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fe39d7a4f..be7c54465 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,6 +74,8 @@ model Transaction { opReturn String @db.LongText @default("") address Address @relation(fields: [addressId], references: [id], onDelete: Cascade, onUpdate: Cascade) prices PricesOnTransactions[] + invoices Invoice[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -166,6 +168,7 @@ model UserProfile { lastSentVerificationEmailAt DateTime? wallets WalletsOnUserProfile[] addresses AddressesOnUserProfiles[] + invoices Invoice[] preferredCurrencyId Int @default(1) preferredTimezone String @db.VarChar(255)@default("") proUntil DateTime? @@ -242,3 +245,25 @@ model OrganizationInvite { organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) } + +model Invoice { + id String @id @default(uuid()) + userId String + invoiceNumber String + transactionId String + transaction Transaction @relation(fields: [transactionId], references: [id], onUpdate: Cascade, onDelete: Restrict) + userProfile UserProfile @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Restrict) + + amount Decimal + description String + recipientName String + recipientAddress String + customerName String + customerAddress String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([invoiceNumber, userId], map: "Invoice_invoiceNumber_userId_unique_constraint") + @@unique([transactionId, userId], map: "Invoice_transactionId_userId_unique_constraint") +} \ No newline at end of file diff --git a/services/invoiceService.ts b/services/invoiceService.ts new file mode 100644 index 000000000..6a640aed4 --- /dev/null +++ b/services/invoiceService.ts @@ -0,0 +1,113 @@ +import prisma from 'prisma/clientInstance' +import { Invoice } from '@prisma/client' +import { RESPONSE_MESSAGES } from 'constants/index' + +interface CreateInvoiceParams { + userId: string + transactionId: string + invoiceNumber: string + amount: number + description: string + recipientName: string + recipientAddress: string + customerName: string + customerAddress: string +} + +interface UpdateInvoiceParams { + description: string + recipientName: string + recipientAddress: string + customerName: string + customerAddress: string +} + +export async function createInvoice (params: CreateInvoiceParams): Promise { + // const invoiceNumber = await getNewInvoiceNumber(params.userId); + console.log('params', params) + return await prisma.invoice.create({ + data: { + ...params + } + }) +} + +export async function getInvoices (userId: string): Promise { + return await prisma.invoice.findMany({ + where: { + userId + } + }) +} + +export async function getInvoiceByTransactionId (transactionId: string, userId: string): Promise { + const invoice = await prisma.invoice.findFirst({ + where: { + transactionId, + userId + } + }) + if (invoice === null) { + throw new Error(RESPONSE_MESSAGES.NO_INVOICE_FOUND_404.message) + } + + return invoice +} + +export async function getInvoiceById (invoiceId: string, userId: string): Promise { + const invoice = await prisma.invoice.findFirst({ + where: { + id: invoiceId, + userId + } + }) + if (invoice === null) { + throw new Error(RESPONSE_MESSAGES.NO_INVOICE_FOUND_404.message) + } + + return invoice +} + +export async function updateInvoice (userId: string, invoiceId: string, params: UpdateInvoiceParams): Promise { + const invoice = await getInvoiceById(invoiceId, userId) + if (invoice === null) { + throw new Error(RESPONSE_MESSAGES.NO_INVOICE_FOUND_404.message) + } + + return await prisma.invoice.update({ + where: { + id: invoice.id + }, + data: params + }) +} + +export async function getNewInvoiceNumber (userId: string): Promise { + const year = new Date().getFullYear() + + const invoiceWithTheLatestInvoiceNumber = await prisma.invoice.findFirst({ + where: { + invoiceNumber: { + startsWith: `${year}-` + }, + userId + }, + orderBy: { + invoiceNumber: 'desc' + } + }) + if (invoiceWithTheLatestInvoiceNumber === null) { + const invoicesCount = await prisma.invoice.count({ + where: { + userId + } + }) + if (invoicesCount > 0) { + return + } + } + const nextNumber = (invoiceWithTheLatestInvoiceNumber != null) ? parseInt(invoiceWithTheLatestInvoiceNumber.invoiceNumber.split('-')[1]) + 1 : 1 + + const invoiceNumber = `${year}-${String(nextNumber)}` + return invoiceNumber +} diff --git a/services/transactionService.ts b/services/transactionService.ts index 1a9003b8d..f15377185 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -114,6 +114,10 @@ const includePaybuttonsAndPrices = { }, ...includePrices } +const includePaybuttonsAndPricesAndInvoices = { + ...includePaybuttonsAndPrices, + invoices: true +} const transactionsWithPaybuttonsAndPrices = Prisma.validator()( { @@ -188,7 +192,7 @@ export async function fetchTransactionsByAddressListWithPagination ( } } }, - include: includePaybuttonsAndPrices, + include: includePaybuttonsAndPricesAndInvoices, orderBy: orderByQuery, skip: page * pageSize, take: pageSize diff --git a/utils/validators.ts b/utils/validators.ts index 27f20eb0e..3d2964fb4 100644 --- a/utils/validators.ts +++ b/utils/validators.ts @@ -550,3 +550,22 @@ export const parseUpdateUserTimezonePUTRequest = function (params: UpdateUserTim return { timezone: params.timezone } } + +export interface CreateinvoicePOSTParameters { + transactionId: string + amount: number + description: string + recipientName: string + recipientAddress: string + customerName: string + customerAddress: string +} + +export const parseCreateInvoicePOSTRequest = function (params: CreateinvoicePOSTParameters): CreateinvoicePOSTParameters { + let description = params.description + if (description === undefined) description = '' + return { + ...params, + description + } +} From 8a1601d92439b1cc7000ac46e22754661f1efc90 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Mon, 26 May 2025 18:13:21 -0300 Subject: [PATCH 23/36] feat: invoices modal --- components/Transaction/InvoiceModal.tsx | 221 ++++++++++++++++++ .../Transaction/PaybuttonTransactions.tsx | 123 +++++++++- components/Transaction/transaction.module.css | 210 +++++++++++++++++ package.json | 1 + yarn.lock | 5 + 5 files changed, 558 insertions(+), 2 deletions(-) create mode 100644 components/Transaction/InvoiceModal.tsx diff --git a/components/Transaction/InvoiceModal.tsx b/components/Transaction/InvoiceModal.tsx new file mode 100644 index 000000000..2bd054f66 --- /dev/null +++ b/components/Transaction/InvoiceModal.tsx @@ -0,0 +1,221 @@ +import React, { useState, useEffect, ReactElement } from 'react' +import style from './transaction.module.css' +import Button from 'components/Button' +import { CreateinvoicePOSTParameters } from 'utils/validators' +import axios from 'axios' + +interface InvoiceData { + invoiceNumber: string + amount: number + recipientName: string + recipientAddress: string + description: string + customerName: string + customerAddress: string +} + +interface InvoiceModalProps { + isOpen: boolean + onClose: () => void + transaction: any + invoiceData: InvoiceData | null + mode: 'create' | 'edit' | 'view' +} + +export default function InvoiceModal ({ + isOpen, + onClose, + invoiceData, + transaction, + mode +}: InvoiceModalProps): ReactElement | null { + const [formData, setFormData] = useState({ + invoiceNumber: '', + amount: Number(transaction?.amount), + recipientName: '', + recipientAddress: transaction?.address?.address, + description: '', + customerName: '', + customerAddress: '' + }) + + useEffect(() => { + setFormData(invoiceData ?? { + invoiceNumber: '', + amount: Number(transaction?.amount), + recipientName: '', + recipientAddress: transaction?.address?.address, + description: '', + customerName: '', + customerAddress: '' + }) + }, [transaction, mode, invoiceData]) + + if (!isOpen) return null + + const handleChange = (e: React.ChangeEvent): void => { + const { name, value } = e.target + setFormData(prev => ({ ...prev, [name]: value })) + } + + const handleModalClose = (): void => { + setFormData({ + invoiceNumber: '', + amount: 0, + recipientName: '', + recipientAddress: '', + description: '', + customerName: '', + customerAddress: '' + }) + onClose() + } + + async function handleSubmit (e: React.FormEvent): Promise { + e.preventDefault() + + const payload: CreateinvoicePOSTParameters = { + ...formData, + transactionId: transaction?.id + } + + try { + await axios.post('/api/invoices', payload) + onClose() + } catch (err: any) { + console.error('Invoice submission error:', err) + } + } + + const isReadOnly = mode === 'view' + + return ( +
+
+

{mode === 'edit' ? 'Edit Invoice' : mode === 'view' ? 'View Invoice' : 'Create Invoice'}

+
+ {!isReadOnly + ?
{ + void handleSubmit(e) + }} method="post"> +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + +
+
+ +
+ {!isReadOnly && ( +
+ +
+ )} +
+
+ :
+
+
+ Invoice Number: {formData.invoiceNumber} +
+
+ Amount: {formData.amount} +
+
+ Recipient Name: {formData.recipientName} +
+
+ Recipient Address: {formData.recipientAddress} +
+
+ Description: {formData.description} +
+
+ Customer Name: {formData.customerName} +
+
+ Customer Address: {formData.customerAddress} +
+
+
+ +
+
+ } +
+
+
+ ) +} diff --git a/components/Transaction/PaybuttonTransactions.tsx b/components/Transaction/PaybuttonTransactions.tsx index db6fa90b5..35a3842ce 100644 --- a/components/Transaction/PaybuttonTransactions.tsx +++ b/components/Transaction/PaybuttonTransactions.tsx @@ -1,14 +1,17 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import Image from 'next/image' import XECIcon from 'assets/xec-logo.png' import BCHIcon from 'assets/bch-logo.png' import EyeIcon from 'assets/eye-icon.png' import CheckIcon from 'assets/check-icon.png' import XIcon from 'assets/x-icon.png' +import { Plus, Pencil, FileText } from 'lucide-react' import TableContainerGetter from '../TableContainer/TableContainerGetter' import { compareNumericString } from 'utils/index' import moment from 'moment-timezone' import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index' +import InvoiceModal from './InvoiceModal' +import style from './transaction.module.css' interface IProps { addressSyncing: { @@ -36,7 +39,68 @@ function fetchTransactionsByPaybuttonId (paybuttonId: string): Function { } } +interface InvoiceData { + invoiceNumber: string + amount: number + recipientName: string + recipientAddress: string + description: string + customerName: string + customerAddress: string +} + +const fetchNextInvoiceNumberByUserId = async (): Promise => { + const response = await fetch('/api/invoices/invoiceNumber/', { + headers: { + Timezone: moment.tz.guess() + } + }) + const result = await response?.json() + return result?.invoiceNumber +} + export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => { + const [isModalOpen, setIsModalOpen] = useState(false) + const [invoiceData, setInvoiceData] = useState(null) + const [invoiceDataTransaction, setInvoiceDataTransaction] = useState(null) + const [localRefreshCount, setLocalRefreshCount] = useState(tableRefreshCount) + + const [invoiceMode, setInvoiceMode] = useState<'create' | 'edit' | 'view'>('create') + + const onCreateInvoice = async (transaction: any): Promise => { + const nextInvoiceNumber = await fetchNextInvoiceNumberByUserId() + const invoiceData = { + invoiceNumber: nextInvoiceNumber ?? '', + amount: transaction.amount, + recipientName: '', + recipientAddress: transaction.address.address, + description: '', + customerName: '', + customerAddress: '' + } + setInvoiceDataTransaction(transaction) + setInvoiceData(invoiceData) + setInvoiceMode('create') + setIsModalOpen(true) + } + + const onEditInvoice = (transaction: any): void => { + setInvoiceData(transaction.invoices[0]) + setInvoiceMode('edit') + setIsModalOpen(true) + } + + const onSeeInvoice = (transaction: any): void => { + setInvoiceData(transaction.invoices[0]) + setInvoiceMode('view') + setIsModalOpen(true) + } + + const handleCloseModal = (): void => { + setIsModalOpen(false) + setInvoiceData(null) + setLocalRefreshCount(prev => prev + 1) + } const columns = useMemo( () => [ { @@ -98,6 +162,53 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom ) } }, + { + Header: () => (
Actions
), + id: 'actions', + Cell: (cellProps) => { + const invoices = cellProps.row.original.invoices + const hasInvoice = invoices?.length > 0 + + return ( +
+ {!hasInvoice + ? ( +
+ +
New button
+
+ ) + : ( + <> + + + + )} +
+ ) + } + }, { Header: () => (
Address
), accessor: 'address.address', @@ -110,12 +221,20 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom ) } } + ], [] ) return ( <> - + + ) } diff --git a/components/Transaction/transaction.module.css b/components/Transaction/transaction.module.css index 043235d0f..c8332c071 100644 --- a/components/Transaction/transaction.module.css +++ b/components/Transaction/transaction.module.css @@ -132,3 +132,213 @@ font-size: 12px; } } + +.form_ctn_outer { + background-color: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(5px); + position: fixed; + top: 0; + left: 0px; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; +} + +.form_ctn_inner { + width: 100%; + max-width: 600px; + align-self: middle; +} + +.form_ctn_inner h4 { + margin: 0; + margin-left: 5px; + margin-bottom: 5px; +} +.form_ctn { + background-color: var(--secondary-bg-color); + width: 100%; + padding: 25px; + border-radius: 10px; + border: 1px solid var(--secondary-text-color); + overflow-y: auto; + max-height: 90vh; +} +body[data-theme='dark'] .form_ctn_outer { + background-color: rgba(0, 0, 0, 0.3); +} + +.form_ctn p { + margin-top: 0; +} + +.form_ctn form { + display: flex; + flex-direction: column; +} + +.form_ctn label { + margin-bottom: 5px; +} + +body[data-theme='dark'] .form_ctn select { + color: #fff; +} + +.form_ctn select { + background-color: var(--secondary-bg-color); +} + +.form_ctn input, +.form_ctn textarea { + border: 1px solid var(--secondary-text-color) !important; + width: 100%; +} + +body[data-theme='dark'] .form_ctn input, +body[data-theme='dark'] .form_ctn textarea, +body[data-theme='dark'] .form_ctn select { + background-color: var(--primary-bg-color); + border-color: #898EA4 !important; +} + +body[data-theme='dark'] .form_ctn select:not([multiple]) { + background-image: linear-gradient(45deg,transparent 49%,#fff 51%),linear-gradient(135deg,#fff 51%,transparent 49%); +} + +.labelMargin { + margin-top: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.form_ctn button:last-child { + margin-right: 0px; + box-shadow: none; +} + +.form_ctn .delete_btn { + background-color: unset; + border: none; + margin-right: 0px; + margin-left: 10px; + color: var(--primary-text-color); + border-radius: 0px; + padding-left: 0px; + padding-right: 0px; + opacity: 0.6; + position: relative; + transition: all ease-in-out 100ms; +} + +.form_ctn .delete_btn:hover { + color: red; + opacity: 1; +} + +.form_ctn .delete_btn div { + width: 15px; + border-radius: 0px !important; + position: absolute; + right: 0px; + top: 12px; + opacity: -10; + transition: all ease-in-out 100ms; +} + +.form_ctn .delete_btn:hover div { + right: -18px; + opacity: 1; +} + +.form_ctn .delete_confirm_btn { + padding-left: 20px; + padding-right: 20px; + border: 1px solid var(--primary-text-color); + background-color: var(--primary-bg-color); + color: red; + transition: all ease-in-out 100ms; + font-weight: 600; +} + +.delete_button_form_ctn { + word-break: normal; +} + +.form_ctn .delete_confirm_btn:hover { + border: 1px solid var(--primary-text-color); + background-color: rgb(149, 0, 0); + color: var(--secondary-bg-color); +} +.tooltiptext { + visibility: hidden; + opacity: 0; + background-color: var(--secondary-bg-color); + color: var(--primary-text-color); + text-align: center; + padding: 2px 0px; + border-radius: 5px; + position: absolute; + z-index: 1; + bottom: -30px; + width: 80px; + right: -5px; + flex-basis: 1; + font-size: 12px; + z-index: 99999999; + box-sizing: border-box; + transition: all 100ms ease-in-out; +} + +.tooltiptext::before { + content: ''; + position: absolute; + left: 50%; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + transform: translate(-50%, -9px); + border-bottom: 7px solid var(--secondary-bg-color); +} + +.create_invoice:hover .tooltiptext { + visibility: visible; + bottom: -35px; + opacity: 1; +} + +.create_invoice_ctn { + position: relative; + position: sticky; + bottom: 0; + margin-left: auto; + float: right; +} + +.create_invoice { + background-color: var(--secondary-bg-color); + font-size: 44px; + display: inline-block; + border-radius: 100px; + padding: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all ease-in-out 100ms; + position: relative; + border: 1px solid var(--secondary-text-color); +} + +.invoice_view_item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid var(--primary-text-color); +} \ No newline at end of file diff --git a/package.json b/package.json index 905524440..cf144bd1d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "jest": "^29.7.0", "jest-mock-extended": "^2.0.6", "lint-staged": "^12.4.1", + "lucide-react": "^0.510.0", "nodemailer": "^6.9.15", "nodemon": "^2.0.4", "redis": "^4.5.1", diff --git a/yarn.lock b/yarn.lock index 3b5d3cade..fe7bc4ee3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5396,6 +5396,11 @@ lru-cache@^7.14.1: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.1.tgz" integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== +lucide-react@^0.510.0: + version "0.510.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.510.0.tgz#933b19d893b30ac5cb355e4b91eeefc13088caa3" + integrity sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q== + luxon@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz" From 1c3f7e5ff5556d3869226701ea8f1f0a6d3df195 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 4 Jun 2025 15:51:44 -0300 Subject: [PATCH 24/36] refactor: clean up --- components/Transaction/InvoiceModal.tsx | 6 +-- .../Transaction/PaybuttonTransactions.tsx | 54 ++++++++----------- pages/api/invoices/index.ts | 9 ++-- services/invoiceService.ts | 4 +- services/transactionService.ts | 6 ++- utils/validators.ts | 11 +--- 6 files changed, 36 insertions(+), 54 deletions(-) diff --git a/components/Transaction/InvoiceModal.tsx b/components/Transaction/InvoiceModal.tsx index 2bd054f66..55332bf0b 100644 --- a/components/Transaction/InvoiceModal.tsx +++ b/components/Transaction/InvoiceModal.tsx @@ -1,10 +1,10 @@ import React, { useState, useEffect, ReactElement } from 'react' import style from './transaction.module.css' import Button from 'components/Button' -import { CreateinvoicePOSTParameters } from 'utils/validators' +import { CreateInvoicePOSTParameters } from 'utils/validators' import axios from 'axios' -interface InvoiceData { +export interface InvoiceData { invoiceNumber: string amount: number recipientName: string @@ -74,7 +74,7 @@ export default function InvoiceModal ({ async function handleSubmit (e: React.FormEvent): Promise { e.preventDefault() - const payload: CreateinvoicePOSTParameters = { + const payload: CreateInvoicePOSTParameters = { ...formData, transactionId: transaction?.id } diff --git a/components/Transaction/PaybuttonTransactions.tsx b/components/Transaction/PaybuttonTransactions.tsx index 35a3842ce..bd548420c 100644 --- a/components/Transaction/PaybuttonTransactions.tsx +++ b/components/Transaction/PaybuttonTransactions.tsx @@ -10,7 +10,7 @@ import TableContainerGetter from '../TableContainer/TableContainerGetter' import { compareNumericString } from 'utils/index' import moment from 'moment-timezone' import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index' -import InvoiceModal from './InvoiceModal' +import InvoiceModal, { InvoiceData } from './InvoiceModal' import style from './transaction.module.css' interface IProps { @@ -39,16 +39,6 @@ function fetchTransactionsByPaybuttonId (paybuttonId: string): Function { } } -interface InvoiceData { - invoiceNumber: string - amount: number - recipientName: string - recipientAddress: string - description: string - customerName: string - customerAddress: string -} - const fetchNextInvoiceNumberByUserId = async (): Promise => { const response = await fetch('/api/invoices/invoiceNumber/', { headers: { @@ -62,16 +52,16 @@ const fetchNextInvoiceNumberByUserId = async (): Promise => { export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => { const [isModalOpen, setIsModalOpen] = useState(false) const [invoiceData, setInvoiceData] = useState(null) - const [invoiceDataTransaction, setInvoiceDataTransaction] = useState(null) + const [invoiceDataTransaction, setInvoiceDataTransaction] = useState(null) const [localRefreshCount, setLocalRefreshCount] = useState(tableRefreshCount) const [invoiceMode, setInvoiceMode] = useState<'create' | 'edit' | 'view'>('create') - const onCreateInvoice = async (transaction: any): Promise => { + const onCreateInvoice = async (transaction: TransactionWithAddressAndPricesAndInvoices): Promise => { const nextInvoiceNumber = await fetchNextInvoiceNumberByUserId() const invoiceData = { invoiceNumber: nextInvoiceNumber ?? '', - amount: transaction.amount, + amount: Number(transaction.amount), recipientName: '', recipientAddress: transaction.address.address, description: '', @@ -84,14 +74,14 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom setIsModalOpen(true) } - const onEditInvoice = (transaction: any): void => { - setInvoiceData(transaction.invoices[0]) + const onEditInvoice = (invoiceData: InvoiceData): void => { + setInvoiceData(invoiceData) setInvoiceMode('edit') setIsModalOpen(true) } - const onSeeInvoice = (transaction: any): void => { - setInvoiceData(transaction.invoices[0]) + const onSeeInvoice = (invoiceData: InvoiceData): void => { + setInvoiceData(invoiceData) setInvoiceMode('view') setIsModalOpen(true) } @@ -162,6 +152,18 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom ) } }, + { + Header: () => (
Address
), + accessor: 'address.address', + shrinkable: true, + Cell: (cellProps) => { + return ( +
+ {cellProps.cell.value} +
+ ) + } + }, { Header: () => (
Actions
), id: 'actions', @@ -190,14 +192,14 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom : ( <>
New button
) : ( <> - - +
+ +
+
+ +
)} diff --git a/components/Transaction/transaction.module.css b/components/Transaction/transaction.module.css index c8332c071..dff9198fc 100644 --- a/components/Transaction/transaction.module.css +++ b/components/Transaction/transaction.module.css @@ -341,4 +341,31 @@ body[data-theme='dark'] .form_ctn select:not([multiple]) { justify-content: space-between; padding: 10px; border-bottom: 1px solid var(--primary-text-color); +} + +body[data-theme='dark'] .create_invoice img { + filter: invert(1); +} + +body[data-theme='dark'] .edit_invoice img { + filter: invert(1); +} + +body[data-theme='dark'] .see_invoice img { + filter: invert(1); +} + +.create_invoice_ctn:hover .create_invoice { + filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg) + brightness(100%) contrast(102%); +} + +.edit_invoice_ctn:hover .edit_invoice{ + filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg) + brightness(100%) contrast(102%); +} + +.see_invoice_ctn:hover .see_invoice { + filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg) + brightness(100%) contrast(102%); } \ No newline at end of file diff --git a/package.json b/package.json index cf144bd1d..905524440 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "jest": "^29.7.0", "jest-mock-extended": "^2.0.6", "lint-staged": "^12.4.1", - "lucide-react": "^0.510.0", "nodemailer": "^6.9.15", "nodemon": "^2.0.4", "redis": "^4.5.1", diff --git a/pages/api/invoices/index.ts b/pages/api/invoices/index.ts index 6c2ee3415..a63deb1af 100644 --- a/pages/api/invoices/index.ts +++ b/pages/api/invoices/index.ts @@ -1,6 +1,6 @@ import { setSession } from 'utils/setSession' import { RESPONSE_MESSAGES } from 'constants/index' -import { CreateInvoiceParams, createInvoice } from 'services/invoiceService' +import { CreateInvoiceParams, UpdateInvoiceParams, createInvoice, updateInvoice } from 'services/invoiceService' export default async ( req: any, @@ -27,5 +27,25 @@ export default async ( res.status(500).json({ statusCode: 500, message: err.message }) } } + } else if (req.method === 'PUT') { + try { + const updateInvoiceParams: UpdateInvoiceParams = { + ...req.body + } + const invoiceId = req.query.invoiceId as string + const invoice = await updateInvoice(session.userId, + invoiceId, updateInvoiceParams) + res.status(200).json({ + invoice + }) + } catch (err: any) { + switch (err.message) { + case RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400.message: + res.status(400).json(RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400) + break + default: + res.status(500).json({ statusCode: 500, message: err.message }) + } + } } } diff --git a/services/invoiceService.ts b/services/invoiceService.ts index a9e9dcce0..509ae4845 100644 --- a/services/invoiceService.ts +++ b/services/invoiceService.ts @@ -14,7 +14,7 @@ export interface CreateInvoiceParams { customerAddress: string } -interface UpdateInvoiceParams { +export interface UpdateInvoiceParams { description: string recipientName: string recipientAddress: string @@ -30,7 +30,7 @@ export async function createInvoice (params: CreateInvoiceParams): Promise { +export async function getUserInvoices (userId: string): Promise { return await prisma.invoice.findMany({ where: { userId From 7e87d8ebd4cbce053172ca07c54f5d872a2d4b17 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Tue, 10 Jun 2025 12:14:25 -0300 Subject: [PATCH 27/36] refactor: clean up --- services/invoiceService.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/services/invoiceService.ts b/services/invoiceService.ts index 509ae4845..8b4d053a8 100644 --- a/services/invoiceService.ts +++ b/services/invoiceService.ts @@ -94,13 +94,8 @@ export async function getNewInvoiceNumber (userId: string): Promise defaultPattern.test(invoice.invoiceNumber)) - if (invoicesWithOurPattern === null) { - const invoicesCount = await prisma.invoice.count({ - where: { - userId - } - }) - if (invoicesCount > 0) { + if (invoicesWithOurPattern === null || invoicesWithOurPattern.length < userInvoices.length) { + if (userInvoices.length > 0) { return } } From 8dc874ee06a3fd210f996e167700700312f05c4e Mon Sep 17 00:00:00 2001 From: lissavxo Date: Fri, 13 Jun 2025 11:47:42 -0300 Subject: [PATCH 28/36] feat: invoice transaction optional --- prisma/migrations/20250515150215_invoices/migration.sql | 2 +- prisma/schema.prisma | 4 ++-- services/invoiceService.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/prisma/migrations/20250515150215_invoices/migration.sql b/prisma/migrations/20250515150215_invoices/migration.sql index 87c6e0f3d..d62e6efd2 100644 --- a/prisma/migrations/20250515150215_invoices/migration.sql +++ b/prisma/migrations/20250515150215_invoices/migration.sql @@ -3,7 +3,7 @@ CREATE TABLE `Invoice` ( `id` VARCHAR(191) NOT NULL, `userId` VARCHAR(191) NOT NULL, `invoiceNumber` VARCHAR(191) NOT NULL, - `transactionId` VARCHAR(191) NOT NULL, + `transactionId` VARCHAR(191) NULL, `amount` DECIMAL(65, 30) NOT NULL, `description` VARCHAR(191) NOT NULL, `recipientName` VARCHAR(191) NOT NULL, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index be7c54465..cdfc7fa0c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -250,8 +250,8 @@ model Invoice { id String @id @default(uuid()) userId String invoiceNumber String - transactionId String - transaction Transaction @relation(fields: [transactionId], references: [id], onUpdate: Cascade, onDelete: Restrict) + transactionId String? + transaction Transaction? @relation(fields: [transactionId], references: [id], onUpdate: Cascade, onDelete: Restrict) userProfile UserProfile @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Restrict) amount Decimal diff --git a/services/invoiceService.ts b/services/invoiceService.ts index 8b4d053a8..3d02acd03 100644 --- a/services/invoiceService.ts +++ b/services/invoiceService.ts @@ -4,7 +4,7 @@ import { RESPONSE_MESSAGES } from 'constants/index' export interface CreateInvoiceParams { userId: string - transactionId: string + transactionId?: string invoiceNumber: string amount: number description: string From 6dd4900f0438e82ee16e5ee7dab501f47008474a Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 18 Jun 2025 12:51:59 -0300 Subject: [PATCH 29/36] refactor: clean up --- services/invoiceService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/invoiceService.ts b/services/invoiceService.ts index 3d02acd03..ae03e49a2 100644 --- a/services/invoiceService.ts +++ b/services/invoiceService.ts @@ -95,9 +95,7 @@ export async function getNewInvoiceNumber (userId: string): Promise defaultPattern.test(invoice.invoiceNumber)) if (invoicesWithOurPattern === null || invoicesWithOurPattern.length < userInvoices.length) { - if (userInvoices.length > 0) { - return - } + return } const invoiceWithTheLatestInvoiceNumber = invoicesWithOurPattern[0] From b7efd58e040b2beb215d5765390f9145fb6cc196 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 19 Jun 2025 16:21:36 -0300 Subject: [PATCH 30/36] feat: use prisma decimal --- components/Transaction/InvoiceModal.tsx | 3 ++- services/invoiceService.ts | 3 ++- utils/validators.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/Transaction/InvoiceModal.tsx b/components/Transaction/InvoiceModal.tsx index befd64c26..82fcb6394 100644 --- a/components/Transaction/InvoiceModal.tsx +++ b/components/Transaction/InvoiceModal.tsx @@ -3,10 +3,11 @@ import style from './transaction.module.css' import Button from 'components/Button' import { CreateInvoicePOSTParameters } from 'utils/validators' import axios from 'axios' +import { Prisma } from '@prisma/client' export interface InvoiceData { id?: string - invoiceNumber: string + invoiceNumber: Prisma.Decimal amount: number recipientName: string recipientAddress: string diff --git a/services/invoiceService.ts b/services/invoiceService.ts index ae03e49a2..1ab5dfc18 100644 --- a/services/invoiceService.ts +++ b/services/invoiceService.ts @@ -1,3 +1,4 @@ +import { Decimal } from '@prisma/client/runtime/library' import prisma from 'prisma/clientInstance' import { Invoice } from '@prisma/client' import { RESPONSE_MESSAGES } from 'constants/index' @@ -6,7 +7,7 @@ export interface CreateInvoiceParams { userId: string transactionId?: string invoiceNumber: string - amount: number + amount: Decimal description: string recipientName: string recipientAddress: string diff --git a/utils/validators.ts b/utils/validators.ts index 3f4ca0f11..8d6f18348 100644 --- a/utils/validators.ts +++ b/utils/validators.ts @@ -553,7 +553,7 @@ export const parseUpdateUserTimezonePUTRequest = function (params: UpdateUserTim export interface CreateInvoicePOSTParameters { transactionId: string - amount: number + amount: Prisma.Decimal description: string recipientName: string recipientAddress: string From 8d385ba5877bbaef1642e9163e71ffc6f21707d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 19 Jun 2025 21:27:42 -0300 Subject: [PATCH 31/36] fix: button detail for 0 txs --- .../TableContainer/TableContainerGetter.tsx | 16 ++++++++-------- components/Transaction/PaybuttonTransactions.tsx | 4 ++-- services/transactionService.ts | 4 ---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/components/TableContainer/TableContainerGetter.tsx b/components/TableContainer/TableContainerGetter.tsx index c2b4e8e6c..62f794c03 100644 --- a/components/TableContainer/TableContainerGetter.tsx +++ b/components/TableContainer/TableContainerGetter.tsx @@ -3,7 +3,7 @@ import { useTable, usePagination } from 'react-table' import { DEFAULT_EMPTY_TABLE_MESSAGE } from 'constants/index' import style from './table-container.module.css' -interface DataGetterReturn { +export interface DataGetterReturn { data: any totalCount: number } @@ -35,7 +35,7 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp const [pageCount, setPageCount] = useState(0) const [loading, setLoading] = useState(true) const emptyMessageDisplay = emptyMessage ?? DEFAULT_EMPTY_TABLE_MESSAGE - const [hiddenColumns, setHiddenColumns] = useState({}) + const [hiddenColumns, setHiddenColumns] = useState>({}) const triggerSort = (column: any): void => { if (column.disableSortBy === true || hiddenColumns[column.id]) return @@ -49,9 +49,9 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp } gotoPage(0) } - + const toggleColumn = (id: any): void => { - setHiddenColumns((prev) => ({ ...prev, [id]: !prev[id]})) + setHiddenColumns((prev) => ({ ...prev, [id]: !prev[id] })) } const { @@ -122,12 +122,12 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp { triggerSort(column) }}>
{column.render('Header')} - {column.shrinkable && ( + {column.shrinkable === true && ( toggleColumn(column.id)} style={{ cursor: 'pointer' }}> - {hiddenColumns[column.id] ?
:
} + {hiddenColumns[column.id] ?
:
} )} - {!column.shrinkable && generateSortingIndicator(column)} + {column.shrinkable !== true && generateSortingIndicator(column)}
))} @@ -143,7 +143,7 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp return ( {row.cells.map((cell: any) => - hiddenColumns[cell.column.id] ? : {cell.render('Cell')} + hiddenColumns[cell.column.id] ? : {cell.render('Cell')} )} ) diff --git a/components/Transaction/PaybuttonTransactions.tsx b/components/Transaction/PaybuttonTransactions.tsx index e05814d49..94b5685f3 100644 --- a/components/Transaction/PaybuttonTransactions.tsx +++ b/components/Transaction/PaybuttonTransactions.tsx @@ -9,7 +9,7 @@ import Plus from 'assets/plus.png' import Pencil from 'assets/pencil.png' import FileText from 'assets/file-text.png' -import TableContainerGetter from '../TableContainer/TableContainerGetter' +import TableContainerGetter, { DataGetterReturn } from '../TableContainer/TableContainerGetter' import { compareNumericString } from 'utils/index' import moment from 'moment-timezone' import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index' @@ -26,7 +26,7 @@ interface IProps { timezone: string } -function fetchTransactionsByPaybuttonId (paybuttonId: string): Function { +function fetchTransactionsByPaybuttonId (paybuttonId: string): (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => Promise { return async (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => { const response = await fetch(`/api/paybutton/transactions/${paybuttonId}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`, { headers: { diff --git a/services/transactionService.ts b/services/transactionService.ts index 06a1a2649..f43d0a0fd 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -587,10 +587,6 @@ export async function fetchTransactionsByPaybuttonIdWithPagination ( orderDesc, networkIds) - if (transactions.length === 0) { - throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) - } - return transactions } From 63fa2a2d2fdb141fc592962a20e7c89659f9daba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 18 Jul 2025 15:52:20 -0300 Subject: [PATCH 32/36] fix: missing comma --- config/example-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/example-config.json b/config/example-config.json index 12f24a115..1ba97b69b 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -38,7 +38,7 @@ "6": 30, "12": 50 }, - "payoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" + "payoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un", "standardDailyEmailLimit": 5, "proDailyEmailLimit": 100, "standardAddressesPerButtonLimit": 20, From 9b1e07fce5a8c184e89c17ed17f86abdca1c8820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 5 Aug 2025 19:05:00 -0300 Subject: [PATCH 33/36] refactor: prodisplay modal rewording --- components/Account/ProDisplay.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index b99d7c4f4..f352b2165 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -50,7 +50,6 @@ const ProConfig = (): JSX.Element => { {isPro === false && } {infoModal && (
-

PayButton Pro users get higher limits:

@@ -61,15 +60,20 @@ const ProConfig = (): JSX.Element => { - - - + + + + + + + +
Daily Emails{showLimit(config.proSettings.standardDailyEmailLimit)}{showLimit(config.proSettings.proDailyEmailLimit)}Outgoing Emails on Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
Addresses Per Button {showLimit(config.proSettings.standardAddressesPerButtonLimit)} {showLimit(config.proSettings.proAddressesPerButtonLimit)}
Outgoing Server-to-Server Messages On Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
From a1c80d16c43cc7098f7e5362a8e54bc8ec60786c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 5 Aug 2025 19:08:47 -0300 Subject: [PATCH 34/36] refactor: display pro info all the time --- components/Account/ProDisplay.tsx | 66 +++++++++++++------------------ 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/components/Account/ProDisplay.tsx b/components/Account/ProDisplay.tsx index f352b2165..5339d736a 100644 --- a/components/Account/ProDisplay.tsx +++ b/components/Account/ProDisplay.tsx @@ -7,7 +7,6 @@ import config from 'config/index' const ProConfig = (): JSX.Element => { const [text, setText] = useState('') const [isPro, setIsPro] = useState() - const [infoModal, setInfoModal] = useState(false) const showLimit = (configLimit: number | 'Inf'): string => { return configLimit === 'Inf' ? 'Unlimited' : configLimit.toString() @@ -40,45 +39,36 @@ const ProConfig = (): JSX.Element => {
{text} -
setInfoModal(!infoModal)} - className={stylep.whats_this_btn} - > - {infoModal ? 'Close' : 'What is this?'} -
{isPro === false && } - {infoModal && ( -
- - - - - - - - - - - - - - - - - - - - - - - - - -
StandardPro
Outgoing Emails on Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
Addresses Per Button{showLimit(config.proSettings.standardAddressesPerButtonLimit)}{showLimit(config.proSettings.proAddressesPerButtonLimit)}
Outgoing Server-to-Server Messages On Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
- -
- )} +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
StandardPro
Outgoing Emails on Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
Addresses Per Button{showLimit(config.proSettings.standardAddressesPerButtonLimit)}{showLimit(config.proSettings.proAddressesPerButtonLimit)}
Outgoing Server-to-Server Messages On Payment{showLimit(config.proSettings.standardDailyEmailLimit)} / day{showLimit(config.proSettings.proDailyEmailLimit)} / day
+
} From 57e8c01a9ef79e53ae3c21a7ee9761c8b94a93f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 6 Aug 2025 13:29:25 -0300 Subject: [PATCH 35/36] chore: remove defaults from README.md --- README.md | 48 +----------------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/README.md b/README.md index 07aa81737..55d3f5ccd 100644 --- a/README.md +++ b/README.md @@ -75,14 +75,13 @@ For example, `yarn test` runs the `test` script, but this won't work properly wh ### Optional configuration -PayButton Server is configured with a `paybutton-config.json` file in the root of the repository. An example file can be find at [config/example-config.json](https://github.com/PayButton/paybutton-server/blob/master/config/example-config.json). The values it takes are: +PayButton Server is configured with a `paybutton-config.json` file in the root of the repository. An example file with the default values can be find at [config/example-config.json](https://github.com/PayButton/paybutton-server/blob/master/config/example-config.json). The values it takes are: --- #### **apiDomain** ``` type: string -default: "http://localhost:3000/api", ``` > Base path for the API. @@ -90,7 +89,6 @@ default: "http://localhost:3000/api", #### apiBasePath ``` type: string -default: "/api/auth" ``` > Base API endpoint for authentication. @@ -98,14 +96,12 @@ default: "/api/auth" #### websiteBasePath ``` type: string -default: "/auth", ``` > Base API endpoint for authentication through SuperTokens. #### websiteDomain ``` type: string -default: "http://localhost:3000" ``` > Base path for the website. @@ -113,7 +109,6 @@ default: "http://localhost:3000" #### wsBaseURL ``` type: string -default: "http://localhost:5000" ``` > Base path for the websocket server. @@ -121,7 +116,6 @@ default: "http://localhost:5000" #### showTestNetworks ``` type: boolean -default: false, ``` > If the connection of test networks for eCash and Bitcoin Cash should appear in the Networks tab. @@ -140,14 +134,12 @@ type: { #### priceAPIURL ``` type: string -default: "https://coin.dance/api/" ``` > API to get prices from. Only coin.dance currently supported. #### redisURL ``` type: string -default: "redis://paybutton-cache:6379" ``` > URL for the Redis server. @@ -158,10 +150,6 @@ type: { "ecash": "chronik", "bitcoincash": "chronik" } -default: { - "ecash": "chronik", - "bitcoincash": "chronik" -} ``` > Which client to use to get the blockchain information for each network. Currently, only "chronik" is supported for eCash and Bitcoin Cash. @@ -172,10 +160,6 @@ type: { "ecash": boolean "bitcoincash": boolean } - -default: { - "bitcoincash": true -} ``` > What networks are currently under maintenance. @@ -183,14 +167,12 @@ default: { #### triggerPOSTTimeout ``` type: number -default: 3000 ``` > How long a POST request triggered from a button payment will wait for an answer to be marked as successful. #### smtpHost ``` type: string -default: N/A ``` > Host name for the server from which payment trigger emails will be sent. Not setting this up will result in email triggers not working. @@ -198,7 +180,6 @@ default: N/A #### smtpPort ``` type: number -default: N/A ``` > Port for the SMTP server from which payment trigger emails will be sent. Not setting this up will result in email triggers not working. @@ -206,7 +187,6 @@ default: N/A #### sideshiftAffiliateId ``` type: string -default: N/A ``` > Necessary only for paybutton client to interact with sideshift through the server. @@ -223,27 +203,12 @@ type: { standardAddressesPerButtonLimit: number | "Inf", proAddressesPerButtonLimit: number | "Inf" } -default: { - "enabled": true, - "monthsCost": { - "1": 10, - "3": 20, - "6": 30, - "12": 50 - }, - "payoutAddress": "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" - "standardDailyEmailLimit": 5, - "proDailyEmailLimit": 100, - "standardAddressesPerButtonLimit": 20, - "proAddressesPerButtonLimit": "Inf" -} ``` > General configuration for PayButton Pro. Each parameter is described below. ##### proSettings.enabled ``` type: boolean -default: true ``` > If the Pro feature should be enabled or hidden. @@ -252,47 +217,36 @@ default: true type: { [key: string]: number } -default: { -"1": 10, -"3": 20, -"6": 30, -"12": 50 -} ``` > The pricing model for PayButton Pro subscription — [value] USD for [key] months. ##### proSettings.payoutAddress ``` type: string -default: "ecash:qrf4zh4vgrdal8d8gu905d90w5u2y60djcd2d5h6un" ``` > The payout address for PayButton Pro subscriptions. ##### proSettings.standardDailyEmailLimit ``` type: number | "Inf" -default: 5 ``` > How many emails can a standard user send daily. ##### proSettings.proDailyEmailLimit ``` type: number | "Inf" -default: 20 ``` > How many emails can a PayButton Pro user send daily. ##### proSettings.standardAddressesPerButtonLimit ``` type: number | "Inf" -default: 5 ``` > How many addresses can a standard Pro user add for a single button. ##### proSettings.proAddressesPerButtonLimit ``` type: number | "Inf" -default: "Inf" ``` > How many addresses can a PayButton Pro user add for a single button. From 2930c901f11539c0d800d9fd4b65ecfa789d7c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Wed, 6 Aug 2025 13:30:46 -0300 Subject: [PATCH 36/36] chore: remove redundant type explanation --- README.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/README.md b/README.md index 55d3f5ccd..1e70d9fc4 100644 --- a/README.md +++ b/README.md @@ -192,17 +192,7 @@ type: string #### proSettings ``` -type: { - enabled: boolean, - monthsCost: { - [key: string]: number - }, - payoutAddress: string, - standardDailyEmailLimit: number | "Inf", - proDailyEmailLimit: number | "Inf", - standardAddressesPerButtonLimit: number | "Inf", - proAddressesPerButtonLimit: number | "Inf" -} +type: object ``` > General configuration for PayButton Pro. Each parameter is described below.