diff --git a/README.md b/README.md index ec8c10f68..cdebf9341 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Please read the [contribution guidelines](.github/contributing.md) before submit - [Docker](https://docs.docker.com/get-docker/) - [NVM](https://github.com/nvm-sh/nvm) -- [Rapyd](https://www.rapyd.net) account in Sandbox mode +- [GateHub](https://sandbox.gatehub.net) account in Sandbox mode ### Environment Setup @@ -86,7 +86,7 @@ cp ./docker/dev/.env.example ./docker/dev/.env ``` Using your preferred text editor, open the `./docker/dev/.env` file and configure the necessary environment variables. -The `RAPYD_ACCESS_KEY` and `RAPYD_SECRET_KEY` variables values can be found in your Rapyd Sandbox account (you need to create an account at [rapyd.net](https://www.rapyd.net)), under the Developers menu item. The `RAPYD_SETTLEMENT_EWALLET` variable value can be found in your Rapyd Sandbox account details. +The `GATEHUB` related environment variables are necessary in order to complete Sandbox KYC, and add play money to your account. In order to have the correct variables, create a `GateHub` Sandbox account. Optionally you could send an email to `timea@interledger.foundation` and request these variables. To create a new Interledger Test Wallet account, a verification email will be sent to the provided email address. If you want to send emails within the development environment, you will need to have a personal Sendgrid account and update the following environment variables: `SEND_EMAIL` to `true`, `SENDGRID_API_KEY` and `FROM_EMAIL`. If you prefer not to send emails in the development environment, simply set `SEND_EMAIL` to `false` and use the verification link found in the Docker `wallet-backend` container logs to finalize the registration process for a new user. @@ -95,14 +95,7 @@ To enable rate limiter on the wallet for security purposes you can set these env Cross-currency transactions are supported. To enable this functionality, you will need to register at [freecurrencyapi.com/](https://freecurrencyapi.com/) and update the `RATE_API_KEY` environment variable with your own API key. Currencies can be added in the `admin` environment. For example `assetCode` is `EUR`, `assetScale` is `2`, and you will need to add an amount to `liquidity`. -To have everything ready for `DEV` environment, we already set up some default values for Interledger Test Wallet, this way developers are ready to login without validation, and test e-commerce application without any additional setup: - -- a `USD` asset set by default in the `admin` environment -- a user with email address `dev@email.com` and password `123456`, with a `USD` account, payment pointer and test money -- a user with email address `boutique@email.com` and password `123456`, with a `USD` account and a payment pointer `boutique`, which is used as a receiver payment pointer at the e-commerce application -- developer keys for the `boutique` payment pointer, these values will be copied to `.env` file from `.env.example`, as mentioned above - -If you would like to set up e-commerce application manually for another payment pointer, you will need to create a USD payment pointer, then generate public and private key for the payment pointer in the `Developer Keys` found in the `Settings` menu of Interledger Test Wallet. You also need to update the following environment variables: `PRIVATE_KEY` to the generated base64 encoded private key, `KEY_ID` to the payment pointer key id and `PAYMENT_POINTER` to the created payment pointer address. +If you would like to set up e-commerce application, you will need to create a USD payment pointer, then generate public and private key for the payment pointer in the `Developer Keys` found in the `Settings` menu of Interledger Test Wallet. You also need to update the following environment variables: `PRIVATE_KEY` to the generated base64 encoded private key, `KEY_ID` to the payment pointer key id and `PAYMENT_POINTER` to the created payment pointer address. ### Local Playground diff --git a/docker/dev/.env.example b/docker/dev/.env.example index defbf5450..ec9c444b1 100644 --- a/docker/dev/.env.example +++ b/docker/dev/.env.example @@ -9,11 +9,12 @@ GATEHUB_GATEWAY_UUID= GATEHUB_VAULT_UUID_EUR= GATEHUB_VAULT_UUID_USD= GATEHUB_SETTLEMENT_WALLET_ADDRESS= +GATEHUB_CARD_APP_ID= RATE_LIMIT= RATE_LIMIT_LEVEL= # commerce env variables # encoded base 64 private key -PRIVATE_KEY=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUI0YzgyOVlSMDZCTUhtQmpIVTNrOHZmMEZhVEFvOHNCTzIrRFZwY1lPSk0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQ== -KEY_ID=32499ede-78d9-424d-a43e-3796ee07b60c -PAYMENT_POINTER=https://rafiki-backend/boutique +PRIVATE_KEY= +KEY_ID= +PAYMENT_POINTER= diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 34699f29b..70c2cae79 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -55,6 +55,7 @@ services: GATEHUB_VAULT_UUID_EUR: ${GATEHUB_VAULT_UUID_EUR} GATEHUB_VAULT_UUID_USD: ${GATEHUB_VAULT_UUID_USD} GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS} + GATEHUB_CARD_APP_ID: ${GATEHUB_CARD_APP_ID} RATE_LIMIT: ${RATE_LIMIT} RATE_LIMIT_LEVEL: ${RATE_LIMIT_LEVEL} restart: always @@ -180,6 +181,7 @@ services: KRATOS_CONTAINER_PUBLIC_URL: 'http://kratos:4433' KRATOS_BROWSER_PUBLIC_URL: 'http://localhost:4433' KRATOS_ADMIN_URL: 'http://kratos:4434/admin' + AUTH_ENABLED: false <<: *logging kratos: @@ -199,7 +201,7 @@ services: - testnet tigerbeetle: - image: ghcr.io/tigerbeetle/tigerbeetle:0.16.2 + image: ghcr.io/tigerbeetle/tigerbeetle:0.16.3 privileged: true volumes: - tigerbeetle-data:/var/lib/tigerbeetle diff --git a/docker/prod/.env.example b/docker/prod/.env.example index cfef6d447..a25cded13 100644 --- a/docker/prod/.env.example +++ b/docker/prod/.env.example @@ -32,6 +32,7 @@ WALLET_BACKEND_GATEHUB_GATEWAY_UUID= WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR= WALLET_BACKEND_GATEHUB_VAULT_UUID_USD= WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS= +WALLET_BACKEND_GATEHUB_CARD_APP_ID= # BOUTIQUE BOUTIQUE_BACKEND_PORT= diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 0ae55c1ce..af6c85eab 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -27,7 +27,6 @@ services: args: PORT: ${WALLET_FRONTEND_PORT} COOKIE_NAME: ${WALLET_BACKEND_COOKIE_NAME} - NEXT_PUBLIC_USE_TEST_KYC_DATA: ${WALLET_FRONTEND_USE_TEST_KYC_DATA} NEXT_PUBLIC_BACKEND_URL: ${WALLET_FRONTEND_BACKEND_URL} NEXT_PUBLIC_OPEN_PAYMENTS_HOST: ${WALLET_FRONTEND_OPEN_PAYMENTS_HOST} NEXT_PUBLIC_AUTH_HOST: ${WALLET_FRONTEND_AUTH_HOST} @@ -67,6 +66,7 @@ services: GATEHUB_VAULT_UUID_EUR: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR} GATEHUB_VAULT_UUID_USD: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_USD} GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS} + GATEHUB_CARD_APP_ID: ${WALLET_BACKEND_GATEHUB_CARD_APP_ID} networks: - testnet ports: @@ -232,7 +232,7 @@ services: <<: *logging tigerbeetle: - image: ghcr.io/tigerbeetle/tigerbeetle:0.16.2 + image: ghcr.io/tigerbeetle/tigerbeetle:0.16.3 privileged: true volumes: - tigerbeetle-data:/var/lib/tigerbeetle diff --git a/package.json b/package.json index a3f8ac740..72ddda3b9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "only-allow": "^1.2.1", "prettier": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.6", + "prettier-plugin-tailwindcss": "^0.6.8", "typescript": "^5.6.2" }, "engines": { diff --git a/packages/boutique/backend/package.json b/packages/boutique/backend/package.json index f66d637c6..ed9022a6a 100644 --- a/packages/boutique/backend/package.json +++ b/packages/boutique/backend/package.json @@ -17,7 +17,7 @@ "express": "^4.21.0", "helmet": "^7.1.0", "knex": "^3.1.0", - "objection": "^3.1.4", + "objection": "^3.1.5", "pg": "^8.13.0", "winston": "^3.14.2", "zod": "^3.23.8" diff --git a/packages/boutique/frontend/package.json b/packages/boutique/frontend/package.json index df0058fe5..8f7a5496a 100644 --- a/packages/boutique/frontend/package.json +++ b/packages/boutique/frontend/package.json @@ -33,13 +33,13 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", - "@types/react": "18.3.8", + "@types/react": "18.3.10", "@types/react-dom": "18.3.0", "@vitejs/plugin-react-swc": "^3.7.0", "autoprefixer": "^10.4.20", "postcss": "^8.4.47", - "tailwindcss": "^3.4.12", + "tailwindcss": "^3.4.13", "typescript": "^5.6.2", - "vite": "^5.4.7" + "vite": "^5.4.8" } } diff --git a/packages/shared/backend/package.json b/packages/shared/backend/package.json index a5326f2a0..71f5a4839 100644 --- a/packages/shared/backend/package.json +++ b/packages/shared/backend/package.json @@ -24,7 +24,7 @@ "@google-cloud/logging-winston": "^6.0.0", "awilix": "^11.0.0", "express": "^4.21.0", - "objection": "^3.1.4", + "objection": "^3.1.5", "knex": "^3.1.0", "winston": "^3.14.2", "axios": "^1.7.7" diff --git a/packages/wallet/backend/migrations/20230317135105_create_users.js b/packages/wallet/backend/migrations/20230317135105_create_users.js index e23331bc6..a5277b890 100644 --- a/packages/wallet/backend/migrations/20230317135105_create_users.js +++ b/packages/wallet/backend/migrations/20230317135105_create_users.js @@ -12,8 +12,8 @@ exports.up = function (knex) { table.string('firstName') table.string('address') table.string('country') - table.string('kycId') // TODO: replace with GateHub kyc check + table.boolean('kycVerified').defaultTo(false) table.string('gateHubUserId') table.timestamp('createdAt').notNullable() diff --git a/packages/wallet/backend/migrations/20240923105930_update_wallet_addresses.js b/packages/wallet/backend/migrations/20240923105930_update_wallet_addresses.js new file mode 100644 index 000000000..8aee65995 --- /dev/null +++ b/packages/wallet/backend/migrations/20240923105930_update_wallet_addresses.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('walletAddresses', function (table) { + table.string('cardId').unique().nullable() + }) +} + +exports.down = function (knex) { + return knex.schema.table('walletAddresses', function (table) { + table.dropColumn('cardId') + }) +} diff --git a/packages/wallet/backend/package.json b/packages/wallet/backend/package.json index 8f6a8b106..19e17c120 100644 --- a/packages/wallet/backend/package.json +++ b/packages/wallet/backend/package.json @@ -29,7 +29,7 @@ "knex": "^3.1.0", "moment": "^2.30.1", "node-cache": "^5.1.2", - "objection": "^3.1.4", + "objection": "^3.1.5", "pg": "^8.13.0", "randexp": "^0.5.3", "rate-limiter-flexible": "^5.0.3", @@ -39,7 +39,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@faker-js/faker": "^9.0.1", + "@faker-js/faker": "^9.0.3", "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript-operations": "^4.2.3", diff --git a/packages/wallet/backend/src/account/service.ts b/packages/wallet/backend/src/account/service.ts index 70bc64631..8a34ef957 100644 --- a/packages/wallet/backend/src/account/service.ts +++ b/packages/wallet/backend/src/account/service.ts @@ -22,7 +22,7 @@ interface IAccountService { includeWalletKeys?: boolean ) => Promise getAccountById: (userId: string, accountId: string) => Promise - getAccountBalance: (userId: string, account: Account) => Promise + getAccountBalance: (account: Account) => Promise } export class AccountService implements IAccountService { @@ -109,10 +109,12 @@ export class AccountService implements IAccountService { const accounts = await query if (!includeWalletAddress) { - accounts.forEach(async (acc) => { - const balance = await this.getAccountBalance(userId, acc) - acc.balance = transformBalance(balance, acc.assetScale) - }) + await Promise.all( + accounts.map(async (acc) => { + const balance = await this.getAccountBalance(acc) + acc.balance = transformBalance(balance, acc.assetScale) + }) + ) } return accounts @@ -132,7 +134,7 @@ export class AccountService implements IAccountService { } account.balance = transformBalance( - await this.getAccountBalance(userId, account), + await this.getAccountBalance(account), account.assetScale ) @@ -152,26 +154,19 @@ export class AccountService implements IAccountService { } account.balance = transformBalance( - await this.getAccountBalance(userId, account), + await this.getAccountBalance(account), account.assetScale ) return account } - async getAccountBalance(userId: string, account: Account): Promise { - const user = await User.query().findById(userId) - - if (!user || !user.gateHubUserId) { - throw new NotFound() - } - + async getAccountBalance(account: Account): Promise { const balances = await this.gateHubClient.getWalletBalance( - account.gateHubWalletId, - userId + account.gateHubWalletId ) return Number( - balances.find((balance) => balance.vault.assetCode === account.assetCode) + balances.find((balance) => balance.vault.asset_code === account.assetCode) ?.total ?? 0 ) } diff --git a/packages/wallet/backend/src/app.ts b/packages/wallet/backend/src/app.ts index fd9da8da2..bdca5a4aa 100644 --- a/packages/wallet/backend/src/app.ts +++ b/packages/wallet/backend/src/app.ts @@ -42,6 +42,8 @@ import { initErrorHandler, RedisClient } from '@shared/backend' import { GateHubController } from '@/gatehub/controller' import { GateHubClient } from '@/gatehub/client' import { GateHubService } from '@/gatehub/service' +import { CardController } from './card/controller' +import { CardService } from './card/service' export interface Bindings { env: Env @@ -78,6 +80,8 @@ export interface Bindings { gateHubClient: GateHubClient gateHubController: GateHubController gateHubService: GateHubService + cardService: CardService + cardController: CardController } export class App { @@ -146,6 +150,7 @@ export class App { const accountController = await this.container.resolve('accountController') const rafikiController = await this.container.resolve('rafikiController') const gateHubController = await this.container.resolve('gateHubController') + const cardController = await this.container.resolve('cardController') app.use( cors({ @@ -303,6 +308,34 @@ export class App { // GateHub router.get('/iframe-urls/:type', isAuth, gateHubController.getIframeUrl) + router.post('/gatehub-webhooks', gateHubController.webhook) + router.post( + '/gatehub/add-user-to-gateway', + isAuth, + gateHubController.addUserToGateway + ) + + // Cards + router.get( + '/customers/:customerId/cards', + isAuth, + cardController.getCardsByCustomer + ) + router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails) + router.put('/cards/:cardId/lock', isAuth, cardController.lock) + router.put('/cards/:cardId/unlock', isAuth, cardController.unlock) + router.get( + '/cards/:cardId/transactions', + isAuth, + cardController.getCardTransactions + ) + router.get('/cards/:cardId/pin', isAuth, cardController.getPin) + router.post('/cards/:cardId/change-pin', isAuth, cardController.changePin) + router.put( + '/cards/:cardId/block', + isAuth, + cardController.permanentlyBlockCard + ) // Return an error for invalid routes router.use('*', (req: Request, res: CustomResponse) => { diff --git a/packages/wallet/backend/src/auth/controller.ts b/packages/wallet/backend/src/auth/controller.ts index 1ad4173c7..886416245 100644 --- a/packages/wallet/backend/src/auth/controller.ts +++ b/packages/wallet/backend/src/auth/controller.ts @@ -55,7 +55,7 @@ export class AuthController implements IAuthController { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await req.session.save() diff --git a/packages/wallet/backend/src/card/controller.ts b/packages/wallet/backend/src/card/controller.ts new file mode 100644 index 000000000..17b70ada7 --- /dev/null +++ b/packages/wallet/backend/src/card/controller.ts @@ -0,0 +1,190 @@ +import { Request, Response, NextFunction } from 'express' +import { Controller } from '@shared/backend' +import { CardService } from '@/card/service' +import { toSuccessResponse } from '@shared/backend' +import { + ICardDetailsRequest, + ICardDetailsResponse, + ICardLockRequest, + ICardResponse, + ICardUnlockRequest +} from './types' +import { IGetTransactionsResponse } from '@wallet/shared/src' +import { validate } from '@/shared/validate' +import { + getCardsByCustomerSchema, + getCardDetailsSchema, + lockCardSchema, + unlockCardSchema, + getCardTransactionsSchema, + changePinSchema, + permanentlyBlockCardSchema +} from './validation' + +export interface ICardController { + getCardsByCustomer: Controller + getCardDetails: Controller + getCardTransactions: Controller + getPin: Controller + changePin: Controller + lock: Controller + unlock: Controller + permanentlyBlockCard: Controller +} + +export class CardController implements ICardController { + constructor(private cardService: CardService) {} + + public getCardsByCustomer = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const { params } = await validate(getCardsByCustomerSchema, req) + const { customerId } = params + + const cards = await this.cardService.getCardsByCustomer(customerId) + res.status(200).json(toSuccessResponse(cards)) + } catch (error) { + next(error) + } + } + + public getCardDetails = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const { params, query } = await validate(getCardDetailsSchema, req) + const { cardId } = params + const { publicKeyBase64 } = query + + const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 } + const cardDetails = await this.cardService.getCardDetails( + userId, + requestBody + ) + res.status(200).json(toSuccessResponse(cardDetails)) + } catch (error) { + next(error) + } + } + + public getCardTransactions = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const { params, query } = await validate(getCardTransactionsSchema, req) + const { cardId } = params + const { pageSize, pageNumber } = query + + const transactions = await this.cardService.getCardTransactions( + userId, + cardId, + pageSize, + pageNumber + ) + + res.status(200).json(toSuccessResponse(transactions)) + } catch (error) { + next(error) + } + } + + public getPin = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.session.user.id + const { params, query } = await validate(getCardDetailsSchema, req) + const { cardId } = params + const { publicKeyBase64 } = query + + const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 } + const cardPin = await this.cardService.getPin(userId, requestBody) + res.status(200).json(toSuccessResponse(cardPin)) + } catch (error) { + next(error) + } + } + + public changePin = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const { params, body } = await validate(changePinSchema, req) + const { cardId } = params + const { cypher } = body + + const result = await this.cardService.changePin(userId, cardId, cypher) + res.status(201).json(toSuccessResponse(result)) + } catch (error) { + next(error) + } + } + + public lock = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.session.user.id + const { params, query, body } = await validate(lockCardSchema, req) + const { cardId } = params + const { reasonCode } = query + const requestBody: ICardLockRequest = body + + const result = await this.cardService.lock( + userId, + cardId, + reasonCode, + requestBody + ) + + res.status(200).json(toSuccessResponse(result)) + } catch (error) { + next(error) + } + } + + public unlock = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.session.user.id + const { params, body } = await validate(unlockCardSchema, req) + const { cardId } = params + const requestBody: ICardUnlockRequest = body + + const result = await this.cardService.unlock(userId, cardId, requestBody) + + res.status(200).json(toSuccessResponse(result)) + } catch (error) { + next(error) + } + } + + public permanentlyBlockCard = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const { params, query } = await validate(permanentlyBlockCardSchema, req) + const { cardId } = params + const { reasonCode } = query + + const result = await this.cardService.permanentlyBlockCard( + userId, + cardId, + reasonCode + ) + res.status(200).json(toSuccessResponse(result)) + } catch (error) { + next(error) + } + } +} diff --git a/packages/wallet/backend/src/card/service.ts b/packages/wallet/backend/src/card/service.ts new file mode 100644 index 000000000..a3a72e60c --- /dev/null +++ b/packages/wallet/backend/src/card/service.ts @@ -0,0 +1,109 @@ +import { WalletAddressService } from '@/walletAddress/service' +import { GateHubClient } from '../gatehub/client' +import { + ICardDetailsRequest, + ICardDetailsResponse, + ICardLockRequest, + ICardResponse, + ICardUnlockRequest +} from './types' +import { IGetTransactionsResponse } from '@wallet/shared/src' +import { LockReasonCode } from '@wallet/shared/src' +import { NotFound } from '@shared/backend' +import { BlockReasonCode } from '@wallet/shared/src' + +export class CardService { + constructor( + private gateHubClient: GateHubClient, + private walletAddressService: WalletAddressService + ) {} + + async getCardsByCustomer(customerId: string): Promise { + return this.gateHubClient.getCardsByCustomer(customerId) + } + + async getCardDetails( + userId: string, + requestBody: ICardDetailsRequest + ): Promise { + const { cardId } = requestBody + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.getCardDetails(requestBody) + } + + async getCardTransactions( + userId: string, + cardId: string, + pageSize?: number, + pageNumber?: number + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.getCardTransactions(cardId, pageSize, pageNumber) + } + + async getPin( + userId: string, + requestBody: ICardDetailsRequest + ): Promise { + const { cardId } = requestBody + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.getPin(requestBody) + } + + async changePin( + userId: string, + cardId: string, + cypher: string + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + await this.gateHubClient.changePin(cardId, cypher) + } + + async lock( + userId: string, + cardId: string, + reasonCode: LockReasonCode, + requestBody: ICardLockRequest + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.lockCard(cardId, reasonCode, requestBody) + } + + async unlock( + userId: string, + cardId: string, + requestBody: ICardUnlockRequest + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.unlockCard(cardId, requestBody) + } + + async permanentlyBlockCard( + userId: string, + cardId: string, + reasonCode: BlockReasonCode + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.permanentlyBlockCard(cardId, reasonCode) + } + + private async ensureWalletAddressExists( + userId: string, + cardId: string + ): Promise { + const walletAddress = await this.walletAddressService.getByCardId( + userId, + cardId + ) + if (!walletAddress) { + throw new NotFound('Card not found or not associated with the user.') + } + } +} diff --git a/packages/wallet/backend/src/card/types.ts b/packages/wallet/backend/src/card/types.ts new file mode 100644 index 000000000..1d8c96b2c --- /dev/null +++ b/packages/wallet/backend/src/card/types.ts @@ -0,0 +1,104 @@ +export interface ICardDetailsRequest { + cardId: string + publicKeyBase64: string +} + +export interface ICardDetailsResponse { + cipher: string | null +} + +export interface ILinksResponse { + token: string | null + links: Array<{ + href: string | null + rel: string | null + method: string | null + }> | null +} + +export interface ICreateCustomerRequest { + emailAddress: string + account: { + productCode: string + } + card: { + productCode: string + } + user: { + firstName: string + lastName: string + mobileNumber?: string + nationalIdentifier?: string + } + identification: { + documents: Array<{ + type: string + file: string // Base64-encoded file content + }> + } + address: { + addressLine1: string + addressLine2?: string + city: string + region?: string + postalCode: string + countryCode: string + } +} + +export interface ICreateCustomerResponse { + customerId: string + accountId: string + cardId: string +} + +export interface ICardResponse { + sourceId: string + nameOnCard: string + productCode: string + id: string + accountId: string + accountSourceId: string + maskedPan: string + status: string + statusReasonCode: string | null + lockLevel: string | null + expiryDate: string + customerId: string + customerSourceId: string +} + +export type CardLimitType = + | 'perTransaction' + | 'dailyOverall' + | 'weeklyOverall' + | 'monthlyOverall' + | 'dailyAtm' + | 'dailyEcomm' + | 'monthlyOpenScheme' + | 'nonEUPayments' + +export interface ICardProductLimit { + type: CardLimitType + currency: string + limit: string + isDisabled: boolean +} + +export interface ICardProductResponse { + cardProductLimits: ICardProductLimit[] + deletedAt: string | null + uuid: string + accountProductCode: string + code: string + name: string + cost: string +} + +export interface ICardLockRequest { + note: string +} + +export interface ICardUnlockRequest { + note: string +} diff --git a/packages/wallet/backend/src/card/validation.ts b/packages/wallet/backend/src/card/validation.ts new file mode 100644 index 000000000..869e1d3e6 --- /dev/null +++ b/packages/wallet/backend/src/card/validation.ts @@ -0,0 +1,83 @@ +import { z } from 'zod' + +export const getCardsByCustomerSchema = z.object({ + params: z.object({ + customerId: z.string() + }) +}) + +export const getCardDetailsSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + query: z.object({ + publicKeyBase64: z.string() + }) +}) + +export const lockCardSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + query: z.object({ + reasonCode: z.enum([ + 'ClientRequestedLock', + 'LostCard', + 'StolenCard', + 'IssuerRequestGeneral', + 'IssuerRequestFraud', + 'IssuerRequestLegal' + ]) + }), + body: z.object({ + note: z.string() + }) +}) + +export const unlockCardSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + body: z.object({ + note: z.string() + }) +}) + +export const getCardTransactionsSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + query: z.object({ + pageSize: z.coerce.number().int().positive().optional(), + pageNumber: z.coerce.number().int().nonnegative().optional() + }) +}) + +export const changePinSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + body: z.object({ + cypher: z.string() + }) +}) + +export const permanentlyBlockCardSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + query: z.object({ + reasonCode: z.enum([ + 'LostCard', + 'StolenCard', + 'IssuerRequestGeneral', + 'IssuerRequestFraud', + 'IssuerRequestLegal', + 'IssuerRequestIncorrectOpening', + 'CardDamagedOrNotWorking', + 'UserRequest', + 'IssuerRequestCustomerDeceased', + 'ProductDoesNotRenew' + ]) + }) +}) diff --git a/packages/wallet/backend/src/config/env.ts b/packages/wallet/backend/src/config/env.ts index 3aa9013cb..1a9308dc8 100644 --- a/packages/wallet/backend/src/config/env.ts +++ b/packages/wallet/backend/src/config/env.ts @@ -20,6 +20,7 @@ const envSchema = z.object({ GATEHUB_SETTLEMENT_WALLET_ADDRESS: z .string() .default('GATEHUB_SETTLEMENT_WALLET_ADDRESS'), + GATEHUB_CARD_APP_ID: z.string().default('GATEHUB_CARD_APP_ID'), GRAPHQL_ENDPOINT: z.string().url().default('http://localhost:3011/graphql'), AUTH_GRAPHQL_ENDPOINT: z .string() diff --git a/packages/wallet/backend/src/createContainer.ts b/packages/wallet/backend/src/createContainer.ts index 31eb31370..69a337ad1 100644 --- a/packages/wallet/backend/src/createContainer.ts +++ b/packages/wallet/backend/src/createContainer.ts @@ -51,6 +51,8 @@ import { KratosService } from './rafiki/kratos.service' import { GateHubController } from '@/gatehub/controller' import { GateHubClient } from '@/gatehub/client' import { GateHubService } from '@/gatehub/service' +import { CardController } from './card/controller' +import { CardService } from './card/service' export interface Cradle { env: Env @@ -92,6 +94,8 @@ export interface Cradle { gateHubClient: GateHubClient gateHubController: GateHubController gateHubService: GateHubService + cardService: CardService + cardController: CardController } export async function createContainer( @@ -129,6 +133,7 @@ export async function createContainer( quoteService: asClass(QuoteService).singleton(), grantService: asClass(GrantService).singleton(), socketService: asClassSingletonWithLogger(SocketService, logger), + cardService: asClass(CardService).singleton(), userController: asClass(UserController).singleton(), authController: asClass(AuthController).singleton(), assetController: asClass(AssetController).singleton(), @@ -144,7 +149,8 @@ export async function createContainer( kratosService: asClassSingletonWithLogger(KratosService, logger), gateHubClient: asClass(GateHubClient).singleton(), gateHubController: asClass(GateHubController).singleton(), - gateHubService: asClass(GateHubService).singleton() + gateHubService: asClass(GateHubService).singleton(), + cardController: asClass(CardController).singleton() }) return container diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index 94e650acf..d1d21f2c9 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -10,6 +10,7 @@ import { ICreateTransactionResponse, ICreateWalletRequest, ICreateWalletResponse, + IGetUserStateResponse, IGetVaultsResponse, IGetWalletResponse, IRatesResponse, @@ -29,8 +30,24 @@ import { } from '@/gatehub/consts' import axios, { AxiosError } from 'axios' import { Logger } from 'winston' -import { IFRAME_TYPE } from '@wallet/shared/src' +import { + IFRAME_TYPE, + LockReasonCode, + IGetTransactionsResponse +} from '@wallet/shared/src' import { BadRequest } from '@shared/backend' +import { + ICardDetailsResponse, + ILinksResponse, + ICardResponse, + ICreateCustomerRequest, + ICreateCustomerResponse, + ICardProductResponse, + ICardDetailsRequest, + ICardLockRequest, + ICardUnlockRequest +} from '@/card/types' +import { BlockReasonCode } from '@wallet/shared/src' export class GateHubClient { private clientIds = SANDBOX_CLIENT_IDS @@ -38,7 +55,7 @@ export class GateHubClient { private iframeMappings: Record< IFRAME_TYPE, - (managedUserId: string) => Promise + (managedUserUuid: string) => Promise > = { deposit: this.getDepositUrl.bind(this), withdrawal: this.getWithdrawalUrl.bind(this), @@ -49,14 +66,14 @@ export class GateHubClient { private env: Env, private logger: Logger ) { - if (this.isSandbox) { + if (this.isProduction) { this.clientIds = PRODUCTION_CLIENT_IDS this.mainUrl = 'gatehub.net' } } - get isSandbox() { - return this.env.NODE_ENV !== 'production' + get isProduction() { + return this.env.NODE_ENV === 'production' } get apiUrl() { @@ -75,41 +92,41 @@ export class GateHubClient { return `https://onboarding.${this.mainUrl}` } - async getWithdrawalUrl(managedUserId: string): Promise { + async getWithdrawalUrl(managedUserUuid: string): Promise { const token = await this.getIframeAuthorizationToken( this.clientIds.onOffRamp, DEFAULT_APP_SCOPE, - managedUserId + managedUserUuid ) return `${this.rampUrl}/?paymentType=${PAYMENT_TYPE.withdrawal}&bearer=${token}` } - async getDepositUrl(managedUserId: string): Promise { + async getDepositUrl(managedUserUuid: string): Promise { const token = await this.getIframeAuthorizationToken( this.clientIds.onOffRamp, DEFAULT_APP_SCOPE, - managedUserId + managedUserUuid ) return `${this.rampUrl}/?paymentType=${PAYMENT_TYPE.deposit}&bearer=${token}` } - async getOnboardingUrl(managedUserId: string): Promise { + async getOnboardingUrl(managedUserUuid: string): Promise { const token = await this.getIframeAuthorizationToken( this.clientIds.onboarding, ONBOARDING_APP_SCOPE, - managedUserId + managedUserUuid ) return `${this.onboardingUrl}/?bearer=${token}` } - async getExchangeUrl(managedUserId: string): Promise { + async getExchangeUrl(managedUserUuid: string): Promise { const token = await this.getIframeAuthorizationToken( this.clientIds.exchange, DEFAULT_APP_SCOPE, - managedUserId + managedUserUuid ) return `${this.exchangeUrl}/?bearer=${token}` @@ -117,28 +134,30 @@ export class GateHubClient { async getIframeUrl( type: IFRAME_TYPE, - managedUserId: string + managedUserUuid: string ): Promise { if (!this.iframeMappings[type]) { throw new BadRequest('Invalid iframe type') } - return await this.iframeMappings[type](managedUserId) + return await this.iframeMappings[type](managedUserUuid) } async getIframeAuthorizationToken( clientId: string, scope: string[], - managedUserId: string + managedUserUuid: string ): Promise { - const url = `${this.apiUrl}/auth/v1/tokens?${clientId}` + const url = `${this.apiUrl}/auth/v1/tokens?clientId=${clientId}` const body: ITokenRequest = { scope } const response = await this.request( 'POST', url, JSON.stringify(body), - managedUserId + { + managedUserUuid + } ) return response.token @@ -172,23 +191,32 @@ export class GateHubClient { return response } + async getUserState(userId: string): Promise { + const url = `${this.apiUrl}/id/v1/users/${userId}` + + const response = await this.request('GET', url) + + return response + } + async connectUserToGateway( - userUuid: string, + managedUserUuid: string, gatewayUuid: string - ): Promise { - const url = `${this.apiUrl}/id/v1/users/${userUuid}/hubs/${gatewayUuid}` + ): Promise { + const url = `${this.apiUrl}/id/v1/users/${managedUserUuid}/hubs/${gatewayUuid}` - const response = await this.request( - 'POST', - url - ) + await this.request('POST', url, undefined, { + managedUserUuid + }) - if (this.isSandbox) { + if (!this.isProduction) { // Auto approve user to gateway in sandbox environment - await this.approveUserToGateway(userUuid, gatewayUuid) + await this.approveUserToGateway(managedUserUuid, gatewayUuid) + + return true } - return response + return false } private async approveUserToGateway( @@ -212,10 +240,10 @@ export class GateHubClient { } async createWallet( - userUuid: string, + managedUserUuid: string, name: string ): Promise { - const url = `${this.apiUrl}/core/v1/users/${userUuid}/wallets` + const url = `${this.apiUrl}/core/v1/users/${managedUserUuid}/wallets` const body: ICreateWalletRequest = { name, type: HOSTED_WALLET_TYPE @@ -224,7 +252,10 @@ export class GateHubClient { const response = await this.request( 'POST', url, - JSON.stringify(body) + JSON.stringify(body), + { + managedUserUuid + } ) return response @@ -241,31 +272,27 @@ export class GateHubClient { return response } - async getWalletBalance( - userUuid: string, - walletId: string - ): Promise { + async getWalletBalance(walletId: string): Promise { const url = `${this.apiUrl}/core/v1/wallets/${walletId}/balances` - const response = await this.request( - 'GET', - url, - undefined, - userUuid - ) + const response = await this.request('GET', url) return response } async createTransaction( - body: ICreateTransactionRequest + body: ICreateTransactionRequest, + managedUserUuid?: string ): Promise { const url = `${this.apiUrl}/core/v1/transactions` const response = await this.request( 'POST', url, - JSON.stringify(body) + JSON.stringify(body), + { + managedUserUuid + } ) return response @@ -295,11 +322,207 @@ export class GateHubClient { return flatRates } + // This should be called before creating customers to get the product codes for the card and account + async fetchCardApplicationProducts(): Promise { + const url = `${this.apiUrl}/v1/card-applications/${this.env.GATEHUB_CARD_APP_ID}/card-products` + const response = await this.request('GET', url) + return response + } + + async createCustomer( + requestBody: ICreateCustomerRequest + ): Promise { + const url = `${this.apiUrl}/v1/customers` + return this.request( + 'POST', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + + async getCardsByCustomer(customerId: string): Promise { + const url = `${this.apiUrl}/v1/customers/${customerId}/cards` + return this.request('GET', url) + } + + async getCardDetails( + requestBody: ICardDetailsRequest + ): Promise { + const url = `${this.apiUrl}/token/card-data` + + const response = await this.request( + 'POST', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + + const token = response.token + if (!token) { + throw new Error('Failed to obtain token for card data retrieval') + } + + // TODO change this to direct call to card managing entity + // Will get this from the GateHub proxy for now + const cardDetailsUrl = `${this.apiUrl}/v1/proxy/client-device/card-data` + const cardDetailsResponse = await this.request( + 'GET', + cardDetailsUrl, + undefined, + { + token + } + ) + + return cardDetailsResponse + } + + async getCardTransactions( + cardId: string, + pageSize?: number, + pageNumber?: number + ): Promise { + let url = `${this.apiUrl}/v1/cards/${cardId}/transactions` + + const queryParams: string[] = [] + + if (pageSize !== undefined) + queryParams.push(`pageSize=${encodeURIComponent(pageSize.toString())}`) + if (pageNumber !== undefined) + queryParams.push( + `pageNumber=${encodeURIComponent(pageNumber.toString())}` + ) + + if (queryParams.length > 0) { + url += `?${queryParams.join('&')}` + } + + return this.request('GET', url) + } + + async getPin( + requestBody: ICardDetailsRequest + ): Promise { + const url = `${this.apiUrl}/token/pin` + + const response = await this.request( + 'POST', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + + const token = response.token + if (!token) { + throw new Error('Failed to obtain token for card pin retrieval') + } + + // TODO change this to direct call to card managing entity + // Will get this from the GateHub proxy for now + const cardPinUrl = `${this.apiUrl}/v1/proxy/client-device/pin` + const cardPinResponse = await this.request( + 'GET', + cardPinUrl, + undefined, + { + token + } + ) + + return cardPinResponse + } + + async changePin(cardId: string, cypher: string): Promise { + const url = `${this.apiUrl}/token/pin-change` + + const response = await this.request( + 'POST', + url, + JSON.stringify({ cardId: cardId }), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + + const token = response.token + if (!token) { + throw new Error('Failed to obtain token for card pin retrieval') + } + + // TODO change this to direct call to card managing entity + // Will get this from the GateHub proxy for now + const cardPinUrl = `${this.apiUrl}/v1/proxy/client-device/pin` + await this.request( + 'POST', + cardPinUrl, + JSON.stringify({ cypher: cypher }), + { + token + } + ) + } + + async lockCard( + cardId: string, + reasonCode: LockReasonCode, + requestBody: ICardLockRequest + ): Promise { + let url = `${this.apiUrl}/v1/cards/${cardId}/lock` + url += `?reasonCode=${encodeURIComponent(reasonCode)}` + + return this.request( + 'PUT', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + + async unlockCard( + cardId: string, + requestBody: ICardUnlockRequest + ): Promise { + const url = `${this.apiUrl}/v1/cards/${cardId}/unlock` + + return this.request( + 'PUT', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + + async permanentlyBlockCard( + cardId: string, + reasonCode: BlockReasonCode + ): Promise { + let url = `${this.apiUrl}/v1/cards/${cardId}/block` + + url += `?reasonCode=${encodeURIComponent(reasonCode)}` + + return this.request('PUT', url) + } + private async request( method: HTTP_METHODS, url: string, body?: string, - managedUserUuid?: string + headersOptions?: { + managedUserUuid?: string + token?: string + cardAppId?: string + } ): Promise { const timestamp = Date.now().toString() const headers = this.getRequestHeaders( @@ -307,7 +530,7 @@ export class GateHubClient { method, url, body ?? '', - managedUserUuid + headersOptions ) try { @@ -318,6 +541,11 @@ export class GateHubClient { headers }) + this.logger.debug( + `Axios ${method} request for ${url} succeeded:\n ${JSON.stringify(res.data, undefined, 2)}`, + body ? JSON.parse(body) : {} + ) + return res.data } catch (e) { if (e instanceof AxiosError) { @@ -337,14 +565,26 @@ export class GateHubClient { method: HTTP_METHODS, url: string, body?: string, - managedUserUuid?: string + headersOptions?: { + managedUserUuid?: string + token?: string + cardAppId?: string + } ) { return { 'Content-Type': 'application/json', 'x-gatehub-app-id': this.env.GATEHUB_ACCESS_KEY, 'x-gatehub-timestamp': timestamp, 'x-gatehub-signature': this.getSignature(timestamp, method, url, body), - ...(managedUserUuid && { 'x-gatehub-managed-user-uuid': managedUserUuid }) + ...(headersOptions?.managedUserUuid && { + 'x-gatehub-managed-user-uuid': headersOptions.managedUserUuid + }), + ...(headersOptions?.cardAppId && { + 'x-gatehub-card-app-id': headersOptions.cardAppId + }), + ...(headersOptions?.token && { + Authorization: `Bearer ${headersOptions.token}` + }) } } diff --git a/packages/wallet/backend/src/gatehub/controller.ts b/packages/wallet/backend/src/gatehub/controller.ts index 8e647c556..990f676db 100644 --- a/packages/wallet/backend/src/gatehub/controller.ts +++ b/packages/wallet/backend/src/gatehub/controller.ts @@ -5,6 +5,7 @@ import { GateHubService } from '@/gatehub/service' interface IGateHubController { getIframeUrl: Controller + addUserToGateway: Controller } export class GateHubController implements IGateHubController { @@ -24,4 +25,43 @@ export class GateHubController implements IGateHubController { next(e) } } + + public addUserToGateway = async ( + req: Request, + res: CustomResponse, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const approved = await this.gateHubService.addUserToGateway(userId) + + if (approved) { + req.session.user.needsIDProof = false + await req.session.save() + } + + res.status(200).json(toSuccessResponse()) + } catch (e) { + next(e) + } + } + + public webhook = async ( + req: Request, + res: CustomResponse, + next: NextFunction + ) => { + try { + // TODO: implement signature check + if (!req.body.uuid) { + return + } + + await this.gateHubService.handleWebhook(req.body) + + res.status(200).json() + } catch (e) { + next(e) + } + } } diff --git a/packages/wallet/backend/src/gatehub/service.ts b/packages/wallet/backend/src/gatehub/service.ts index 16b770426..5a87a7aff 100644 --- a/packages/wallet/backend/src/gatehub/service.ts +++ b/packages/wallet/backend/src/gatehub/service.ts @@ -2,9 +2,16 @@ import { GateHubClient } from '@/gatehub/client' import { IFRAME_TYPE } from '@wallet/shared/src' import { User } from '@/user/model' import { NotFound } from '@shared/backend' +import { IWebhookDate } from '@/gatehub/types' +import { Logger } from 'winston' +import { Env } from '@/config/env' export class GateHubService { - constructor(private gateHubClient: GateHubClient) {} + constructor( + private gateHubClient: GateHubClient, + private logger: Logger, + private env: Env + ) {} async getIframeUrl(iframeType: IFRAME_TYPE, userId: string): Promise { const user = await User.query().findById(userId) @@ -19,4 +26,61 @@ export class GateHubService { return url } + + async handleWebhook(data: IWebhookDate) { + this.logger.debug(`GateHub webhook event received: ${JSON.stringify(data)}`) + // TODO: handle other webhook types + switch (data.event_type) { + case 'id.verification.accepted': + await this.markUserAsVerified(data.user_uuid) + break + } + } + + async addUserToGateway(userId: string) { + const user = await User.query().findById(userId) + if (!user || !user.gateHubUserId) { + throw new NotFound() + } + + const isUserApproved = await this.gateHubClient.connectUserToGateway( + user.gateHubUserId, + this.env.GATEHUB_GATEWAY_UUID + ) + + const userState = await this.gateHubClient.getUserState(user.gateHubUserId) + + const userDetails: Partial = { + lastName: userState.profile.last_name, + firstName: userState.profile.first_name, + country: userState.profile.address_country_code, + address: [ + userState.profile.address_street1, + userState.profile.address_street2, + userState.profile.address_city + ] + .filter(Boolean) + .join(', ') + } + + if (isUserApproved) { + userDetails.kycVerified = true + } + + await User.query().findById(user.id).patch(userDetails) + + return isUserApproved + } + + private async markUserAsVerified(uuid: string): Promise { + const user = await User.query().findOne({ gateHubUserId: uuid }) + + if (!user) { + throw new NotFound('User not found') + } + + await User.query().findById(user.id).patch({ + kycVerified: true + }) + } } diff --git a/packages/wallet/backend/src/gatehub/types.ts b/packages/wallet/backend/src/gatehub/types.ts index 1aa2aa142..471af48d3 100644 --- a/packages/wallet/backend/src/gatehub/types.ts +++ b/packages/wallet/backend/src/gatehub/types.ts @@ -36,6 +36,17 @@ export interface ICreateManagedUserResponse { managedBy: string } +export interface IGetUserStateResponse { + profile: { + first_name: string + last_name: string + address_country_code: string + address_city: string + address_street1: string + address_street2: string + } +} + export interface ICreateWalletRequest { name: string type: number @@ -84,9 +95,9 @@ export interface IWalletBalance { interface IVault { uuid: string name: string - assetCode: string - createdAt: string - updatedAt: string + asset_code: string + created_at: string + updated_at: string } export interface IConnectUserToGatewayResponse {} @@ -96,3 +107,12 @@ export interface IApproveUserToGatewayRequest { customMessage: boolean } export interface IApproveUserToGatewayResponse {} + +export interface IWebhookDate { + uuid: string + timestamp: string + event_type: string + user_uuid: string + environment: 'sandbox' | 'production' + data: Record +} diff --git a/packages/wallet/backend/src/middleware/isAuth.ts b/packages/wallet/backend/src/middleware/isAuth.ts index b83dc04fd..6c5106d9c 100644 --- a/packages/wallet/backend/src/middleware/isAuth.ts +++ b/packages/wallet/backend/src/middleware/isAuth.ts @@ -1,7 +1,7 @@ import type { NextFunction, Request, Response } from 'express' import { Unauthorized } from '@shared/backend' -const KYCRoutes = ['/wallet', '/verify', '/countries', '/documents'] +const KYCRoutes = ['/iframe-urls/onboarding', '/gatehub/add-user-to-gateway'] export const isAuth = async ( req: Request, diff --git a/packages/wallet/backend/src/rafiki/service.ts b/packages/wallet/backend/src/rafiki/service.ts index dba597db4..dc8e78bd6 100644 --- a/packages/wallet/backend/src/rafiki/service.ts +++ b/packages/wallet/backend/src/rafiki/service.ts @@ -247,21 +247,27 @@ export class RafikiService implements IRafikiService { const walletAddress = await this.getWalletAddress(wh) const debitAmount = this.getAmountFromWebHook(wh) - const { gateHubWalletId: sendingWallet, userId } = - await this.getGateHubWalletAddress(walletAddress) + const { + gateHubWalletId: sendingWallet, + userId, + gateHubUserId + } = await this.getGateHubWalletAddress(walletAddress) if (!this.validateAmount(debitAmount, wh.type)) { return } - await this.gateHubClient.createTransaction({ - amount: this.amountToNumber(debitAmount), - vault_uuid: this.getVaultUuid(debitAmount.assetCode), - sending_address: sendingWallet, - receiving_address: this.env.GATEHUB_SETTLEMENT_WALLET_ADDRESS, - type: HOSTED_TRANSACTION_TYPE, - message: 'Transfer' - }) + await this.gateHubClient.createTransaction( + { + amount: this.amountToNumber(debitAmount), + vault_uuid: this.getVaultUuid(debitAmount.assetCode), + sending_address: sendingWallet, + receiving_address: this.env.GATEHUB_SETTLEMENT_WALLET_ADDRESS, + type: HOSTED_TRANSACTION_TYPE, + message: 'Transfer' + }, + gateHubUserId + ) if (wh.data.balance !== '0') { await this.rafikiClient.withdrawLiqudity(wh.id) @@ -365,9 +371,11 @@ export class RafikiService implements IRafikiService { } private async getGateHubWalletAddress(walletAddress: WalletAddress) { - const account = await Account.query().findById(walletAddress.accountId) + const account = await Account.query() + .findById(walletAddress.accountId) + .withGraphFetched('user') - if (!account || !account.gateHubWalletId) { + if (!account?.gateHubWalletId || !account.user?.gateHubUserId) { throw new BadRequest( 'No account associated to the provided payment pointer' ) @@ -375,7 +383,8 @@ export class RafikiService implements IRafikiService { return { userId: account.userId, - gateHubWalletId: account.gateHubWalletId + gateHubWalletId: account.gateHubWalletId, + gateHubUserId: account.user.gateHubUserId } } } diff --git a/packages/wallet/backend/src/user/controller.ts b/packages/wallet/backend/src/user/controller.ts index 69f43d18e..28c9decb3 100644 --- a/packages/wallet/backend/src/user/controller.ts +++ b/packages/wallet/backend/src/user/controller.ts @@ -46,7 +46,7 @@ export class UserController implements IUserController { lastName: user.lastName, address: user.address, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified }, 'User retrieved successfully' ) diff --git a/packages/wallet/backend/src/user/model.ts b/packages/wallet/backend/src/user/model.ts index 59e0b37a9..c853b307b 100644 --- a/packages/wallet/backend/src/user/model.ts +++ b/packages/wallet/backend/src/user/model.ts @@ -12,13 +12,15 @@ export class User extends BaseModel { public verifyEmailToken?: string | null public isEmailVerified!: boolean private password!: string + public lastName?: string public firstName?: string public address?: string public country?: string - public kycId?: string + public kycVerified!: boolean public gateHubUserId?: string + public sessions?: Session[] public passwordResetToken?: string | null public passwordResetExpiresAt?: Date | null diff --git a/packages/wallet/backend/src/user/service.ts b/packages/wallet/backend/src/user/service.ts index 6c03a44f0..e3588c082 100644 --- a/packages/wallet/backend/src/user/service.ts +++ b/packages/wallet/backend/src/user/service.ts @@ -4,7 +4,6 @@ import { getRandomToken, hashToken } from '@/utils/helpers' import { Logger } from 'winston' import { BadRequest, Conflict } from '@shared/backend' import { GateHubClient } from '@/gatehub/client' -import { Env } from '@/config/env' interface CreateUserArgs { email: string @@ -31,8 +30,7 @@ export class UserService implements IUserService { constructor( private emailService: EmailService, private gateHubClient: GateHubClient, - private logger: Logger, - private env: Env + private logger: Logger ) {} public async create(args: CreateUserArgs): Promise { @@ -133,11 +131,6 @@ export class UserService implements IUserService { verifyEmailToken: null, gateHubUserId: gateHubUser.id }) - - await this.gateHubClient.connectUserToGateway( - gateHubUser.id, - this.env.GATEHUB_GATEWAY_UUID - ) } public async resetVerifyEmailToken(args: VerifyEmailArgs): Promise { diff --git a/packages/wallet/backend/src/walletAddress/model.ts b/packages/wallet/backend/src/walletAddress/model.ts index 2e6ffdca1..20d5f8080 100644 --- a/packages/wallet/backend/src/walletAddress/model.ts +++ b/packages/wallet/backend/src/walletAddress/model.ts @@ -12,6 +12,7 @@ export class WalletAddress extends BaseModel implements IWalletAddressResponse { readonly id!: string readonly url!: string readonly accountId!: string + cardId?: string active!: boolean account!: Account transactions!: Array diff --git a/packages/wallet/backend/src/walletAddress/service.ts b/packages/wallet/backend/src/walletAddress/service.ts index 45ca51c00..02d2aebaf 100644 --- a/packages/wallet/backend/src/walletAddress/service.ts +++ b/packages/wallet/backend/src/walletAddress/service.ts @@ -168,6 +168,20 @@ export class WalletAddressService implements IWalletAddressService { return walletAddress } + async getByCardId(userId: string, cardId: string): Promise { + const walletAddress = await WalletAddress.query() + .join('accounts', 'walletAddresses.accountId', 'accounts.id') + .where('walletAddresses.cardId', cardId) + .andWhere('accounts.userId', userId) + .first() + + if (!walletAddress) { + throw new NotFound() + } + + return walletAddress + } + async listIdentifiersByUserId(userId: string): Promise { const accounts = await Account.query() .where('userId', userId) diff --git a/packages/wallet/backend/tests/account/controller.test.ts b/packages/wallet/backend/tests/account/controller.test.ts index fce2f770d..1d3805fa5 100644 --- a/packages/wallet/backend/tests/account/controller.test.ts +++ b/packages/wallet/backend/tests/account/controller.test.ts @@ -51,7 +51,7 @@ describe('Account Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' diff --git a/packages/wallet/backend/tests/asset/controller.test.ts b/packages/wallet/backend/tests/asset/controller.test.ts index afd1b2b7e..b370a88e4 100644 --- a/packages/wallet/backend/tests/asset/controller.test.ts +++ b/packages/wallet/backend/tests/asset/controller.test.ts @@ -53,7 +53,7 @@ describe('Asset Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } }) diff --git a/packages/wallet/backend/tests/auth/controller.test.ts b/packages/wallet/backend/tests/auth/controller.test.ts index 71303d317..1f5aff882 100644 --- a/packages/wallet/backend/tests/auth/controller.test.ts +++ b/packages/wallet/backend/tests/auth/controller.test.ts @@ -148,7 +148,7 @@ describe('Authentication Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified }) expect(res.statusCode).toBe(200) expect(res._getJSONData()).toMatchObject({ @@ -266,7 +266,7 @@ describe('Authentication Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await authController.logOut(req, res, next) @@ -309,7 +309,7 @@ describe('Authentication Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await authController.logOut(req, res, (err) => { next() diff --git a/packages/wallet/backend/tests/cards/controller.test.ts b/packages/wallet/backend/tests/cards/controller.test.ts new file mode 100644 index 000000000..f2e35b8ca --- /dev/null +++ b/packages/wallet/backend/tests/cards/controller.test.ts @@ -0,0 +1,661 @@ +import { + createRequest, + createResponse, + MockRequest, + MockResponse +} from 'node-mocks-http' +import { CardController } from '@/card/controller' +import { BadRequest } from '@shared/backend' +import { ICardDetailsResponse, ICardResponse } from '@/card/types' +import { IGetTransactionsResponse } from '@wallet/shared/src' +import { AwilixContainer } from 'awilix' +import { Cradle } from '@/createContainer' +import { createApp, TestApp } from '@/tests/app' +import { Knex } from 'knex' +import { Request, Response } from 'express' +import { env } from '@/config/env' +import { createContainer } from '@/createContainer' +import { AuthService } from '@/auth/service' +import { applyMiddleware } from '../utils' +import { withSession } from '@/middleware/withSession' +import { truncateTables } from '@shared/backend/tests' +import { mockLogInRequest } from '../mocks' +import { createUser } from '../helpers' +import { User } from '@/user/model' + +describe('CardController', () => { + let bindings: AwilixContainer + let appContainer: TestApp + let knex: Knex + let authService: AuthService + let cardController: CardController + let req: MockRequest + let res: MockResponse + let userId: string + + const mockCardService = { + getCardsByCustomer: jest.fn(), + getCardDetails: jest.fn(), + getCardTransactions: jest.fn(), + lock: jest.fn(), + unlock: jest.fn(), + getPin: jest.fn(), + changePin: jest.fn(), + permanentlyBlockCard: jest.fn() + } + + const args = mockLogInRequest().body + + const createReqRes = async () => { + res = createResponse() + req = createRequest() + + await applyMiddleware(withSession, req, res) + + const { user, session } = await authService.authorize(args) + req.session.id = session.id + req.session.user = { + id: user.id, + email: user.email, + needsWallet: !user.gateHubUserId, + needsIDProof: !user.kycVerified + } + + req.params.cardId = 'test-card-id' + + userId = user.id + await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' }) + } + + beforeAll(async (): Promise => { + bindings = await createContainer(env) + appContainer = await createApp(bindings) + knex = appContainer.knex + authService = await bindings.resolve('authService') + cardController = await bindings.resolve('cardController') + }) + + beforeEach(async (): Promise => { + Reflect.set(cardController, 'cardService', mockCardService) + + await createUser({ ...args, isEmailVerified: true }) + await createReqRes() + }) + + afterEach(async (): Promise => { + await truncateTables(knex) + jest.resetAllMocks() + }) + + afterAll(async (): Promise => { + await appContainer.stop() + await knex.destroy() + }) + + describe('getCardsByCustomer', () => { + it('should get cards by customer successfully', async () => { + const next = jest.fn() + + const mockedCards: ICardResponse[] = [ + { + sourceId: '3dc96e41-279d-4355-921a-e1946e90e1ff', + nameOnCard: 'Jane Doe', + id: 'test-card-id', + accountId: '469E3666F8914020B6B2604F7D4A10F6', + accountSourceId: 'c44e6bc8-d0ef-491e-b374-6d09b6fa6332', + maskedPan: '528700******9830', + status: 'Active', + statusReasonCode: null, + lockLevel: null, + expiryDate: '0929', + customerId: 'customer-id', + customerSourceId: 'a5aba6c7-b8ad-4cfe-98d5-497366a4ee2c', + productCode: 'VMDTKPREB' + } + ] + + mockCardService.getCardsByCustomer.mockResolvedValue(mockedCards) + + req.params.customerId = 'customer-id' + + await cardController.getCardsByCustomer(req, res, next) + + expect(mockCardService.getCardsByCustomer).toHaveBeenCalledWith( + 'customer-id' + ) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockedCards + }) + }) + + it('should return 400 if customerId is missing', async () => { + const next = jest.fn() + + delete req.params.customerId + + await cardController.getCardsByCustomer(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + }) + + describe('getCardDetails', () => { + it('should get card details successfully', async () => { + const next = jest.fn() + + req.query = { publicKeyBase64: 'test-public-key' } + + const mockedCardDetails: ICardDetailsResponse = { + cipher: 'encrypted-card-data' + } + + mockCardService.getCardDetails.mockResolvedValue(mockedCardDetails) + + await cardController.getCardDetails(req, res, next) + + expect(mockCardService.getCardDetails).toHaveBeenCalledWith(userId, { + cardId: 'test-card-id', + publicKeyBase64: 'test-public-key' + }) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockedCardDetails + }) + }) + + it('should return 400 if cardId is missing', async () => { + const next = jest.fn() + + delete req.params.cardId + + await cardController.getCardDetails(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + + it('should return 400 if publicKeyBase64 is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.query = {} + + await cardController.getCardDetails(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + }) + + describe('getCardTransactions', () => { + it('should get card transactions successfully', async () => { + const next = jest.fn() + + const mockedTransactions: IGetTransactionsResponse = { + data: [ + { + id: 1, + transactionId: '78b34171-0a7c-4185-9fd5-7c5f366ed50b', + ghResponseCode: 'TRXNS', + cardScheme: 3, + type: 1, + createdAt: '2024-02-01T00:00:00.000Z', + txStatus: 'PROCESSING', + vaultId: 1, + cardId: 1, + refTransactionId: '', + responseCode: null, + transactionAmount: '1.1', + transactionCurrency: 'EUR', + billingAmount: '1.1', + billingCurrency: 'EUR', + terminalId: null, + wallet: 123, + transactionDateTime: '2024-02-01T00:00:00.000Z', + processDateTime: null + }, + { + id: 2, + transactionId: '545b34171-0a7c-4185-9fd5-7c5f366e4566', + ghResponseCode: 'TRXNS', + cardScheme: 3, + type: 1, + createdAt: '2024-02-01T00:00:00.000Z', + txStatus: 'PROCESSING', + vaultId: 1, + cardId: 1, + refTransactionId: '', + responseCode: null, + transactionAmount: '1.1', + transactionCurrency: 'EUR', + billingAmount: '1.1', + billingCurrency: 'EUR', + terminalId: null, + wallet: 123, + transactionDateTime: '2024-02-01T00:00:00.000Z', + processDateTime: null + } + ], + pagination: { + pageNumber: 1, + pageSize: 10, + totalRecords: 2, + totalPages: 1 + } + } + + mockCardService.getCardTransactions.mockResolvedValue(mockedTransactions) + + req.params = { cardId: 'test-card-id' } + req.query = { pageSize: '10', pageNumber: '1' } + + await cardController.getCardTransactions(req, res, next) + + expect(mockCardService.getCardTransactions).toHaveBeenCalledWith( + userId, + 'test-card-id', + 10, + 1 + ) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockedTransactions + }) + }) + it('should return 400 if page size is invalid', async () => { + const next = jest.fn() + + req.params = { cardId: 'test-card-id' } + // Invalid pageSize + req.query = { pageSize: '-1', pageNumber: '1' } + + await cardController.getCardTransactions(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toEqual({ + success: false, + message: 'Invalid input' + }) + }) + }) + + describe('lock', () => { + it('should lock card successfully', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.body = { note: 'Lost my card' } + req.query = { reasonCode: 'LostCard' } + + const mockResult = { status: 'locked' } + mockCardService.lock.mockResolvedValue(mockResult) + + await cardController.lock(req, res, next) + + expect(mockCardService.lock).toHaveBeenCalledWith( + userId, + 'test-card-id', + 'LostCard', + { + note: 'Lost my card' + } + ) + + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockResult + }) + }) + + it('should return 400 if reasonCode is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.body = { note: 'Lost my card' } + delete req.query.reasonCode + + await cardController.lock(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) + }) + + it('should return 400 if reasonCode is invalid', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.query.reasonCode = 'InvalidCode' + req.body = { note: 'Lost my card' } + + await cardController.lock(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) + }) + + it('should return 400 if note is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.query.reasonCode = 'LostCard' + req.body = {} + + await cardController.lock(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) + }) + }) + + describe('unlock', () => { + it('should unlock the card successfully', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.body = { note: 'Found my card' } + + const mockResult = { status: 'unlocked' } + mockCardService.unlock.mockResolvedValue(mockResult) + + await cardController.unlock(req, res, next) + + expect(mockCardService.unlock).toHaveBeenCalledWith( + userId, + 'test-card-id', + { + note: 'Found my card' + } + ) + + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockResult + }) + }) + + it('should return 400 if note is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.body = {} + + await cardController.unlock(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) + }) + }) + + describe('getPin', () => { + it('should get pin successfully', async () => { + const next = jest.fn() + + req.query = { publicKeyBase64: 'test-public-key' } + + const mockedCardDetails: ICardDetailsResponse = { + cipher: 'encrypted-card-pin' + } + + mockCardService.getPin.mockResolvedValue(mockedCardDetails) + + await cardController.getPin(req, res, next) + + expect(mockCardService.getPin).toHaveBeenCalledWith(userId, { + cardId: 'test-card-id', + publicKeyBase64: 'test-public-key' + }) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: mockedCardDetails + }) + }) + + it('should return 400 if cardId is missing', async () => { + const next = jest.fn() + + delete req.params.cardId + + await cardController.getPin(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + + it('should return 400 if publicKeyBase64 is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.query = {} + + await cardController.getPin(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + }) + + describe('changePin', () => { + it('should change pin successfully', async () => { + const next = jest.fn() + req.params.cardId = 'test-card-id' + req.body = { + cypher: 'test-cypher' + } + + mockCardService.changePin.mockResolvedValue({}) + + await cardController.changePin(req, res, next) + + expect(mockCardService.changePin).toHaveBeenCalledWith( + userId, + 'test-card-id', + 'test-cypher' + ) + expect(res.statusCode).toBe(201) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: {} + }) + }) + + it('should return 400 if cardId is missing', async () => { + const next = jest.fn() + + delete req.params.cardId + + await cardController.changePin(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + + it('should return 400 if cypher is missing', async () => { + const next = jest.fn() + + req.params.cardId = 'test-card-id' + req.body = {} + + await cardController.changePin(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + }) + + describe('permanentlyBlockCard', () => { + it('should get block card successfully', async () => { + const next = jest.fn() + + mockCardService.permanentlyBlockCard.mockResolvedValue({}) + + req.params = { cardId: 'test-card-id' } + req.query = { reasonCode: 'StolenCard' } + + await cardController.permanentlyBlockCard(req, res, next) + + expect(mockCardService.permanentlyBlockCard).toHaveBeenCalledWith( + userId, + 'test-card-id', + 'StolenCard' + ) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: {} + }) + }) + it('should return 400 if reasonCode is invalid', async () => { + const next = jest.fn() + + req.params = { cardId: 'test-card-id' } + req.query = { reasonCode: 'InvalidCode' } + + await cardController.permanentlyBlockCard(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toEqual({ + success: false, + message: 'Invalid input' + }) + }) + }) +}) diff --git a/packages/wallet/backend/tests/grant/controller.test.ts b/packages/wallet/backend/tests/grant/controller.test.ts index 2edfc65e2..2a460eaec 100644 --- a/packages/wallet/backend/tests/grant/controller.test.ts +++ b/packages/wallet/backend/tests/grant/controller.test.ts @@ -45,7 +45,7 @@ describe('Grant Controller', () => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' }) } diff --git a/packages/wallet/backend/tests/quote/controller.test.ts b/packages/wallet/backend/tests/quote/controller.test.ts index 7e74dcf45..ad497e32d 100644 --- a/packages/wallet/backend/tests/quote/controller.test.ts +++ b/packages/wallet/backend/tests/quote/controller.test.ts @@ -44,7 +44,7 @@ describe('Quote Controller', () => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' }) } diff --git a/packages/wallet/backend/tests/socket/service.test.ts b/packages/wallet/backend/tests/socket/service.test.ts index 8febd6b86..968ac4d1b 100644 --- a/packages/wallet/backend/tests/socket/service.test.ts +++ b/packages/wallet/backend/tests/socket/service.test.ts @@ -88,7 +88,7 @@ describe('Socket Service', () => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } userId = user.id diff --git a/packages/wallet/backend/tests/transaction/controller.test.ts b/packages/wallet/backend/tests/transaction/controller.test.ts index df5865a14..395629986 100644 --- a/packages/wallet/backend/tests/transaction/controller.test.ts +++ b/packages/wallet/backend/tests/transaction/controller.test.ts @@ -54,7 +54,7 @@ describe('Transaction Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } userId = user.id diff --git a/packages/wallet/backend/tests/user/controller.test.ts b/packages/wallet/backend/tests/user/controller.test.ts index d111db112..cb653bfe9 100644 --- a/packages/wallet/backend/tests/user/controller.test.ts +++ b/packages/wallet/backend/tests/user/controller.test.ts @@ -57,7 +57,7 @@ describe('User Controller', (): void => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } userInfo = { diff --git a/packages/wallet/backend/tests/utils.ts b/packages/wallet/backend/tests/utils.ts index 4714b8780..8632187b5 100644 --- a/packages/wallet/backend/tests/utils.ts +++ b/packages/wallet/backend/tests/utils.ts @@ -53,7 +53,7 @@ export const loginUser = async ({ id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } } if (req && res) { diff --git a/packages/wallet/backend/tests/walletAddress/controller.test.ts b/packages/wallet/backend/tests/walletAddress/controller.test.ts index 8aa9d74d5..f05b16785 100644 --- a/packages/wallet/backend/tests/walletAddress/controller.test.ts +++ b/packages/wallet/backend/tests/walletAddress/controller.test.ts @@ -74,7 +74,7 @@ describe('Wallet Address', () => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } userId = user.id await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' }) diff --git a/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts b/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts index 9fe287637..312e9650c 100644 --- a/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts +++ b/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts @@ -53,7 +53,7 @@ describe('Wallet Address Keys Controller', () => { id: user.id, email: user.email, needsWallet: !user.gateHubUserId, - needsIDProof: !user.kycId + needsIDProof: !user.kycVerified } userId = user.id await User.query().patchAndFetchById(user.id, { gateHubUserId: 'mocked' }) diff --git a/packages/wallet/frontend/package.json b/packages/wallet/frontend/package.json index 7392cac51..95da8f2eb 100644 --- a/packages/wallet/frontend/package.json +++ b/packages/wallet/frontend/package.json @@ -35,11 +35,11 @@ "@tailwindcss/forms": "^0.5.9", "@types/node": "^20.12.11", "@types/nprogress": "^0.2.3", - "@types/react": "18.3.8", + "@types/react": "18.3.10", "@types/react-dom": "18.3.0", "autoprefixer": "^10.4.20", "postcss": "^8.4.47", - "tailwindcss": "^3.4.12", + "tailwindcss": "^3.4.13", "typescript": "^5.6.2" } } diff --git a/packages/wallet/frontend/src/components/dialogs/FundAccountDialog.tsx b/packages/wallet/frontend/src/components/dialogs/FundAccountDialog.tsx deleted file mode 100644 index bfd0fd136..000000000 --- a/packages/wallet/frontend/src/components/dialogs/FundAccountDialog.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import type { DialogProps } from '@/lib/types/dialog' -import { Dialog, Transition } from '@headlessui/react' -import { Fragment } from 'react' -import { Input } from '@/ui/forms/Input' -import { Button } from '@/ui/Button' -import { Account, accountService } from '@/lib/api/account' -import { useDialog } from '@/lib/hooks/useDialog' -import { ErrorDialog } from './ErrorDialog' -import { getCurrencySymbol, getObjectKeys } from '@/utils/helpers' -import { useZodForm } from '@/lib/hooks/useZodForm' -import { Form } from '@/ui/forms/Form' -import { useRouter } from 'next/router' -import { useOnboardingContext } from '@/lib/context/onboarding' -import { fundAccountSchema } from '@wallet/shared' - -type FundAccountDialogProps = Pick & { - account: Account -} - -export const FundAccountDialog = ({ - onClose, - account -}: FundAccountDialogProps) => { - const router = useRouter() - const [openDialog, closeDialog] = useDialog() - const fundAccountForm = useZodForm({ - schema: fundAccountSchema - }) - const { isUserFirstTime, setRunOnboarding, stepIndex, setStepIndex } = - useOnboardingContext() - - return ( - - - -
- -
-
- - - - Add Money to Account - -
-
{ - const response = await accountService.fund(data) - - if (!response) { - openDialog( - - ) - return - } - - if (response.success) { - router.replace(router.asPath) - closeDialog() - if (isUserFirstTime) { - setStepIndex(stepIndex + 1) - setRunOnboarding(true) - } - } else { - const { errors, message } = response - - if (errors) { - getObjectKeys(errors).map((field) => - fundAccountForm.setError(field, { - message: errors[field] - }) - ) - } - if (message) { - fundAccountForm.setError('root', { message }) - } - } - }} - > - - - -
- - -
- -
-
-
-
-
-
-
- ) -} diff --git a/packages/wallet/frontend/src/components/dialogs/WithdrawFundsDialog.tsx b/packages/wallet/frontend/src/components/dialogs/WithdrawFundsDialog.tsx deleted file mode 100644 index 8312966ed..000000000 --- a/packages/wallet/frontend/src/components/dialogs/WithdrawFundsDialog.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import type { DialogProps } from '@/lib/types/dialog' -import { Dialog, Transition } from '@headlessui/react' -import { Fragment } from 'react' -import { Input } from '@/ui/forms/Input' -import { Button } from '@/ui/Button' -import { Account, accountService } from '@/lib/api/account' -import { useDialog } from '@/lib/hooks/useDialog' -import { ErrorDialog } from './ErrorDialog' -import { getCurrencySymbol, getObjectKeys } from '@/utils/helpers' -import { useZodForm } from '@/lib/hooks/useZodForm' -import { Form } from '@/ui/forms/Form' -import { useRouter } from 'next/router' -import { withdrawFundsSchema } from '@wallet/shared' - -type WithdrawFundsDialogProps = Pick & { - account: Account -} - -export const WithdrawFundsDialog = ({ - onClose, - account -}: WithdrawFundsDialogProps) => { - const router = useRouter() - const [openDialog, closeDialog] = useDialog() - const withdrawFundsForm = useZodForm({ - schema: withdrawFundsSchema - }) - - return ( - - - -
- -
-
- - - - Withdraw Money - -
-
{ - const response = await accountService.withdraw(data) - - if (!response) { - openDialog( - - ) - return - } - - if (response.success) { - router.replace(router.asPath) - closeDialog() - } else { - const { errors, message } = response - - if (errors) { - getObjectKeys(errors).map((field) => - withdrawFundsForm.setError(field, { - message: errors[field] - }) - ) - } - if (message) { - withdrawFundsForm.setError('root', { message }) - } - } - }} - > - - - -
- - -
- -
-
-
-
-
-
-
- ) -} diff --git a/packages/wallet/frontend/src/components/layouts/AppLayout.tsx b/packages/wallet/frontend/src/components/layouts/AppLayout.tsx index 140cc90dd..c121aa1ec 100644 --- a/packages/wallet/frontend/src/components/layouts/AppLayout.tsx +++ b/packages/wallet/frontend/src/components/layouts/AppLayout.tsx @@ -28,7 +28,7 @@ export const AppLayout = ({ children }: AppLayoutProps) => { {(isUserFirstTime || isDevKeysOnboarding) && } -
+
{children} diff --git a/packages/wallet/frontend/src/components/layouts/AuthLayout.tsx b/packages/wallet/frontend/src/components/layouts/AuthLayout.tsx index d025d8ea0..224447622 100644 --- a/packages/wallet/frontend/src/components/layouts/AuthLayout.tsx +++ b/packages/wallet/frontend/src/components/layouts/AuthLayout.tsx @@ -38,8 +38,8 @@ const AuthLayout = ({ image, children }: AuthLayoutProps) => { loading="eager" /> -
-
+
+
{children}
diff --git a/packages/wallet/frontend/src/components/settings/PersonalSettingsForm.tsx b/packages/wallet/frontend/src/components/settings/PersonalSettingsForm.tsx index 2a5dd2612..3ebfe2936 100644 --- a/packages/wallet/frontend/src/components/settings/PersonalSettingsForm.tsx +++ b/packages/wallet/frontend/src/components/settings/PersonalSettingsForm.tsx @@ -15,6 +15,7 @@ type PersonalSettingsFormProps = { user: UserResponse } +// TODO: Can these details be updated by the user when switching to GateHub? export const PersonalSettingsForm = ({ user }: PersonalSettingsFormProps) => { const [isReadOnly, setIsReadOnly] = useState(true) const { isChangePassword, setIsChangePassword } = usePasswordContext() diff --git a/packages/wallet/frontend/src/lib/api/account.ts b/packages/wallet/frontend/src/lib/api/account.ts index 4d9747cd1..d88371ffb 100644 --- a/packages/wallet/frontend/src/lib/api/account.ts +++ b/packages/wallet/frontend/src/lib/api/account.ts @@ -5,11 +5,7 @@ import { type ErrorResponse, type SuccessResponse } from '../httpClient' -import { - createAccountSchema, - fundAccountSchema, - withdrawFundsSchema -} from '@wallet/shared' +import { createAccountSchema } from '@wallet/shared' import { WalletAddressResponse } from '@wallet/shared/src' export type Account = { @@ -33,14 +29,6 @@ type CreateAccountResult = SuccessResponse type CreateAccountError = ErrorResponse type CreateAccountResponse = CreateAccountResult | CreateAccountError -type FundAccountArgs = z.infer -type FundAccountError = ErrorResponse -type FundAccountResponse = SuccessResponse | FundAccountError - -type WithdrawFundsArgs = z.infer -type WithdrawFundsError = ErrorResponse -type WithdrawFundsResponse = SuccessResponse | WithdrawFundsError - interface AccountService { get: (accountId: string, cookies?: string) => Promise list: ( @@ -48,8 +36,6 @@ interface AccountService { include?: ('walletAddresses' | 'walletAddressKeys')[] ) => Promise create: (args: CreateAccountArgs) => Promise - fund: (args: FundAccountArgs) => Promise - withdraw: (args: WithdrawFundsArgs) => Promise } const createAccountService = (): AccountService => ({ @@ -106,38 +92,6 @@ const createAccountService = (): AccountService => ({ 'We were not able to create your account. Please try again.' ) } - }, - - async fund(args) { - try { - const response = await httpClient - .post('accounts/fund', { - json: args - }) - .json() - return response - } catch (error) { - return getError( - error, - 'We were not able to fund your account. Please try again.' - ) - } - }, - - async withdraw(args) { - try { - const response = await httpClient - .post('accounts/withdraw', { - json: args - }) - .json() - return response - } catch (error) { - return getError( - error, - 'We were not able to withdraw the funds. Please try again.' - ) - } } }) diff --git a/packages/wallet/frontend/src/lib/api/user.ts b/packages/wallet/frontend/src/lib/api/user.ts index 1d25c6260..d86f9ef25 100644 --- a/packages/wallet/frontend/src/lib/api/user.ts +++ b/packages/wallet/frontend/src/lib/api/user.ts @@ -5,78 +5,22 @@ import { type ErrorResponse, type SuccessResponse } from '../httpClient' -import { ACCEPTED_IMAGE_TYPES } from '@/utils/constants' -import { SelectOption } from '@/ui/forms/Select' import { UserResponse, ValidTokenResponse, emailSchema, isValidPassword, signUpSchema, - loginSchema + loginSchema, + IFRAME_TYPE, + IframeResponse } from '@wallet/shared' -export const personalDetailsSchema = z.object({ - firstName: z.string().min(1, { message: 'First name is required' }), - lastName: z.string().min(1, { message: 'Last name is required' }), - country: z.object({ - value: z.string().length(2), - label: z.string().min(1) - }), - city: z.string().min(1, { message: 'City is required' }), - address: z.string().min(1, { message: 'Address is required' }), - zip: z.string().min(1, { message: 'ZIP code is required' }) -}) - export const profileSchema = z.object({ firstName: z.string().min(1, { message: 'First name is required' }), lastName: z.string().min(1, { message: 'Last name is required' }) }) -export const verifyIdentitySchema = z - .object({ - documentType: z.string({ - invalid_type_error: 'Please select an ID Type' - }), - frontSideImage: z - .string() - .min(1, { message: 'Front side of ID is required' }), - frontSideImageType: z.string(), - backSideImage: z.string().optional(), - backSideImageType: z.string().optional(), - faceImage: z.string().min(1, { message: 'A selfie image is required' }), - faceImageType: z.string() - }) - .superRefine( - ({ frontSideImageType, faceImageType, backSideImageType }, ctx) => { - if (!ACCEPTED_IMAGE_TYPES.includes(frontSideImageType)) { - ctx.addIssue({ - code: 'custom', - message: `Image must be 'jpeg' or 'png'`, - path: ['frontSideImage'] - }) - } - if (!ACCEPTED_IMAGE_TYPES.includes(faceImageType)) { - ctx.addIssue({ - code: 'custom', - message: `Image must be 'jpeg' or 'png'`, - path: ['faceImage'] - }) - } - if ( - backSideImageType && - backSideImageType?.length > 0 && - !ACCEPTED_IMAGE_TYPES.includes(backSideImageType) - ) { - ctx.addIssue({ - code: 'custom', - message: `Image must be 'jpeg' or 'png'`, - path: ['backSideImage'] - }) - } - } - ) - export const resetPasswordSchema = z .object({ password: z @@ -137,12 +81,6 @@ export const changePasswordSchema = z } }) -export type Document = { - type: string - name: string - isBackRequired: boolean -} - type SignUpArgs = z.infer type SignUpError = ErrorResponse type SignUpResponse = SuccessResponse | SignUpError @@ -179,14 +117,6 @@ type VerifyEmailResponse = SuccessResponse | VerifyEmailError type MeResult = SuccessResponse type MeResponse = MeResult | ErrorResponse -type CreateWalletArgs = z.infer -type CreateWalletError = ErrorResponse -type CreateWalletResponse = SuccessResponse | CreateWalletError - -type VerifyIdentityArgs = z.infer -type VerifyIdentityError = ErrorResponse -type VerifyIdentityResponse = SuccessResponse | VerifyIdentityError - type ProfileArgs = z.infer type ProfileError = ErrorResponse type ProfileResponse = SuccessResponse | ProfileError @@ -195,6 +125,9 @@ type ChangePasswordArgs = z.infer type ChangePasswordError = ErrorResponse type ChangePasswordResponse = SuccessResponse | ChangePasswordError +type GetGateHubIframeSrcResult = SuccessResponse +type GetGateHubIframeSrcResponse = GetGateHubIframeSrcResult | ErrorResponse + interface UserService { signUp: (args: SignUpArgs) => Promise login: (args: LoginArgs) => Promise @@ -204,15 +137,15 @@ interface UserService { checkToken: (token: string, cookies?: string) => Promise verifyEmail: (args: VerifyEmailArgs) => Promise me: (cookies?: string) => Promise - createWallet: (args: CreateWalletArgs) => Promise - verifyIdentity: (args: VerifyIdentityArgs) => Promise updateProfile: (args: ProfileArgs) => Promise - getDocuments: (cookies?: string) => Promise - getCountries: (cookies?: string) => Promise changePassword: (args: ChangePasswordArgs) => Promise resendVerifyEmail: ( args: ResendVerificationEmailArgs ) => Promise + getGateHubIframeSrc: ( + type: IFRAME_TYPE, + cookies?: string + ) => Promise } const createUserService = (): UserService => ({ @@ -355,99 +288,53 @@ const createUserService = (): UserService => ({ } }, - async createWallet(args) { - try { - const response = await httpClient - .post('wallet', { - json: { - ...args, - country: args.country.value - } - }) - .json() - return response - } catch (error) { - return getError( - error, - 'Something went wrong while trying to create your wallet. Please try again.' - ) - } - }, - - async verifyIdentity(args) { + async updateProfile(args) { try { const response = await httpClient - .post('verify', { + .post('updateProfile', { json: args }) .json() return response } catch (error) { - return getError( + return getError( error, - 'Something went wrong while verifying your ID. Please try again.' + 'Something went wrong while updating your profile. Please try again.' ) } }, - async updateProfile(args) { + async changePassword(args) { try { const response = await httpClient - .post('updateProfile', { + .patch('change-password', { json: args }) .json() return response } catch (error) { - return getError( + return getError( error, - 'Something went wrong while updating your profile. Please try again.' + 'Something went wrong while changing your password. Please try again.' ) } }, - async getDocuments(cookies) { - try { - const response = await httpClient - .get('documents', { - headers: { - ...(cookies ? { Cookie: cookies } : {}) - } - }) - .json>() - return response?.result ?? [] - } catch (error) { - return [] - } - }, - - async getCountries(cookies) { + async getGateHubIframeSrc(type, cookies) { try { const response = await httpClient - .get('countries', { + .get(`iframe-urls/${type}`, { headers: { ...(cookies ? { Cookie: cookies } : {}) } }) - .json>() - return response?.result ?? [] - } catch (error) { - return [] - } - }, - - async changePassword(args) { - try { - const response = await httpClient - .patch('change-password', { - json: args - }) - .json() + .json() return response } catch (error) { - return getError( + return getError( error, - 'Something went wrong while changing your password. Please try again.' + // TODO: Better error message + 'Something went wrong. Please try again.' ) } } diff --git a/packages/wallet/frontend/src/middleware.ts b/packages/wallet/frontend/src/middleware.ts index 0dca53418..b2ff773d0 100644 --- a/packages/wallet/frontend/src/middleware.ts +++ b/packages/wallet/frontend/src/middleware.ts @@ -10,6 +10,9 @@ const isPublicPath = (path: string) => { const publicPaths = ['/auth*'] +// TODO: Update middleware for the new KYC +// We might want to showcase the users that the identity verification is in +// progress and probably do not let them perform any action (sending money, etc). export async function middleware(req: NextRequest) { const isPublic = isPublicPath(req.nextUrl.pathname) const nextPage = req.nextUrl.searchParams.get('next') @@ -31,12 +34,10 @@ export async function middleware(req: NextRequest) { return NextResponse.redirect(url) } - if ( - response.result?.needsIDProof && - req.nextUrl.pathname !== '/kyc/proof' - ) { + console.log(response.result) + if (response.result?.needsIDProof && req.nextUrl.pathname !== '/kyc') { if (nextPage !== 'proof') - return NextResponse.redirect(new URL('/kyc/proof', req.url)) + return NextResponse.redirect(new URL('/kyc', req.url)) } // If KYC is completed and the user tries to navigate to the page, redirect diff --git a/packages/wallet/frontend/src/pages/account/[accountId].tsx b/packages/wallet/frontend/src/pages/account/[accountId].tsx index 285b88cae..e508eda4d 100644 --- a/packages/wallet/frontend/src/pages/account/[accountId].tsx +++ b/packages/wallet/frontend/src/pages/account/[accountId].tsx @@ -1,6 +1,4 @@ import { CreateWalletAddressDialog } from '@/components/dialogs/CreateWalletAddressDialog' -import { FundAccountDialog } from '@/components/dialogs/FundAccountDialog' -import { WithdrawFundsDialog } from '@/components/dialogs/WithdrawFundsDialog' import { New } from '@/components/icons/New' import { Withdraw } from '@/components/icons/Withdraw' import { Request } from '@/components/icons/Request' @@ -23,6 +21,7 @@ import { balanceState } from '@/lib/balance' import { PageHeader } from '@/components/PageHeader' import { WalletAddressResponse } from '@wallet/shared' import { WalletAddressesTable } from '@/components/WalletAddressesTable' +import { Link } from '@/ui/Link' type AccountPageProps = InferGetServerSidePropsType @@ -84,37 +83,26 @@ const AccountPage: NextPageWithLayout = ({ Add payment pointer - - +

Payment Pointers

diff --git a/packages/wallet/frontend/src/pages/deposit.tsx b/packages/wallet/frontend/src/pages/deposit.tsx new file mode 100644 index 000000000..a5860a000 --- /dev/null +++ b/packages/wallet/frontend/src/pages/deposit.tsx @@ -0,0 +1,64 @@ +import { AppLayout } from '@/components/layouts/AppLayout' +import { PageHeader } from '@/components/PageHeader' +import { userService } from '@/lib/api/user' +import { NextPageWithLayout } from '@/lib/types/app' +import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types' +import { useEffect } from 'react' + +type DepositPageProps = InferGetServerSidePropsType + +const DepositPage: NextPageWithLayout = ({ url }) => { + useEffect(() => { + const onDepositComplete = (e: MessageEvent) => { + // TODO: Handle the received message from iframe + // https://docs.gatehub.net/api-documentation/c3OPAp5dM191CDAdwyYS/gatehub-products/gatehub-onboarding#message-events + console.log('received message from iframe', { e }) + } + window.addEventListener('message', onDepositComplete, false) + + return () => { + window.removeEventListener('message', onDepositComplete) + } + }, []) + + return ( + <> + + + + ) +} + +export const getServerSideProps: GetServerSideProps<{ + url: string +}> = async (ctx) => { + const response = await userService.getGateHubIframeSrc( + 'deposit', + ctx.req.headers.cookie + ) + + if (!response.success || !response.result) { + return { + notFound: true + } + } + + const url = new URL(response.result.url) + url.searchParams.append('paymentType', 'deposit') + + return { + props: { + url: url.href + } + } +} + +DepositPage.getLayout = function (page) { + return {page} +} + +export default DepositPage diff --git a/packages/wallet/frontend/src/pages/kyc.tsx b/packages/wallet/frontend/src/pages/kyc.tsx new file mode 100644 index 000000000..089a2df29 --- /dev/null +++ b/packages/wallet/frontend/src/pages/kyc.tsx @@ -0,0 +1,119 @@ +import { HeaderLogo } from '@/components/HeaderLogo' +import AuthLayout from '@/components/layouts/AuthLayout' +import { userService } from '@/lib/api/user' +import { NextPageWithLayout } from '@/lib/types/app' +import { + GateHubMessageType, + type GateHubMessageError +} from '@/lib/types/windowMessages' +import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types' +import { useEffect } from 'react' + +type KYCPageProps = InferGetServerSidePropsType + +type MessageData = + | { + type: GateHubMessageType.OnboardingCompleted + value: 'submitted' | 'resubmitted' + } + | { type: GateHubMessageType.OnboardingError; value: GateHubMessageError } + | { type: GateHubMessageType.OnboardingInitialized } + +const KYCPage: NextPageWithLayout = ({ + url, + addUserToGatewayUrl +}) => { + // const [openDialog, closeDialog] = useDialog() + // const router = useRouter() + + useEffect(() => { + // TODO: Handle the received message from iframe + // https://docs.gatehub.net/api-documentation/c3OPAp5dM191CDAdwyYS/gatehub-products/gatehub-onboarding#message-events + const onMessage = async (e: MessageEvent) => { + console.log('received message from iframe', { e }) + switch (e.data.type) { + case GateHubMessageType.OnboardingCompleted: + console.log( + 'received message from iframe', + GateHubMessageType.OnboardingCompleted, + JSON.stringify(e.data, null, 2) + ) + await fetch(addUserToGatewayUrl, { + method: 'POST', + body: JSON.stringify(e.data, null, 2), + credentials: 'include' + }) + break + case GateHubMessageType.OnboardingError: + console.log( + 'received message from iframe', + GateHubMessageType.OnboardingError, + JSON.stringify(e.data, null, 2) + ) + break + case GateHubMessageType.OnboardingInitialized: + console.log( + 'received message from iframe', + GateHubMessageType.OnboardingInitialized, + JSON.stringify(e.data, null, 2) + ) + break + } + } + window.addEventListener('message', onMessage, false) + + return () => { + window.removeEventListener('message', onMessage) + } + }, [addUserToGatewayUrl]) + + return ( + <> +

+ Personal Details +

+
+ +
+ + ) +} + +export const getServerSideProps: GetServerSideProps<{ + url: string + addUserToGatewayUrl: string +}> = async (ctx) => { + const response = await userService.getGateHubIframeSrc( + 'onboarding', + ctx.req.headers.cookie + ) + + if (!response.success || !response.result) { + return { + notFound: true + } + } + + return { + props: { + url: response.result.url, + addUserToGatewayUrl: `${process.env.NEXT_PUBLIC_BACKEND_URL}/gatehub/add-user-to-gateway` + } + } +} + +KYCPage.getLayout = function (page) { + return ( + + + {page} + + ) +} + +export default KYCPage diff --git a/packages/wallet/frontend/src/pages/kyc/personal.tsx b/packages/wallet/frontend/src/pages/kyc/personal.tsx deleted file mode 100644 index f7b0c09a6..000000000 --- a/packages/wallet/frontend/src/pages/kyc/personal.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { HeaderLogo } from '@/components/HeaderLogo' -import { SuccessDialog } from '@/components/dialogs/SuccessDialog' -import AuthLayout from '@/components/layouts/AuthLayout' -import { personalDetailsSchema, userService } from '@/lib/api/user' -import { useDialog } from '@/lib/hooks/useDialog' -import { useZodForm } from '@/lib/hooks/useZodForm' -import { NextPageWithLayout } from '@/lib/types/app' -import { Button } from '@/ui/Button' -import { Form } from '@/ui/forms/Form' -import { Input } from '@/ui/forms/Input' -import { Select, SelectOption } from '@/ui/forms/Select' -import { USE_TEST_DATA_KYC } from '@/utils/constants' -import { getObjectKeys } from '@/utils/helpers' -import { useRouter } from 'next/router' -import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types' -import { Controller } from 'react-hook-form' - -type PersonalDetailsProps = InferGetServerSidePropsType< - typeof getServerSideProps -> - -const PersonalDetailsPage: NextPageWithLayout = ({ - countries -}) => { - const [openDialog, closeDialog] = useDialog() - const router = useRouter() - - const defaultTestValues = { - city: 'Copenhagen', - address: 'Den Lille Havfrue', - zip: '2100', - country: { - value: 'DK', - label: 'Denmark' - } - } - - const personalDetailsForm = useZodForm({ - schema: personalDetailsSchema, - defaultValues: { ...(USE_TEST_DATA_KYC ? defaultTestValues : {}) } - }) - - return ( - <> -

- Personal Details -

- {USE_TEST_DATA_KYC && ( - - Denmark is selected by default for testing purposes! - - )} -
{ - const response = await userService.createWallet(data) - - if (response.success) { - openDialog( - { - router.push('/kyc/proof') - closeDialog() - }} - content="Your wallet was created." - redirect="/kyc/proof" - redirectText="Verify your identity" - /> - ) - } else { - const { errors, message } = response - personalDetailsForm.setError('root', { message }) - - if (errors) { - getObjectKeys(errors).map((field) => - personalDetailsForm.setError(field, { message: errors[field] }) - ) - } - } - }} - > -
- - -
- ( - - required - label="Country" - placeholder="Select country..." - options={countries} - value={value} - isDisabled={USE_TEST_DATA_KYC} - error={personalDetailsForm.formState.errors.country?.message} - onChange={(option) => { - if (option) { - personalDetailsForm.setValue('country', { ...option }) - } - }} - /> - )} - /> - - - - - - - ) -} - -export const getServerSideProps: GetServerSideProps<{ - countries: SelectOption[] -}> = async (ctx) => { - const countries = await userService.getCountries(ctx.req.headers.cookie) - return { - props: { - countries - } - } -} - -PersonalDetailsPage.getLayout = function (page) { - return ( - - - {page} - - ) -} - -export default PersonalDetailsPage diff --git a/packages/wallet/frontend/src/pages/kyc/proof.tsx b/packages/wallet/frontend/src/pages/kyc/proof.tsx deleted file mode 100644 index dd2a4c492..000000000 --- a/packages/wallet/frontend/src/pages/kyc/proof.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { HeaderLogo } from '@/components/HeaderLogo' -import { SuccessDialog } from '@/components/dialogs/SuccessDialog' -import AuthLayout from '@/components/layouts/AuthLayout' -import { - userService, - verifyIdentitySchema, - type Document -} from '@/lib/api/user' -import { useDialog } from '@/lib/hooks/useDialog' -import { useZodForm } from '@/lib/hooks/useZodForm' -import { NextPageWithLayout } from '@/lib/types/app' -import { Button } from '@/ui/Button' -import { FieldError } from '@/ui/forms/FieldError' -import { FileUpload } from '@/ui/forms/FileUpload' -import { Form } from '@/ui/forms/Form' -import { USE_TEST_DATA_KYC } from '@/utils/constants' -import { getObjectKeys } from '@/utils/helpers' -import { testImageVerifyIdentity, testImageType } from '@/utils/mocks' -import { cx } from 'class-variance-authority' -import { useRouter } from 'next/router' -import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types' -import { useState, SyntheticEvent } from 'react' - -type IDVerificationPage = InferGetServerSidePropsType - -const IDVerificationPage: NextPageWithLayout = ({ - documents -}) => { - const [openDialog, closeDialog] = useDialog() - const [isBackRequired, setIsBackRequired] = useState(false) - const router = useRouter() - - const handleFileOnChange = (event: SyntheticEvent) => { - const reader = new FileReader() - const target = event.currentTarget - if (target.files && target.files.length > 0) { - const file = target.files[0] - reader.readAsDataURL(file) - reader.onloadend = () => { - let fileBase64 = reader.result?.toString() || '' - fileBase64 = fileBase64.slice(fileBase64.indexOf('base64') + 7) - if (target.name === 'faceImageUpload') { - verifyIdentityForm.setValue('faceImage', fileBase64) - verifyIdentityForm.setValue('faceImageType', file.type) - verifyIdentityForm.trigger('faceImage') - } else if (target.name === 'frontSideIDUpload') { - verifyIdentityForm.setValue('frontSideImage', fileBase64) - verifyIdentityForm.setValue('frontSideImageType', file.type) - verifyIdentityForm.trigger('frontSideImage') - } else { - verifyIdentityForm.setValue('backSideImage', fileBase64) - verifyIdentityForm.setValue('backSideImageType', file.type) - verifyIdentityForm.trigger('backSideImage') - } - } - } - } - - const handleDocumentChange = (event: SyntheticEvent) => { - const target = event.currentTarget - setIsBackRequired(target.getAttribute('data-back-id') === 'true') - if (!isBackRequired) { - verifyIdentityForm.setValue('backSideImage', '') - verifyIdentityForm.setValue('backSideImageType', '') - verifyIdentityForm.trigger('backSideImage') - } - } - - const verifyIdentityForm = useZodForm({ - schema: verifyIdentitySchema, - defaultValues: { - ...(USE_TEST_DATA_KYC ? { documentType: 'PA' } : {}) - } - }) - - return ( - <> - {USE_TEST_DATA_KYC && ( -

- For testing purposes Passport is selected, and images uploaded by - default! -

- )} - -
{ - const response = await userService.verifyIdentity(data) - - if (response.success) { - openDialog( - { - router.reload() - closeDialog() - }} - onSuccess={() => { - window.localStorage.setItem( - 'isUserFirstTimeOnTestnet', - 'true' - ) - router.reload() - }} - content="Your identity has been verified." - redirectText="Go to your account overview" - /> - ) - } else { - const { errors, message } = response - verifyIdentityForm.setError('root', { message }) - - if (errors) { - getObjectKeys(errors).map((field) => - verifyIdentityForm.setError(field, { message: errors[field] }) - ) - } - } - }} - > -
-
- {documents.map((document) => ( -
- Use Passport as default value - value={USE_TEST_DATA_KYC ? 'PA' : document.type} - data-back-id={document.isBackRequired} - {...verifyIdentityForm.register('documentType', { - onChange: handleDocumentChange - })} - /> - -
- ))} -
- -
-
-
- <> - - - - -
-
- <> - - - - -
-
- {isBackRequired && ( - <> - - - - - )} -
-
- - - - ) -} - -export const getServerSideProps: GetServerSideProps<{ - documents: Document[] -}> = async (ctx) => { - const documents = await userService.getDocuments(ctx.req.headers.cookie) - return { - props: { - documents - } - } -} - -IDVerificationPage.getLayout = function (page) { - return ( - - - {page} - - ) -} - -export default IDVerificationPage diff --git a/packages/wallet/frontend/src/pages/withdraw.tsx b/packages/wallet/frontend/src/pages/withdraw.tsx new file mode 100644 index 000000000..b1cd1a62b --- /dev/null +++ b/packages/wallet/frontend/src/pages/withdraw.tsx @@ -0,0 +1,64 @@ +import { AppLayout } from '@/components/layouts/AppLayout' +import { PageHeader } from '@/components/PageHeader' +import { userService } from '@/lib/api/user' +import { NextPageWithLayout } from '@/lib/types/app' +import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types' +import { useEffect } from 'react' + +type WithdrawPageProps = InferGetServerSidePropsType + +const WithdrawPage: NextPageWithLayout = ({ url }) => { + useEffect(() => { + const onWithdrawComplete = (e: MessageEvent) => { + // TODO: Handle the received message from iframe + // https://docs.gatehub.net/api-documentation/c3OPAp5dM191CDAdwyYS/gatehub-products/gatehub-onboarding#message-events + console.log('received message from iframe', { e }) + } + window.addEventListener('message', onWithdrawComplete, false) + + return () => { + window.removeEventListener('message', onWithdrawComplete) + } + }, []) + + return ( + <> + + + + ) +} + +export const getServerSideProps: GetServerSideProps<{ + url: string +}> = async (ctx) => { + const response = await userService.getGateHubIframeSrc( + 'withdrawal', + ctx.req.headers.cookie + ) + + if (!response.success || !response.result) { + return { + notFound: true + } + } + + const url = new URL(response.result.url) + url.searchParams.append('paymentType', 'withdraw') + + return { + props: { + url: url.href + } + } +} + +WithdrawPage.getLayout = function (page) { + return {page} +} + +export default WithdrawPage diff --git a/packages/wallet/frontend/src/utils/constants.ts b/packages/wallet/frontend/src/utils/constants.ts index f04c58c8e..dd3e9d1e8 100644 --- a/packages/wallet/frontend/src/utils/constants.ts +++ b/packages/wallet/frontend/src/utils/constants.ts @@ -1,10 +1,3 @@ -/** - * Default values for countries and documents - */ -export const USE_TEST_DATA_KYC = - process.env.NEXT_PUBLIC_USE_TEST_KYC_DATA === 'true' -export const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png'] - export const OPEN_PAYMENTS_HOST = process.env.NEXT_PUBLIC_OPEN_PAYMENTS_HOST /** diff --git a/packages/wallet/frontend/src/utils/mocks.ts b/packages/wallet/frontend/src/utils/mocks.ts deleted file mode 100644 index 3c03a5341..000000000 --- a/packages/wallet/frontend/src/utils/mocks.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const testImageType = 'image/jpeg' -export const testImageVerifyIdentity = - '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' diff --git a/packages/wallet/shared/src/types/account.ts b/packages/wallet/shared/src/types/account.ts index fdf71c9fb..974c03472 100644 --- a/packages/wallet/shared/src/types/account.ts +++ b/packages/wallet/shared/src/types/account.ts @@ -1,16 +1,5 @@ import { z } from 'zod' -export const fundAccountSchema = z.object({ - accountId: z.string().uuid(), - amount: z.coerce - .number({ - invalid_type_error: 'Please enter a valid amount' - }) - .positive({ message: 'Please enter an amount' }) -}) - -export const withdrawFundsSchema = fundAccountSchema - export const createAccountSchema = z.object({ name: z .string() diff --git a/packages/wallet/shared/src/types/card.ts b/packages/wallet/shared/src/types/card.ts new file mode 100644 index 000000000..10af60c56 --- /dev/null +++ b/packages/wallet/shared/src/types/card.ts @@ -0,0 +1,54 @@ +export type LockReasonCode = + | 'ClientRequestedLock' + | 'LostCard' + | 'StolenCard' + | 'IssuerRequestGeneral' + | 'IssuerRequestFraud' + | 'IssuerRequestLegal' + +export type BlockReasonCode = + | 'LostCard' + | 'StolenCard' + | 'IssuerRequestGeneral' + | 'IssuerRequestFraud' + | 'IssuerRequestLegal' + | 'IssuerRequestIncorrectOpening' + | 'CardDamagedOrNotWorking' + | 'UserRequest' + | 'IssuerRequestCustomerDeceased' + | 'ProductDoesNotRenew' + +// Response for fetching card transactions +export interface ITransaction { + id: number + transactionId: string + ghResponseCode: string + cardScheme: number + type: number + createdAt: string + txStatus: string + vaultId: number + cardId: number + refTransactionId: string + responseCode: string | null + transactionAmount: string + transactionCurrency: string + billingAmount: string + billingCurrency: string + terminalId: string | null + wallet: number + transactionDateTime: string + processDateTime: string | null +} + +export interface IPagination { + pageNumber: number + pageSize: number + totalPages: number + totalRecords: number +} + +export interface IGetTransactionsResponse { + data: ITransaction[] + pagination: IPagination +} diff --git a/packages/wallet/shared/src/types/index.ts b/packages/wallet/shared/src/types/index.ts index d8baf80ee..ad25b2eb9 100644 --- a/packages/wallet/shared/src/types/index.ts +++ b/packages/wallet/shared/src/types/index.ts @@ -8,3 +8,4 @@ export * from './grant' export * from './walletAddress' export * from './user' export * from './iframe' +export * from './card' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68a8b5624..707465cc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 prettier-plugin-tailwindcss: - specifier: ^0.6.6 - version: 0.6.6(prettier@3.3.3) + specifier: ^0.6.8 + version: 0.6.8(prettier@3.3.3) typescript: specifier: ^5.6.2 version: 5.6.2 @@ -75,8 +75,8 @@ importers: specifier: ^3.1.0 version: 3.1.0(pg@8.13.0) objection: - specifier: ^3.1.4 - version: 3.1.4(knex@3.1.0) + specifier: ^3.1.5 + version: 3.1.5(knex@3.1.0) pg: specifier: ^8.13.0 version: 8.13.0 @@ -125,25 +125,25 @@ importers: version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) '@radix-ui/react-dialog': specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.1 - version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.0 - version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 - version: 1.1.0(@types/react@18.3.8)(react@18.3.1) + version: 1.1.0(@types/react@18.3.10)(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.0 - version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toast': specifier: ^1.2.1 - version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': specifier: ^5.56.2 version: 5.56.2(react@18.3.1) @@ -173,29 +173,29 @@ importers: version: 2.5.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.12) + version: 1.0.7(tailwindcss@3.4.13) valtio: specifier: ^2.0.0 - version: 2.0.0(@types/react@18.3.8)(react@18.3.1) + version: 2.0.0(@types/react@18.3.10)(react@18.3.1) zod: specifier: ^3.23.8 version: 3.23.8 devDependencies: '@tailwindcss/forms': specifier: ^0.5.9 - version: 0.5.9(tailwindcss@3.4.12) + version: 0.5.9(tailwindcss@3.4.13) '@tailwindcss/typography': specifier: ^0.5.15 - version: 0.5.15(tailwindcss@3.4.12) + version: 0.5.15(tailwindcss@3.4.13) '@types/react': - specifier: 18.3.8 - version: 18.3.8 + specifier: 18.3.10 + version: 18.3.10 '@types/react-dom': specifier: 18.3.0 version: 18.3.0 '@vitejs/plugin-react-swc': specifier: ^3.7.0 - version: 3.7.0(@swc/helpers@0.5.5)(vite@5.4.7(@types/node@22.5.5)) + version: 3.7.0(@swc/helpers@0.5.5)(vite@5.4.8(@types/node@22.5.5)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.47) @@ -203,14 +203,14 @@ importers: specifier: ^8.4.47 version: 8.4.47 tailwindcss: - specifier: ^3.4.12 - version: 3.4.12 + specifier: ^3.4.13 + version: 3.4.13 typescript: specifier: ^5.6.2 version: 5.6.2 vite: - specifier: ^5.4.7 - version: 5.4.7(@types/node@22.5.5) + specifier: ^5.4.8 + version: 5.4.8(@types/node@22.5.5) packages/boutique/shared: dependencies: @@ -240,8 +240,8 @@ importers: specifier: ^3.1.0 version: 3.1.0(pg@8.13.0) objection: - specifier: ^3.1.4 - version: 3.1.4(knex@3.1.0) + specifier: ^3.1.5 + version: 3.1.5(knex@3.1.0) winston: specifier: ^3.14.2 version: 3.14.2 @@ -328,8 +328,8 @@ importers: specifier: ^5.1.2 version: 5.1.2 objection: - specifier: ^3.1.4 - version: 3.1.4(knex@3.1.0) + specifier: ^3.1.5 + version: 3.1.5(knex@3.1.0) pg: specifier: ^8.13.0 version: 8.13.0 @@ -353,8 +353,8 @@ importers: version: 3.23.8 devDependencies: '@faker-js/faker': - specifier: ^9.0.1 - version: 9.0.1 + specifier: ^9.0.3 + version: 9.0.3 '@graphql-codegen/cli': specifier: ^5.0.2 version: 5.0.2(@types/node@20.14.15)(graphql@16.9.0)(typescript@5.6.2) @@ -411,10 +411,10 @@ importers: version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) '@radix-ui/react-toast': specifier: ^1.2.1 - version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wallet/shared': specifier: workspace:* version: link:../shared @@ -444,10 +444,10 @@ importers: version: 7.53.0(react@18.3.1) react-joyride: specifier: ^2.9.2 - version: 2.9.2(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.9.2(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-select: specifier: ^5.8.1 - version: 5.8.1(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.8.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sharp: specifier: ^0.33.5 version: 0.33.5 @@ -459,10 +459,10 @@ importers: version: 2.5.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.12) + version: 1.0.7(tailwindcss@3.4.13) valtio: specifier: ^2.0.0 - version: 2.0.0(@types/react@18.3.8)(react@18.3.1) + version: 2.0.0(@types/react@18.3.10)(react@18.3.1) zod: specifier: ^3.23.8 version: 3.23.8 @@ -475,7 +475,7 @@ importers: version: 13.5.6 '@tailwindcss/forms': specifier: ^0.5.9 - version: 0.5.9(tailwindcss@3.4.12) + version: 0.5.9(tailwindcss@3.4.13) '@types/node': specifier: ^20.12.11 version: 20.14.15 @@ -483,8 +483,8 @@ importers: specifier: ^0.2.3 version: 0.2.3 '@types/react': - specifier: 18.3.8 - version: 18.3.8 + specifier: 18.3.10 + version: 18.3.10 '@types/react-dom': specifier: 18.3.0 version: 18.3.0 @@ -495,8 +495,8 @@ importers: specifier: ^8.4.47 version: 8.4.47 tailwindcss: - specifier: ^3.4.12 - version: 3.4.12 + specifier: ^3.4.13 + version: 3.4.13 typescript: specifier: ^5.6.2 version: 5.6.2 @@ -1074,8 +1074,8 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@faker-js/faker@9.0.1': - resolution: {integrity: sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==} + '@faker-js/faker@9.0.3': + resolution: {integrity: sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} '@fastify/busboy@2.1.1': @@ -2437,8 +2437,8 @@ packages: '@types/react-transition-group@4.4.11': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} - '@types/react@18.3.8': - resolution: {integrity: sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==} + '@types/react@18.3.10': + resolution: {integrity: sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==} '@types/request@2.48.12': resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} @@ -4854,8 +4854,8 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} - objection@3.1.4: - resolution: {integrity: sha512-BI1YQ18JicfoODgCdKxmw4W8f24/e9hCEQpOTux0xmyd8hOidOzDd1WopOMxqxo7FA+Jfw8XTfZIUaqDnS7r0g==} + objection@3.1.5: + resolution: {integrity: sha512-Hx/ipAwXSuRBbOMWFKtRsAN0yITafqXtWB4OT4Z9wED7ty1h7bOnBdhLtcNus23GwLJqcMsRWdodL2p5GwlnfQ==} engines: {node: '>=14.0.0'} peerDependencies: knex: '>=1.0.1' @@ -5155,8 +5155,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-tailwindcss@0.6.6: - resolution: {integrity: sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==} + prettier-plugin-tailwindcss@0.6.8: + resolution: {integrity: sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -5884,8 +5884,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.12: - resolution: {integrity: sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==} + tailwindcss@3.4.13: + resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==} engines: {node: '>=14.0.0'} hasBin: true @@ -6209,8 +6209,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@5.4.7: - resolution: {integrity: sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==} + vite@5.4.8: + resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6902,7 +6902,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.13.0(@types/react@18.3.8)(react@18.3.1)': + '@emotion/react@11.13.0(@types/react@18.3.10)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 '@emotion/babel-plugin': 11.12.0 @@ -6914,7 +6914,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 transitivePeerDependencies: - supports-color @@ -7030,7 +7030,7 @@ snapshots: '@eslint/js@8.57.1': {} - '@faker-js/faker@9.0.1': {} + '@faker-js/faker@9.0.3': {} '@fastify/busboy@2.1.1': {} @@ -8000,351 +8000,351 @@ snapshots: '@radix-ui/primitive@1.1.0': {} - '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-context@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-context@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.7(@types/react@18.3.8)(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.10)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-direction@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-direction@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-id@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-id@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.7(@types/react@18.3.8)(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.10)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.7(@types/react@18.3.8)(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.10)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.10)(react@18.3.1) '@radix-ui/rect': 1.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-slot@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-slot@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-tabs@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tabs@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-toast@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toast@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-tooltip@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.8)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.10)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 - '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.0 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-use-size@1.1.0(@types/react@18.3.8)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.10)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.10)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-dom': 18.3.0 '@radix-ui/rect@1.1.0': {} @@ -8529,18 +8529,18 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/forms@0.5.9(tailwindcss@3.4.12)': + '@tailwindcss/forms@0.5.9(tailwindcss@3.4.13)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.12 + tailwindcss: 3.4.13 - '@tailwindcss/typography@0.5.15(tailwindcss@3.4.12)': + '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.12 + tailwindcss: 3.4.13 '@tanstack/query-core@5.56.2': {} @@ -8686,13 +8686,13 @@ snapshots: '@types/react-dom@18.3.0': dependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 '@types/react-transition-group@4.4.11': dependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - '@types/react@18.3.8': + '@types/react@18.3.10': dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 @@ -8829,10 +8829,10 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.7.0(@swc/helpers@0.5.5)(vite@5.4.7(@types/node@22.5.5))': + '@vitejs/plugin-react-swc@3.7.0(@swc/helpers@0.5.5)(vite@5.4.8(@types/node@22.5.5))': dependencies: '@swc/core': 1.7.10(@swc/helpers@0.5.5) - vite: 5.4.7(@types/node@22.5.5) + vite: 5.4.8(@types/node@22.5.5) transitivePeerDependencies: - '@swc/helpers' @@ -11647,7 +11647,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - objection@3.1.4(knex@3.1.0): + objection@3.1.5(knex@3.1.0): dependencies: ajv: 8.17.1 ajv-formats: 2.1.1(ajv@8.17.1) @@ -11953,7 +11953,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.6.6(prettier@3.3.3): + prettier-plugin-tailwindcss@0.6.8(prettier@3.3.3): dependencies: prettier: 3.3.3 @@ -12095,16 +12095,16 @@ snapshots: dependencies: react: 18.3.1 - react-innertext@1.1.5(@types/react@18.3.8)(react@18.3.1): + react-innertext@1.1.5(@types/react@18.3.10)(react@18.3.1): dependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 react: 18.3.1 react-is@16.13.1: {} react-is@18.3.1: {} - react-joyride@2.9.2(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-joyride@2.9.2(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@gilbarbara/deep-equal': 0.3.1 deep-diff: 1.0.2 @@ -12113,7 +12113,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-floater: 0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-innertext: 1.1.5(@types/react@18.3.8)(react@18.3.1) + react-innertext: 1.1.5(@types/react@18.3.10)(react@18.3.1) react-is: 16.13.1 scroll: 3.0.1 scrollparent: 2.1.0 @@ -12122,24 +12122,24 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-remove-scroll-bar@2.3.6(@types/react@18.3.8)(react@18.3.1): + react-remove-scroll-bar@2.3.6(@types/react@18.3.10)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.1(@types/react@18.3.8)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.10)(react@18.3.1) tslib: 2.6.3 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - react-remove-scroll@2.5.7(@types/react@18.3.8)(react@18.3.1): + react-remove-scroll@2.5.7(@types/react@18.3.10)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.6(@types/react@18.3.8)(react@18.3.1) - react-style-singleton: 2.2.1(@types/react@18.3.8)(react@18.3.1) + react-remove-scroll-bar: 2.3.6(@types/react@18.3.10)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.10)(react@18.3.1) tslib: 2.6.3 - use-callback-ref: 1.3.2(@types/react@18.3.8)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.8)(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.10)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.10)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -12153,11 +12153,11 @@ snapshots: '@remix-run/router': 1.19.2 react: 18.3.1 - react-select@5.8.1(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-select@5.8.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.0 '@emotion/cache': 11.13.1 - '@emotion/react': 11.13.0(@types/react@18.3.8)(react@18.3.1) + '@emotion/react': 11.13.0(@types/react@18.3.10)(react@18.3.1) '@floating-ui/dom': 1.6.10 '@types/react-transition-group': 4.4.11 memoize-one: 6.0.0 @@ -12165,19 +12165,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.8)(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.10)(react@18.3.1) transitivePeerDependencies: - '@types/react' - supports-color - react-style-singleton@2.2.1(@types/react@18.3.8)(react@18.3.1): + react-style-singleton@2.2.1(@types/react@18.3.10)(react@18.3.1): dependencies: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 tslib: 2.6.3 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -12763,11 +12763,11 @@ snapshots: tailwind-merge@2.5.2: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.12): + tailwindcss-animate@1.0.7(tailwindcss@3.4.13): dependencies: - tailwindcss: 3.4.12 + tailwindcss: 3.4.13 - tailwindcss@3.4.12: + tailwindcss@3.4.13: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -13067,26 +13067,26 @@ snapshots: urlpattern-polyfill@8.0.2: {} - use-callback-ref@1.3.2(@types/react@18.3.8)(react@18.3.1): + use-callback-ref@1.3.2(@types/react@18.3.10)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.6.3 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - use-isomorphic-layout-effect@1.1.2(@types/react@18.3.8)(react@18.3.1): + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.10)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 - use-sidecar@1.1.2(@types/react@18.3.8)(react@18.3.1): + use-sidecar@1.1.2(@types/react@18.3.10)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.6.3 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 util-deprecate@1.0.2: {} @@ -13104,18 +13104,18 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - valtio@2.0.0(@types/react@18.3.8)(react@18.3.1): + valtio@2.0.0(@types/react@18.3.10)(react@18.3.1): dependencies: proxy-compare: 3.0.0 optionalDependencies: - '@types/react': 18.3.8 + '@types/react': 18.3.10 react: 18.3.1 value-or-promise@1.0.12: {} vary@1.1.2: {} - vite@5.4.7(@types/node@22.5.5): + vite@5.4.8(@types/node@22.5.5): dependencies: esbuild: 0.21.5 postcss: 8.4.47