diff --git a/.changeset/happy-spoons-sort.md b/.changeset/happy-spoons-sort.md new file mode 100644 index 000000000..a8b6243b5 --- /dev/null +++ b/.changeset/happy-spoons-sort.md @@ -0,0 +1,10 @@ +--- +"@lightsparkdev/lightspark-sdk": patch +"@lightsparkdev/oauth": patch +"@lightsparkdev/core": patch +"@lightsparkdev/ui": patch +--- + +* Surface error name when the requester hits a graphQL error. +* Update Turbo +* Several small UI package improvements. diff --git a/apps/examples/uma-vasp-cli/package.json b/apps/examples/uma-vasp-cli/package.json index c175699a5..9ddce084e 100644 --- a/apps/examples/uma-vasp-cli/package.json +++ b/apps/examples/uma-vasp-cli/package.json @@ -45,7 +45,7 @@ "@inquirer/prompts": "^1.1.3", "@lightsparkdev/core": "1.3.0", "@lightsparkdev/lightspark-sdk": "1.9.0", - "@uma-sdk/core": "^1.2.3", + "@uma-sdk/core": "^1.3.0", "chalk": "^5.3.0", "commander": "^11.0.0" }, diff --git a/apps/examples/uma-vasp/README.md b/apps/examples/uma-vasp/README.md index 867661940..3df4df58c 100644 --- a/apps/examples/uma-vasp/README.md +++ b/apps/examples/uma-vasp/README.md @@ -7,10 +7,10 @@ An example UMA VASP server implementation using Typescript. Configure environment variables needed to talk to Lightspark and UMA messages (API keys, etc.). Information on how to set them can be found in `src/UmaConfig.ts`. 1. Create an API token (`LIGHTSPARK_API_TOKEN_CLIENT_ID`, `LIGHTSPARK_API_TOKEN_CLIENT_SECRET`) -in your account's [API config page](https://app.lightspark.com/api-config). + in your account's [API config page](https://app.lightspark.com/api-config). 1. Find your node credentials (`LIGHTSPARK_UMA_NODE_ID`, `LIGHTSPARK_UMA_OSK_NODE_SIGNING_KEY_PASSWORD`) -in your account's [API config page](https://app.lightspark.com/api-config). + in your account's [API config page](https://app.lightspark.com/api-config). 1. Create a secp256k1 private key to use as your encryption private key (`LIGHTSPARK_UMA_ENCRYPTION_PRIVKEY`) and use this private key to wrap the corresponding encryption public key in an X.509 Certificate (`LIGHTSPARK_UMA_ENCRYPTION_CERT_CHAIN`). Similarly for signing, create a secp256k1 private key to use as your signing private key (`LIGHTSPARK_UMA_SIGNING_PRIVKEY`) and use this private key to wrap the corresponding signing public key in an X.509 Certificate (`LIGHTSPARK_UMA_SIGNING_CERT_CHAIN`). You may choose to use the same keypair for encryption and signing. For information on generating these, see [our docs](https://docs.uma.me/uma-standard/keys-authentication-encryption). @@ -115,7 +115,7 @@ $ PORT=8081 yarn start Now, you can test the full uma flow like: ```bash -# First, call to vasp1 to lookup Bob at vasp2. This will return currency conversion info, etc. It will also contain a +# First, call to vasp1 to lookup Bob at vasp2. This will return currency conversion info, etc. It will also contain a # callback ID that you'll need for the next call $ curl -X GET http://localhost:8080/api/umalookup/\$bob@localhost:8081 -u bob:pa55word diff --git a/apps/examples/uma-vasp/package.json b/apps/examples/uma-vasp/package.json index d2c7eae75..07d55883c 100644 --- a/apps/examples/uma-vasp/package.json +++ b/apps/examples/uma-vasp/package.json @@ -17,8 +17,9 @@ "dependencies": { "@lightsparkdev/core": "1.3.0", "@lightsparkdev/lightspark-sdk": "1.9.0", - "@uma-sdk/core": "^1.2.3", + "@uma-sdk/core": "^1.3.0", "express": "^4.18.2", + "express-async-handler": "^1.2.0", "uuid": "^9.0.0", "zod": "^3.22.4" }, diff --git a/apps/examples/uma-vasp/src/ReceivingVasp.ts b/apps/examples/uma-vasp/src/ReceivingVasp.ts index 92b0c4d0d..bc9d06b03 100644 --- a/apps/examples/uma-vasp/src/ReceivingVasp.ts +++ b/apps/examples/uma-vasp/src/ReceivingVasp.ts @@ -5,6 +5,7 @@ import { } from "@lightsparkdev/lightspark-sdk"; import * as uma from "@uma-sdk/core"; import { Express } from "express"; +import asyncHandler from "express-async-handler"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import ComplianceService from "./ComplianceService.js"; @@ -32,66 +33,81 @@ export default class ReceivingVasp { ) {} registerRoutes(app: Express): void { - app.get("/.well-known/lnurlp/:username", async (req, resp) => { - const response = await this.handleLnurlpRequest( - req.params.username, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); - - app.get("/api/lnurl/payreq/:uuid", async (req, resp) => { - const response = await this.handleLnurlPayreq( - req.params.uuid, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); - - app.post("/api/uma/payreq/:uuid", async (req, resp) => { - const response = await this.handleUmaPayreq( - req.params.uuid, - fullUrlForRequest(req), - req.body, - ); - sendResponse(resp, response); - }); - - app.post("/api/uma/create_invoice", async (req, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handleUmaCreateInvoice( - user, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); - - app.post("/api/uma/create_and_send_invoice", async (req, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handleUmaCreateAndSendInvoice( - user, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); + app.get( + "/.well-known/lnurlp/:username", + asyncHandler(async (req, resp) => { + const response = await this.handleLnurlpRequest( + req.params.username, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); + + app.get( + "/api/lnurl/payreq/:uuid", + asyncHandler(async (req, resp) => { + const response = await this.handleLnurlPayreq( + req.params.uuid, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); + + app.post( + "/api/uma/payreq/:uuid", + asyncHandler(async (req, resp) => { + const response = await this.handleUmaPayreq( + req.params.uuid, + fullUrlForRequest(req), + req.body, + ); + sendResponse(resp, response); + }), + ); + + app.post( + "/api/uma/create_invoice", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "User not found.", + uma.ErrorCode.USER_NOT_FOUND, + ); + } + const response = await this.handleUmaCreateInvoice( + user, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); + + app.post( + "/api/uma/create_and_send_invoice", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "User not found.", + uma.ErrorCode.USER_NOT_FOUND, + ); + } + const response = await this.handleUmaCreateAndSendInvoice( + user, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); } private async handleLnurlpRequest( @@ -100,7 +116,7 @@ export default class ReceivingVasp { ): Promise { const user = await this.userService.getUserByUma(username); if (!user) { - return { httpStatus: 404, data: "User not found." }; + throw new uma.UmaError("User not found.", uma.ErrorCode.USER_NOT_FOUND); } let lnurlpRequest: uma.LnurlpRequest; @@ -109,18 +125,12 @@ export default class ReceivingVasp { } catch (e: any) { if (e instanceof uma.UnsupportedVersionError) { // For unsupported versions, return a 412 "Precondition Failed" as per the spec. - return { - httpStatus: 412, - data: { - supportedMajorVersions: e.supportedMajorVersions, - unsupportedVersion: e.unsupportedVersion, - }, - }; + throw e; } - return { - httpStatus: 500, - data: new Error("Invalid lnurlp Query", { cause: e }), - }; + throw new uma.UmaError( + "Invalid lnurlp Query", + uma.ErrorCode.PARSE_LNURLP_REQUEST_ERROR, + ); } if (uma.isLnurlpRequestForUma(lnurlpRequest)) { @@ -148,10 +158,10 @@ export default class ReceivingVasp { user: User, ): Promise { if (!uma.isLnurlpRequestForUma(umaQuery)) { - return { - httpStatus: 400, - data: "Invalid UMA query.", - }; + throw new uma.UmaError( + "Invalid UMA query.", + uma.ErrorCode.INTERNAL_ERROR, + ); } if ( !this.complianceService.shouldAcceptTransactionFromVasp( @@ -159,10 +169,10 @@ export default class ReceivingVasp { umaQuery.receiverAddress, ) ) { - return { - httpStatus: 403, - data: "This user is not allowed to transact with this VASP.", - }; + throw new uma.UmaError( + "This user is not allowed to transact with this VASP.", + uma.ErrorCode.COUNTERPARTY_NOT_ALLOWED, + ); } let pubKeys: uma.PubKeyResponse; @@ -173,10 +183,10 @@ export default class ReceivingVasp { }); } catch (e) { console.error(e); - return { - httpStatus: 500, - data: new Error("Failed to fetch public key.", { cause: e }), - }; + throw new uma.UmaError( + "Failed to fetch public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); } try { @@ -186,26 +196,29 @@ export default class ReceivingVasp { this.nonceCache, ); if (!isSignatureValid) { - return { - httpStatus: 500, - data: "Invalid UMA query signature.", - }; + throw new uma.UmaError( + "Invalid UMA query signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } } catch (e) { - return { - httpStatus: 500, - data: new Error("Invalid UMA query signature.", { cause: e }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Invalid UMA query signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } const currencyPrefs = await this.userService.getCurrencyPreferencesForUser( user.id, ); if (!currencyPrefs) { - return { - httpStatus: 500, - data: "Failed to fetch currency preferences.", - }; + throw new uma.UmaError( + "Failed to fetch currency preferences.", + uma.ErrorCode.INTERNAL_ERROR, + ); } const [minSendableSats, maxSendableSats] = @@ -227,10 +240,13 @@ export default class ReceivingVasp { return { httpStatus: 200, data: response.toJsonSchemaObject() }; } catch (e) { console.error(e); - return { - httpStatus: 500, - data: new Error("Failed to generate UMA response.", { cause: e }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Failed to generate UMA response.", + uma.ErrorCode.INTERNAL_ERROR, + ); } } @@ -241,7 +257,7 @@ export default class ReceivingVasp { ): Promise { const user = await this.userService.getUserById(userId); if (!user) { - return { httpStatus: 404, data: "User not found." }; + throw new uma.UmaError("User not found.", uma.ErrorCode.USER_NOT_FOUND); } let payreq: uma.PayRequest; @@ -250,20 +266,26 @@ export default class ReceivingVasp { payreq = uma.PayRequest.fromJson(requestBody); } catch (e) { console.error("Failed to parse pay req", e); - return { - httpStatus: 500, - data: new Error("Invalid UMA pay request.", { cause: e }), - }; + throw new uma.UmaError( + "Invalid UMA pay request.", + uma.ErrorCode.PARSE_PAYREQ_REQUEST_ERROR, + ); } console.log( `Parsed payreq: ${JSON.stringify(payreq.toJsonSchemaObject(), null, 2)}`, ); if (!payreq.isUma()) { - return { httpStatus: 400, data: "Invalid UMA payreq." }; + throw new uma.UmaError( + "Invalid UMA pay request.", + uma.ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, + ); } if (!payreq.payerData!.identifier) { - return { httpStatus: 400, data: "Payer identifier is missing" }; + throw new uma.UmaError( + "Payer identifier is missing.", + uma.ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, + ); } let pubKeys: uma.PubKeyResponse; @@ -276,10 +298,10 @@ export default class ReceivingVasp { }); } catch (e) { console.error(e); - return { - httpStatus: 500, - data: new Error("Failed to fetch public key.", { cause: e }), - }; + throw new uma.UmaError( + "Failed to fetch public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); } console.log(`Fetched pubkeys: ${JSON.stringify(pubKeys, null, 2)}`); @@ -291,14 +313,20 @@ export default class ReceivingVasp { this.nonceCache, ); if (!isSignatureValid) { - return { httpStatus: 400, data: "Invalid payreq signature." }; + throw new uma.UmaError( + "Invalid payreq signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } } catch (e) { console.error(e); - return { - httpStatus: 500, - data: new Error("Invalid payreq signature.", { cause: e }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Invalid payreq signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } console.log(`Verified payreq signature.`); @@ -315,25 +343,28 @@ export default class ReceivingVasp { ); if (!currencyPrefs) { console.error("Failed to fetch currency preferences."); - return { - httpStatus: 500, - data: "Failed to fetch currency preferences.", - }; + throw new uma.UmaError( + "Failed to fetch currency preferences.", + uma.ErrorCode.INTERNAL_ERROR, + ); } let receivingCurrency = currencyPrefs.find( (c) => c.code === payreq.receivingCurrencyCode, ); if (payreq.receivingCurrencyCode && !receivingCurrency) { console.error(`Invalid currency: ${payreq.receivingCurrencyCode}`); - return { - httpStatus: 400, - data: `Invalid currency. This user does not accept ${payreq.receivingCurrencyCode}.`, - }; + throw new uma.UmaError( + `Invalid currency. This user does not accept ${payreq.receivingCurrencyCode}.`, + uma.ErrorCode.INVALID_CURRENCY, + ); } else if (!payreq.receivingCurrencyCode) { receivingCurrency = SATS_CURRENCY; } else if (!receivingCurrency) { // This can't actually happen, but TypeScript doesn't know that. - return { httpStatus: 400, data: "Invalid currency." }; + throw new uma.UmaError( + "Invalid currency.", + uma.ErrorCode.INVALID_CURRENCY, + ); } const isSendingAmountMsats = !payreq.sendingAmountCurrencyCode; @@ -345,12 +376,11 @@ export default class ReceivingVasp { : payreq.amount >= receivingCurrency.minSendable && payreq.amount <= receivingCurrency.maxSendable; if (!isCurrencyAmountInBounds) { - return { - httpStatus: 400, - data: - `Invalid amount. This user only accepts between ${receivingCurrency.minSendable} ` + + throw new uma.UmaError( + `Invalid amount. This user only accepts between ${receivingCurrency.minSendable} ` + `and ${receivingCurrency.maxSendable} ${receivingCurrency.code}.`, - }; + uma.ErrorCode.AMOUNT_OUT_OF_RANGE, + ); } // TODO(Jeremy): Move this to the currency service. @@ -359,10 +389,10 @@ export default class ReceivingVasp { payreq.sendingAmountCurrencyCode !== "SAT" && payreq.sendingAmountCurrencyCode !== receivingCurrency.code ) { - return { - httpStatus: 400, - data: `Invalid sending currency. Cannot convert from ${payreq.sendingAmountCurrencyCode}.`, - }; + throw new uma.UmaError( + `Invalid sending currency. Cannot convert from ${payreq.sendingAmountCurrencyCode}.`, + uma.ErrorCode.INVALID_CURRENCY, + ); } const receiverFeesMillisats = 0; const amountMsats = isSendingAmountMsats @@ -378,10 +408,10 @@ export default class ReceivingVasp { payreq.payerData.compliance?.utxos ?? [], ); if (!shouldTransact) { - return { - httpStatus: 403, - data: "This transaction is too risky.", - }; + throw new uma.UmaError( + "This transaction is too risky.", + uma.ErrorCode.COUNTERPARTY_NOT_ALLOWED, + ); } } @@ -449,10 +479,13 @@ export default class ReceivingVasp { } catch (e) { console.log(`Failed to generate UMA response: ${e}`); console.error(e); - return { - httpStatus: 500, - data: new Error("Failed to generate UMA response.", { cause: e }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Failed to generate UMA response.", + uma.ErrorCode.INTERNAL_ERROR, + ); } } @@ -466,12 +499,18 @@ export default class ReceivingVasp { private async handleUmaCreateAndSendInvoice(user: User, requestUrl: URL) { const senderUma = requestUrl.searchParams.get("senderUma"); if (!senderUma) { - return { httpStatus: 422, data: "missing parameter senderUma" }; + throw new uma.UmaError( + "missing parameter senderUma", + uma.ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, + ); } const { httpStatus, data: bech32EncodedInvoice } = await this.parseAndEncodeUmaInvoice(user, requestUrl, senderUma); if (httpStatus !== 200) { - return { httpStatus, data: bech32EncodedInvoice }; + throw new uma.UmaError( + `Failed to parse and encode UMA invoice: ${bech32EncodedInvoice}`, + uma.ErrorCode.INVALID_INVOICE, + ); } const [, senderDomain] = senderUma.split("@"); @@ -485,7 +524,10 @@ export default class ReceivingVasp { ); } catch (e) { console.error("Error fetching UMA configuration:", e); - return { httpStatus: 500, data: "Error fetching UMA configuration." }; + throw new uma.UmaError( + "Error fetching UMA configuration.", + uma.ErrorCode.INTERNAL_ERROR, + ); } const senderUrl = new URL(umaRequestEndpoint); @@ -499,14 +541,17 @@ export default class ReceivingVasp { body: JSON.stringify({ invoice: bech32EncodedInvoice }), }); } catch (e) { - return { httpStatus: 500, data: "Error sending payreq." }; + throw new uma.UmaError( + "Error sending payreq.", + uma.ErrorCode.PAYREQ_REQUEST_FAILED, + ); } if (response.status !== 200) { - return { - httpStatus: 200, - data: `Error: request pay invoice failed: ${response.statusText}`, - }; + throw new uma.UmaError( + `Error sending payreq: ${response.statusText}`, + uma.ErrorCode.PAYREQ_REQUEST_FAILED, + ); } return { @@ -525,32 +570,32 @@ export default class ReceivingVasp { user.id, ); if (!currencyPrefs) { - return { - httpStatus: 500, - data: "Failed to fetch currency preferences.", - }; + throw new uma.UmaError( + "Failed to fetch currency preferences.", + uma.ErrorCode.INTERNAL_ERROR, + ); } let receivingCurrency = currencyPrefs.find((c) => c.code === currencyCode); if (currencyCode && !receivingCurrency) { console.error(`Invalid currency: ${currencyCode}`); - return { - httpStatus: 400, - data: `Invalid currency. This user does not accept ${currencyCode}.`, - }; + throw new uma.UmaError( + `Invalid currency. This user does not accept ${currencyCode}.`, + uma.ErrorCode.INVALID_CURRENCY, + ); } else if (!currencyCode) { - return { - httpStatus: 422, - data: `Invalid target currency code: ${currencyCode}`, - }; + throw new uma.UmaError( + `Invalid target currency code: ${currencyCode}`, + uma.ErrorCode.INVALID_CURRENCY, + ); } const amountResult = z.coerce .number() .safeParse(requestUrl.searchParams.get("amount")); if (!amountResult.success) { - return { - httpStatus: 400, - data: "Invalid amount parameter.", - }; + throw new uma.UmaError( + "Invalid amount parameter.", + uma.ErrorCode.INVALID_INPUT, + ); } const amount = amountResult.data; const { code, minSendable, maxSendable } = @@ -563,12 +608,11 @@ export default class ReceivingVasp { ? amount >= minSendableSats && amount / 1000 <= maxSendableSats : amount >= minSendable && amount <= maxSendable; if (!isCurrencyAmountInBounds) { - return { - httpStatus: 400, - data: - `Invalid amount. This user only accepts between ${minSendable} ` + + throw new uma.UmaError( + `Invalid amount. This user only accepts between ${minSendable} ` + `and ${maxSendable} ${code}.`, - }; + uma.ErrorCode.AMOUNT_OUT_OF_RANGE, + ); } const umaDomain = hostNameWithPort(requestUrl); @@ -591,7 +635,7 @@ export default class ReceivingVasp { ): Promise { const protocol = isDomainLocalhost(fullUrl.hostname) ? "http" : "https"; const umaConfigResponse = await fetch( - `http://${domain}/.well-known/uma-configuration`, + `${protocol}://${domain}/.well-known/uma-configuration`, ); if (!umaConfigResponse.ok) { throw new Error(`HTTP error! status: ${umaConfigResponse.status}`); @@ -651,17 +695,17 @@ export default class ReceivingVasp { ): Promise { const user = await this.userService.getUserById(userId); if (!user) { - return { httpStatus: 404, data: "User not found." }; + throw new uma.UmaError("User not found.", uma.ErrorCode.USER_NOT_FOUND); } let request: uma.PayRequest; try { request = uma.PayRequest.fromUrlSearchParams(requestUrl.searchParams); } catch (e) { - return { - httpStatus: 400, - data: "Invalid pay request: " + e, - }; + throw new uma.UmaError( + "Invalid pay request: " + e, + uma.ErrorCode.PARSE_PAYREQ_REQUEST_ERROR, + ); } return this.handlePayReq(request, user, requestUrl); @@ -690,13 +734,22 @@ export default class ReceivingVasp { try { node = await this.lightsparkClient.executeRawQuery(nodeQuery); } catch (e) { - throw new Error(`Failed to fetch node ${this.config.nodeID}.`); + throw new uma.UmaError( + `Failed to fetch node ${this.config.nodeID}.`, + uma.ErrorCode.INTERNAL_ERROR, + ); } if (!node) { - throw new Error(`Node ${this.config.nodeID} not found.`); + throw new uma.UmaError( + `Node ${this.config.nodeID} not found.`, + uma.ErrorCode.INTERNAL_ERROR, + ); } if (!node.publicKey) { - throw new Error(`Node ${this.config.nodeID} has no known public key.`); + throw new uma.UmaError( + `Node ${this.config.nodeID} has no known public key.`, + uma.ErrorCode.INTERNAL_ERROR, + ); } return node.publicKey; } diff --git a/apps/examples/uma-vasp/src/SendingVasp.ts b/apps/examples/uma-vasp/src/SendingVasp.ts index f50c6ac94..3e1cfff20 100644 --- a/apps/examples/uma-vasp/src/SendingVasp.ts +++ b/apps/examples/uma-vasp/src/SendingVasp.ts @@ -9,7 +9,8 @@ import { TransactionStatus, } from "@lightsparkdev/lightspark-sdk"; import * as uma from "@uma-sdk/core"; -import { Express, Request } from "express"; +import { Express } from "express"; +import asyncHandler from "express-async-handler"; import ComplianceService from "./ComplianceService.js"; import InternalLedgerService from "./InternalLedgerService.js"; import SendingVaspRequestCache, { @@ -44,120 +45,137 @@ export default class SendingVasp { private readonly nonceCache: uma.NonceValidator, ) {} - registerRoutes(app: Express) { - app.get("/api/umalookup/:receiver", async (req: Request, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handleClientUmaLookup( - user, - req.params.receiver, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); - - app.get("/api/umapayreq/:callbackUuid", async (req: Request, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handleClientUmaPayreq( - user, - req.params.callbackUuid, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); + registerRoutes(app: Express): void { + app.get( + "/api/umalookup/:receiver", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "Unauthorized. Check your credentials.", + uma.ErrorCode.FORBIDDEN, + ); + } + const response = await this.handleClientUmaLookup( + user, + req.params.receiver, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); - app.post("/api/sendpayment/:callbackUuid", async (req, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handleClientSendPayment( - user, - req.params.callbackUuid, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); + app.get( + "/api/umapayreq/:callbackUuid", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "Unauthorized. Check your credentials.", + uma.ErrorCode.FORBIDDEN, + ); + } + const response = await this.handleClientUmaPayreq( + user, + req.params.callbackUuid, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); - app.post("/api/uma/pay_invoice", async (req, resp) => { - const user = await this.userService.getCallingUserFromRequest( - fullUrlForRequest(req), - req.headers, - ); - if (!user) { - return sendResponse(resp, { - httpStatus: 401, - data: "Unauthorized. Check your credentials.", - }); - } - const response = await this.handlePayInvoice( - user, - fullUrlForRequest(req), - ); - sendResponse(resp, response); - }); + app.post( + "/api/sendpayment/:callbackUuid", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "Unauthorized. Check your credentials.", + uma.ErrorCode.FORBIDDEN, + ); + } + const response = await this.handleClientSendPayment( + user, + req.params.callbackUuid, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); - app.post("/api/uma/request_invoice_payment", async (req, resp) => { - let invoiceBech32Str; - try { - invoiceBech32Str = JSON.parse(req.body)["invoice"]; - } catch (e) { - return sendResponse(resp, { - httpStatus: 500, - data: "Error. unable to parse uma invoice .", - }); - } - if (!invoiceBech32Str || typeof invoiceBech32Str !== "string") { - return sendResponse(resp, { - httpStatus: 401, - data: "Error. Required invoice not provided.", - }); - } - const invoice = uma.InvoiceSerializer.fromBech32(invoiceBech32Str); - if (!invoice.senderUma) { - return sendResponse(resp, { - httpStatus: 401, - data: "Error. Sender Uma not present on invoice.", - }); - } + app.post( + "/api/uma/pay_invoice", + asyncHandler(async (req, resp) => { + const user = await this.userService.getCallingUserFromRequest( + fullUrlForRequest(req), + req.headers, + ); + if (!user) { + throw new uma.UmaError( + "Unauthorized. Check your credentials.", + uma.ErrorCode.FORBIDDEN, + ); + } + const response = await this.handlePayInvoice( + user, + fullUrlForRequest(req), + ); + sendResponse(resp, response); + }), + ); - const response = await this.handleRequestPayInvoice( - invoice, - invoiceBech32Str, - ); - sendResponse(resp, response); - }); + app.post( + "/api/uma/request_invoice_payment", + asyncHandler(async (req, resp) => { + let invoiceBech32Str; + try { + invoiceBech32Str = JSON.parse(req.body)["invoice"]; + } catch (e) { + throw new uma.UmaError( + "Error. unable to parse uma invoice .", + uma.ErrorCode.INVALID_INPUT, + ); + } + if (!invoiceBech32Str || typeof invoiceBech32Str !== "string") { + throw new uma.UmaError( + "Error. Required invoice not provided.", + uma.ErrorCode.INVALID_INPUT, + ); + } + const invoice = uma.InvoiceSerializer.fromBech32(invoiceBech32Str); + if (!invoice.senderUma) { + throw new uma.UmaError( + "Error. Sender Uma not present on invoice.", + uma.ErrorCode.INVALID_INPUT, + ); + } + const response = await this.handleRequestPayInvoice( + invoice, + invoiceBech32Str, + ); + sendResponse(resp, response); + }), + ); - app.get("/api/uma/pending_requests", async (req, resp) => { - const pendingRequests = this.requestCache.getPendingPayReqs(); - sendResponse(resp, { - httpStatus: 200, - data: pendingRequests, - }); - }); + app.get( + "/api/uma/pending_requests", + asyncHandler(async (req, resp) => { + const pendingRequests = this.requestCache.getPendingPayReqs(); + sendResponse(resp, { + httpStatus: 200, + data: pendingRequests, + }); + }), + ); } private async handleClientUmaLookup( @@ -166,13 +184,13 @@ export default class SendingVasp { requestUrl: URL, ): Promise { if (!receiverUmaAddress) { - return { httpStatus: 400, data: "Missing receiver" }; + throw new uma.UmaError("Missing receiver", uma.ErrorCode.INVALID_INPUT); } const [receiverId, receivingVaspDomain] = receiverUmaAddress.split("@"); if (!receiverId || !receivingVaspDomain) { console.error(`Invalid receiver: ${receiverUmaAddress}`); - return { httpStatus: 400, data: "Invalid receiver" }; + throw new uma.UmaError("Invalid receiver", uma.ErrorCode.INVALID_INPUT); } if ( @@ -182,10 +200,10 @@ export default class SendingVasp { receiverUmaAddress, ) ) { - return { - httpStatus: 400, - data: `Transaction not allowed to ${receiverUmaAddress}.`, - }; + throw new uma.UmaError( + `Transaction not allowed to ${receiverUmaAddress}.`, + uma.ErrorCode.COUNTERPARTY_NOT_ALLOWED, + ); } let lnurlpRequestUrl: URL; @@ -210,7 +228,10 @@ export default class SendingVasp { response = await fetch(lnurlpRequestUrl); } catch (e) { console.error("Error fetching Lnurlp request.", e); - return { httpStatus: 424, data: "Error fetching Lnurlp request." }; + throw new uma.UmaError( + "Error fetching Lnurlp request.", + uma.ErrorCode.INTERNAL_ERROR, + ); } if (response.status === 412) { @@ -222,18 +243,18 @@ export default class SendingVasp { ); } catch (e) { console.error("Error fetching Lnurlp request.", e); - return { - httpStatus: 424, - data: new Error("Error fetching Lnurlp request.", { cause: e }), - }; + throw new uma.UmaError( + "Error fetching Lnurlp request.", + uma.ErrorCode.INTERNAL_ERROR, + ); } } if (!response.ok) { - return { - httpStatus: 424, - data: `Error fetching Lnurlp request. ${response.status}`, - }; + throw new uma.UmaError( + `Error fetching Lnurlp request. ${response.status}`, + uma.ErrorCode.LNURLP_REQUEST_FAILED, + ); } let lnurlpResponse: uma.LnurlpResponse; @@ -242,7 +263,10 @@ export default class SendingVasp { lnurlpResponse = uma.LnurlpResponse.fromJson(responseJson); } catch (e) { console.error("Error parsing lnurlp response.", e, responseJson); - return { httpStatus: 424, data: `Error parsing Lnurlp response. ${e}` }; + throw new uma.UmaError( + `Error parsing Lnurlp response. ${e}`, + uma.ErrorCode.PARSE_LNURLP_RESPONSE_ERROR, + ); } if (!lnurlpResponse.isUma()) { @@ -256,10 +280,10 @@ export default class SendingVasp { let pubKeys = await this.fetchPubKeys(receivingVaspDomain); if (!pubKeys) - return { - httpStatus: 424, - data: "Error fetching receiving vasp public key.", - }; + throw new uma.UmaError( + "Error fetching receiving vasp public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); try { const isSignatureValid = await uma.verifyUmaLnurlpResponseSignature( @@ -268,16 +292,20 @@ export default class SendingVasp { this.nonceCache, ); if (!isSignatureValid) { - return { httpStatus: 424, data: "Invalid UMA response signature." }; + throw new uma.UmaError( + "Invalid UMA response signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } } catch (e) { console.error("Error verifying UMA response signature.", e); - return { - httpStatus: 424, - data: new Error("Error verifying UMA response signature.", { - cause: e, - }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Error verifying UMA response signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } const callbackUuid = this.requestCache.saveLnurlpResponseData( @@ -351,26 +379,35 @@ export default class SendingVasp { requestUrl: URL, ): Promise { if (!callbackUuid || callbackUuid === "") { - return { httpStatus: 400, data: "Missing callbackUuid" }; + throw new uma.UmaError( + "Missing callbackUuid", + uma.ErrorCode.INVALID_INPUT, + ); } const initialRequestData = this.requestCache.getLnurlpResponseData(callbackUuid); if (!initialRequestData) { - return { httpStatus: 400, data: "callbackUuid not found" }; + throw new uma.UmaError( + "callbackUuid not found", + uma.ErrorCode.REQUEST_NOT_FOUND, + ); } const amountStr = requestUrl.searchParams.get("amount"); if (!amountStr || typeof amountStr !== "string") { - return { httpStatus: 400, data: "Missing amount" }; + throw new uma.UmaError("Missing amount", uma.ErrorCode.INVALID_INPUT); } const amount = parseFloat(amountStr); if (isNaN(amount)) { - return { httpStatus: 400, data: "Invalid amount" }; + throw new uma.UmaError("Invalid amount", uma.ErrorCode.INVALID_INPUT); } if (!initialRequestData.lnurlpResponse) { - return { httpStatus: 400, data: "Invalid callbackUuid" }; + throw new uma.UmaError( + "Invalid callbackUuid", + uma.ErrorCode.INVALID_INPUT, + ); } const receivingCurrencyCode = requestUrl.searchParams.get( @@ -401,18 +438,27 @@ export default class SendingVasp { } if (!payerProfile) { - return { httpStatus: 400, data: "Missing payerData" }; + throw new uma.UmaError( + "Missing payerData", + uma.ErrorCode.REQUEST_NOT_FOUND, + ); } if (!receivingCurrencyCode || typeof receivingCurrencyCode !== "string") { - return { httpStatus: 400, data: "Missing currencyCode" }; + throw new uma.UmaError( + "Missing currencyCode", + uma.ErrorCode.INVALID_INPUT, + ); } const selectedCurrency = ( initialRequestData.lnurlpResponse.currencies || [] ).find((c) => c.code === receivingCurrencyCode); if (selectedCurrency === undefined) { - return { httpStatus: 400, data: "Currency code not supported" }; + throw new uma.UmaError( + "Currency code not supported", + uma.ErrorCode.INVALID_CURRENCY, + ); } const msatsParam = requestUrl.searchParams.get("isAmountInMsats"); @@ -426,7 +472,10 @@ export default class SendingVasp { await this.userService.getCurrencyPreferencesForUser(user.id) )?.find((c) => c.code === sendingCurrencyCode); if (sendingCurrency === undefined) { - return { httpStatus: 400, data: "Sending currency code not supported" }; + throw new uma.UmaError( + "Sending currency code not supported", + uma.ErrorCode.INVALID_CURRENCY, + ); } const sendingCurrencyAmount = amountValueMillisats / sendingCurrency.multiplier; @@ -439,17 +488,20 @@ export default class SendingVasp { sendingCurrencyCode, ) ) { - return { httpStatus: 400, data: "Insufficient balance." }; + throw new uma.UmaError( + "Insufficient balance.", + uma.ErrorCode.INTERNAL_ERROR, + ); } let pubKeys = await this.fetchPubKeys( initialRequestData.receivingVaspDomain, ); if (!pubKeys) { - return { - httpStatus: 424, - data: "Error fetching receiving vasp public key.", - }; + throw new uma.UmaError( + "Error fetching receiving vasp public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); } const umaVersion = @@ -541,7 +593,13 @@ export default class SendingVasp { }); } catch (e) { console.error("Error generating payreq.", e); - return { httpStatus: 500, data: "Error generating payreq." }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Error generating payreq.", + uma.ErrorCode.INTERNAL_ERROR, + ); } console.log( @@ -557,14 +615,20 @@ export default class SendingVasp { body: payReq.toJsonString(), }); } catch (e) { - return { httpStatus: 500, data: "Error sending payreq." }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Error sending payreq.", + uma.ErrorCode.INTERNAL_ERROR, + ); } if (!response.ok || response.status !== 200) { - return { - httpStatus: 424, - data: `Payreq failed. ${response.status}, ${response.body}`, - }; + throw new uma.UmaError( + `Payreq failed. ${response.status}, ${response.body}`, + uma.ErrorCode.PAYREQ_REQUEST_FAILED, + ); } let payResponse: uma.PayReqResponse; @@ -574,15 +638,18 @@ export default class SendingVasp { } catch (e) { console.error("Error parsing payreq response. Raw response: " + bodyText); console.error("Error:", e); - return { httpStatus: 424, data: "Error parsing payreq response." }; + throw new uma.UmaError( + "Error parsing payreq response.", + uma.ErrorCode.PARSE_PAYREQ_RESPONSE_ERROR, + ); } if (!payResponse.isUma()) { console.log("Received non-uma response for uma payreq."); - return { - httpStatus: 424, - data: "Received non-uma response for uma payreq.", - }; + throw new uma.UmaError( + "Received non-uma response for uma payreq.", + uma.ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, + ); } try { @@ -595,18 +662,21 @@ export default class SendingVasp { this.nonceCache, ); if (!isSignatureValid) { - return { - httpStatus: 424, - data: "Invalid payreq response signature.", - }; + throw new uma.UmaError( + "Invalid payreq response signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } } } catch (e) { console.error(e); - return { - httpStatus: 424, - data: new Error("Invalid payreq response signature.", { cause: e }), - }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Invalid payreq response signature.", + uma.ErrorCode.INVALID_SIGNATURE, + ); } console.log(`Verified payreq response signature.`); @@ -619,10 +689,10 @@ export default class SendingVasp { payResponse.payeeData?.compliance?.utxos ?? [], ); if (!shouldTransact) { - return { - httpStatus: 424, - data: "Transaction not allowed due to risk rating.", - }; + throw new uma.UmaError( + "Transaction not allowed due to risk rating.", + uma.ErrorCode.COUNTERPARTY_NOT_ALLOWED, + ); } let invoice: InvoiceData; @@ -630,7 +700,10 @@ export default class SendingVasp { invoice = await this.lightsparkClient.decodeInvoice(payResponse.pr); } catch (e) { console.error("Error decoding invoice.", e); - return { httpStatus: 500, data: "Error decoding invoice." }; + throw new uma.UmaError( + "Error decoding invoice.", + uma.ErrorCode.INTERNAL_ERROR, + ); } const senderCurrencies = @@ -697,28 +770,37 @@ export default class SendingVasp { }); } catch (e) { console.error("Error sending payreq.", e); - return { httpStatus: 500, data: "Error sending payreq." }; + throw new uma.UmaError( + "Error sending payreq.", + uma.ErrorCode.INTERNAL_ERROR, + ); } const responseText = await response.text(); if (!response.ok) { - return { httpStatus: 424, data: `Payreq failed. ${response.status}` }; + throw new uma.UmaError( + `Payreq failed. ${response.status}`, + uma.ErrorCode.PAYREQ_REQUEST_FAILED, + ); } const responseJson = JSON.parse(responseText); if (responseJson.status === "ERROR") { console.error("Error on pay request.", responseJson.reason); - return { - httpStatus: 424, - data: `Error on pay request. reason: ${responseJson.reason}`, - }; + throw new uma.UmaError( + `Error on pay request. reason: ${responseJson.reason} code: ${responseJson.code}`, + uma.ErrorCode.PAYREQ_REQUEST_FAILED, + ); } let payreqResponse: uma.PayReqResponse; try { payreqResponse = uma.PayReqResponse.fromJson(responseText); } catch (e) { console.error("Error parsing payreq response.", e); - return { httpStatus: 424, data: "Error parsing payreq response." }; + throw new uma.UmaError( + "Error parsing payreq response.", + uma.ErrorCode.PARSE_PAYREQ_RESPONSE_ERROR, + ); } const encodedInvoice = payreqResponse.pr; @@ -728,7 +810,10 @@ export default class SendingVasp { invoice = await this.lightsparkClient.decodeInvoice(encodedInvoice); } catch (e) { console.error("Error decoding invoice.", e); - return { httpStatus: 500, data: "Error decoding invoice." }; + throw new uma.UmaError( + "Error decoding invoice.", + uma.ErrorCode.INVALID_INVOICE, + ); } const newCallbackUuid = this.requestCache.savePayReqData( @@ -758,7 +843,7 @@ export default class SendingVasp { getLightsparkNodeQuery(this.config.nodeID), ); if (!node) { - throw new Error("Node not found."); + throw new uma.UmaError("Node not found.", uma.ErrorCode.INTERNAL_ERROR); } return node; @@ -772,7 +857,13 @@ export default class SendingVasp { }); } catch (e) { console.error("Error fetching public key.", e); - return null; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Error fetching public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); } } @@ -782,23 +873,29 @@ export default class SendingVasp { requestUrl: URL, ): Promise { if (!callbackUuid || callbackUuid === "") { - return { httpStatus: 400, data: "Missing callbackUuid" }; + throw new uma.UmaError( + "Missing callbackUuid", + uma.ErrorCode.INVALID_INPUT, + ); } const payReqData = this.requestCache.getPayReqData(callbackUuid); if (!payReqData || !payReqData?.invoiceData) { - return { httpStatus: 400, data: "callbackUuid not found" }; + throw new uma.UmaError( + "Could not find pay request associated with uuid", + uma.ErrorCode.REQUEST_NOT_FOUND, + ); } if (new Date(payReqData.invoiceData.expiresAt) < new Date()) { - return { httpStatus: 400, data: "Invoice expired" }; + throw new uma.UmaError("Invoice expired", uma.ErrorCode.INVOICE_EXPIRED); } if (payReqData.invoiceData.amount.originalValue <= 0) { - return { - httpStatus: 400, - data: "Invalid invoice amount. Positive amount required.", - }; + throw new uma.UmaError( + "Invalid invoice amount. Positive amount required.", + uma.ErrorCode.INVALID_INPUT, + ); } const sendingCurrencyCode = @@ -807,7 +904,10 @@ export default class SendingVasp { await this.userService.getCurrencyPreferencesForUser(user.id) )?.find((c) => c.code === sendingCurrencyCode); if (sendingCurrency === undefined) { - return { httpStatus: 400, data: "Sending currency code not supported" }; + throw new uma.UmaError( + "Sending currency code not supported", + uma.ErrorCode.INVALID_CURRENCY, + ); } const amountMsats = convertCurrencyAmount( @@ -817,16 +917,16 @@ export default class SendingVasp { const sendingCurrencyAmount = amountMsats / sendingCurrency.multiplier; if (sendingCurrencyAmount < sendingCurrency.minSendable) { - return { - httpStatus: 400, - data: `Invalid invoice amount. Minimum amount is ${sendingCurrency.minSendable} ${sendingCurrency.code}.`, - }; + throw new uma.UmaError( + `Invalid invoice amount. Minimum amount is ${sendingCurrency.minSendable} ${sendingCurrency.code}.`, + uma.ErrorCode.AMOUNT_OUT_OF_RANGE, + ); } if (sendingCurrencyAmount > sendingCurrency.maxSendable) { - return { - httpStatus: 400, - data: `Invalid invoice amount. Maximum amount is ${sendingCurrency.maxSendable} ${sendingCurrency.code}.`, - }; + throw new uma.UmaError( + `Invalid invoice amount. Maximum amount is ${sendingCurrency.maxSendable} ${sendingCurrency.code}.`, + uma.ErrorCode.AMOUNT_OUT_OF_RANGE, + ); } if ( @@ -837,7 +937,10 @@ export default class SendingVasp { sendingCurrencyCode, ) ) { - return { httpStatus: 400, data: "Insufficient balance." }; + throw new uma.UmaError( + "Insufficient balance.", + uma.ErrorCode.INTERNAL_ERROR, + ); } let payment: OutgoingPayment; @@ -845,7 +948,10 @@ export default class SendingVasp { try { const signingKeyLoaded = await this.loadNodeSigningKey(); if (!signingKeyLoaded) { - throw new Error("Error loading signing key."); + throw new uma.UmaError( + "Error loading signing key.", + uma.ErrorCode.INTERNAL_ERROR, + ); } const paymentResult = await this.lightsparkClient.payUmaInvoice( this.config.nodeID, @@ -853,7 +959,10 @@ export default class SendingVasp { /* maxFeesMsats */ 1_000_000, ); if (!paymentResult) { - throw new Error("Payment request failed."); + throw new uma.UmaError( + "Payment request failed.", + uma.ErrorCode.INTERNAL_ERROR, + ); } paymentId = paymentResult.id; await this.ledgerService.recordOutgoingTransactionBegan( @@ -885,7 +994,13 @@ export default class SendingVasp { paymentId, ); } - return { httpStatus: 500, data: "Error paying invoice." }; + if (e instanceof uma.UmaError) { + throw e; + } + throw new uma.UmaError( + "Error paying invoice.", + uma.ErrorCode.INTERNAL_ERROR, + ); } await this.sendPostTransactionCallback(payment, payReqData, requestUrl); @@ -912,7 +1027,10 @@ export default class SendingVasp { private async handlePayInvoice(user: User, requestUrl: URL) { const invoiceStr = requestUrl.searchParams.get("invoice"); if (!invoiceStr) { - return { httpStatus: 422, data: "Missing argument: invoice" }; + throw new uma.UmaError( + "Missing argument: invoice", + uma.ErrorCode.INVALID_INPUT, + ); } // payments can also be done via uuid of cached request // by checking uma prefix, determine if invoiceStr is a bech32 encoded invoice. @@ -926,13 +1044,13 @@ export default class SendingVasp { try { invoice = uma.InvoiceSerializer.fromBech32(encodedInvoice ?? invoiceStr); } catch (e) { - return { - httpStatus: 500, - data: "Cannot parse Invoice from invoice paramater", - }; + throw new uma.UmaError( + "Cannot parse Invoice from invoice parameter", + uma.ErrorCode.INVALID_INVOICE, + ); } - let payerPofile = this.getPayerProfile( + let payerProfile = this.getPayerProfile( user, invoice.requiredPayerData ?? {}, this.getSendingVaspDomain(requestUrl), @@ -942,24 +1060,27 @@ export default class SendingVasp { const [, receivingVaspDomain] = invoice.receiverUma.split("@"); let pubKeys = await this.fetchPubKeys(receivingVaspDomain); if (!pubKeys) - return { - httpStatus: 424, - data: "Error fetching receiving vasp public key.", - }; + throw new uma.UmaError( + "Error fetching receiving vasp public key.", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); const signatureVerified = uma.verifyUmaInvoiceSignature( invoice, pubKeys.getSigningPubKey(), ); if (!signatureVerified) { - return { httpStatus: 500, data: "Unable to verify invoice signature" }; + throw new uma.UmaError( + "Unable to verify invoice signature", + uma.ErrorCode.INVALID_SIGNATURE, + ); } if (!isCurrencyType(invoice.receivingCurrency.code)) { - return { - httpStatus: 500, - data: `Invalid currency code ${invoice.receivingCurrency.code}`, - }; + throw new uma.UmaError( + `Invalid currency code ${invoice.receivingCurrency.code}`, + uma.ErrorCode.INVALID_CURRENCY, + ); } const isAmountInMsats = invoice.receivingCurrency.code == SATS_CURRENCY.code; @@ -967,7 +1088,10 @@ export default class SendingVasp { const currencyInMsats = invoice.amount * currency.multiplier; if (invoice.expiration < Date.now()) { - return { httpStatus: 500, data: "Unable to process expired invoice" }; + throw new uma.UmaError( + "Unable to process expired invoice", + uma.ErrorCode.INVOICE_EXPIRED, + ); } return this.handleUmaPayReqInternal({ @@ -982,7 +1106,7 @@ export default class SendingVasp { invoice.senderUma ?? this.formatUma(user.umaUserName, this.getSendingVaspDomain(requestUrl)), requestUrl: requestUrl, - senderProfile: payerPofile, + senderProfile: payerProfile, pubKeys: pubKeys, umaVersion: invoice.umaVersions, invoiceUUID: invoice.invoiceUUID, @@ -1001,20 +1125,20 @@ export default class SendingVasp { let pubKeys = await this.fetchPubKeys(receivingVaspDomain); if (!pubKeys) { - return { - httpStatus: 500, - data: "Error, unable to get receiving Vasp public signing key", - }; + throw new uma.UmaError( + "Error, unable to get receiving Vasp public signing key", + uma.ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); } const verified = uma.verifyUmaInvoiceSignature( invoice, pubKeys.getSigningPubKey(), ); if (!verified) { - return { - httpStatus: 500, - data: "Error, unable to verify uma invoice", - }; + throw new uma.UmaError( + "Error, unable to verify uma invoice", + uma.ErrorCode.INVALID_SIGNATURE, + ); } // save request this.requestCache.savePayReqData( @@ -1085,7 +1209,10 @@ export default class SendingVasp { OutgoingPayment.getOutgoingPaymentQuery(paymentResult.id), ); if (!payment) { - throw new Error("Payment not found."); + throw new uma.UmaError( + "Payment not found.", + uma.ErrorCode.INTERNAL_ERROR, + ); } if (payment.status !== TransactionStatus.PENDING) { @@ -1094,7 +1221,10 @@ export default class SendingVasp { const maxRetries = 40; if (retryNum >= maxRetries) { - throw new Error("Payment timed out."); + throw new uma.UmaError( + "Payment timed out.", + uma.ErrorCode.INTERNAL_ERROR, + ); } await new Promise((resolve) => setTimeout(resolve, 250)); @@ -1147,7 +1277,7 @@ export default class SendingVasp { getLightsparkNodeQuery(this.config.nodeID), ); if (!node) { - throw new Error("Node not found."); + throw new uma.UmaError("Node not found.", uma.ErrorCode.INTERNAL_ERROR); } if (node.typename.includes("OSK")) { @@ -1155,9 +1285,10 @@ export default class SendingVasp { !this.config.oskSigningKeyPassword || this.config.oskSigningKeyPassword === "" ) { - throw new Error( + throw new uma.UmaError( "Node is an OSK, but no signing key password was provided in the config. " + "Set the LIGHTSPARK_UMA_OSK_NODE_SIGNING_KEY_PASSWORD environment variable", + uma.ErrorCode.INTERNAL_ERROR, ); } return await this.lightsparkClient.loadNodeSigningKey( @@ -1171,9 +1302,10 @@ export default class SendingVasp { // Assume remote signing node. const remoteSigningMasterSeed = this.config.remoteSigningMasterSeed(); if (!remoteSigningMasterSeed) { - throw new Error( + throw new uma.UmaError( "Node is a remote signing node, but no master seed was provided in the config. " + "Set the LIGHTSPARK_UMA_REMOTE_SIGNING_NODE_MASTER_SEED environment variable", + uma.ErrorCode.INTERNAL_ERROR, ); } return await this.lightsparkClient.loadNodeSigningKey(this.config.nodeID, { diff --git a/apps/examples/uma-vasp/src/server.ts b/apps/examples/uma-vasp/src/server.ts index c02231227..3d84a306c 100644 --- a/apps/examples/uma-vasp/src/server.ts +++ b/apps/examples/uma-vasp/src/server.ts @@ -1,15 +1,18 @@ import { LightsparkClient } from "@lightsparkdev/lightspark-sdk"; import { + ErrorCode, fetchPublicKeyForVasp, getPubKeyResponse, InMemoryPublicKeyCache, NonceValidator, parsePostTransactionCallback, PubKeyResponse, + UmaError, verifyPostTransactionCallbackSignature, } from "@uma-sdk/core"; import bodyParser from "body-parser"; import express from "express"; +import asyncHandler from "express-async-handler"; import ComplianceService from "./ComplianceService.js"; import InternalLedgerService from "./InternalLedgerService.js"; import ReceivingVasp from "./ReceivingVasp.js"; @@ -18,7 +21,10 @@ import SendingVaspRequestCache from "./SendingVaspRequestCache.js"; import UmaConfig from "./UmaConfig.js"; import UserService from "./UserService.js"; import { errorMessage } from "./errors.js"; -import { fullUrlForRequest } from "./networking/expressAdapters.js"; +import { + fullUrlForRequest, + sendResponse, +} from "./networking/expressAdapters.js"; export const createUmaServer = ( config: UmaConfig, @@ -62,7 +68,7 @@ export const createUmaServer = ( ); receivingVasp.registerRoutes(app); - app.get("/.well-known/lnurlpubkey", (req, res) => { + app.get("/.well-known/lnurlpubkey", (_req, res) => { res.send( getPubKeyResponse({ signingCertChainPem: config.umaSigningCertChain, @@ -81,54 +87,64 @@ export const createUmaServer = ( }); }); - app.post("/api/uma/utxoCallback", async (req, res) => { - const postTransactionCallback = parsePostTransactionCallback(req.body); + app.post( + "/api/uma/utxoCallback", + asyncHandler(async (req, resp) => { + const postTransactionCallback = parsePostTransactionCallback(req.body); - let pubKeys: PubKeyResponse; - try { - pubKeys = await fetchPublicKeyForVasp({ - cache: pubKeyCache, - vaspDomain: postTransactionCallback.vaspDomain, - }); - } catch (e) { - console.error(e); - return { - httpStatus: 500, - data: new Error("Failed to fetch public key.", { cause: e }), - }; - } + let pubKeys: PubKeyResponse; + try { + pubKeys = await fetchPublicKeyForVasp({ + cache: pubKeyCache, + vaspDomain: postTransactionCallback.vaspDomain, + }); + } catch (e) { + console.error(e); + if (e instanceof UmaError) { + throw e; + } + throw new UmaError( + "Failed to fetch public key.", + ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + ); + } - console.log(`Fetched pubkeys: ${JSON.stringify(pubKeys, null, 2)}`); + console.log(`Fetched pubkeys: ${JSON.stringify(pubKeys, null, 2)}`); - try { - const isSignatureValid = await verifyPostTransactionCallbackSignature( - postTransactionCallback, - pubKeys, - nonceCache, - ); - if (!isSignatureValid) { - return { - httpStatus: 400, - data: "Invalid post transaction callback signature.", - }; + try { + const isSignatureValid = await verifyPostTransactionCallbackSignature( + postTransactionCallback, + pubKeys, + nonceCache, + ); + if (!isSignatureValid) { + throw new UmaError( + "Invalid post transaction callback signature.", + ErrorCode.INVALID_SIGNATURE, + ); + } + } catch (e) { + console.error(e); + if (e instanceof UmaError) { + throw e; + } + throw new UmaError( + "Invalid post transaction callback signature.", + ErrorCode.INVALID_SIGNATURE, + ); } - } catch (e) { - console.error(e); - return { - httpStatus: 500, - data: new Error("Invalid post transaction callback signature.", { - cause: e, - }), - }; - } - console.log(`Received UTXO callback for ${req.query.txid}`); - console.log(` ${req.body}`); - res.send("ok"); - }); + console.log(`Received UTXO callback for ${req.query.txid}`); + console.log(` ${req.body}`); + sendResponse(resp, { + httpStatus: 200, + data: "ok", + }); + }), + ); // Default 404 handler. - app.use(function (req, res, next) { + app.use(function (_req, res) { res.status(404); res.send(errorMessage("Not found.")); }); @@ -139,12 +155,16 @@ export const createUmaServer = ( return next(err); } - if (err.message === "User not found.") { - res.status(404).send(errorMessage(err.message)); + if (err instanceof UmaError) { + res.status(err.httpStatusCode).json(JSON.parse(err.toJSON())); return; } - res.status(500).send(errorMessage(`Something broke! ${err.message}`)); + const error = new UmaError( + `Something broke! ${err.message}`, + ErrorCode.INTERNAL_ERROR, + ); + res.status(error.httpStatusCode).json(JSON.parse(error.toJSON())); }); return app; diff --git a/package.json b/package.json index e947806ce..2145fcb88 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build-sb": "turbo run build-sb", "build": "turbo run build", "build:watch": "turbo run build:watch", - "checks": "yarn deps:check && turbo gql-codegen && turbo lint format test circular-deps package-types", + "checks": "yarn deps:check && turbo gql-codegen && turbo run lint format test circular-deps package:checks", "circular-deps": "turbo run circular-deps", "clean": "turbo run clean", "clean-all": "./clean-all.sh", @@ -24,7 +24,7 @@ "gql-codegen": "turbo run gql-codegen", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", - "package-types": "turbo run package-types", + "package:checks": "turbo run package:checks", "release": "turbo build && changeset publish", "start:prod": "VITE_PROXY_TARGET=https://app.lightspark.com yarn start", "start": "./start.sh", @@ -46,7 +46,7 @@ "@octokit/auth-action": "^4.0.1", "octokit": "^4.0.2", "ts-prune": "^0.10.3", - "turbo": "^1.13.3" + "turbo": "^2.4.4" }, "engines": { "node": ">=18" diff --git a/packages/core/package.json b/packages/core/package.json index 68392097f..bf29c54ff 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/lightsparkdev/js-sdk", "repository": { "type": "git", - "url": "https://github.com/lightsparkdev/js-sdk.git" + "url": "git+https://github.com/lightsparkdev/js-sdk.git" }, "bugs": { "url": "https://github.com/lightsparkdev/js-sdk/issues" @@ -38,6 +38,7 @@ "files": [ "src/*", "dist/*", + "dist/utils/*", "CHANGELOG.md" ], "scripts": { @@ -50,7 +51,7 @@ "lint:fix": "eslint --fix .", "lint:watch": "esw ./src -w --ext .ts,.tsx,.js --color", "lint": "eslint .", - "package-types": "yarn attw --pack .", + "package:checks": "yarn publint && yarn attw --pack .", "postversion": "yarn build", "test": "node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --bail src/**/tests/**/*.test.ts", "types:watch": "tsc-absolute --watch", @@ -82,6 +83,7 @@ "lodash-es": "^4.17.21", "prettier": "3.0.3", "prettier-plugin-organize-imports": "^3.2.4", + "publint": "^0.3.9", "ts-jest": "^29.1.1", "tsc-absolute": "^1.0.1", "tsup": "^8.2.4", diff --git a/packages/core/src/requester/Requester.ts b/packages/core/src/requester/Requester.ts index 651f75428..cedabadc2 100644 --- a/packages/core/src/requester/Requester.ts +++ b/packages/core/src/requester/Requester.ts @@ -208,9 +208,22 @@ class Requester { }; const data = responseJson.data; if (!data) { + let firstErrorName: string | undefined = undefined; + if ( + Array.isArray(responseJson.errors) && + responseJson.errors.length > 0 + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const firstError = responseJson.errors[0]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + firstErrorName = firstError["extensions"]?.["error_name"] as + | string + | undefined; + } throw new LightsparkException( "RequestFailed", `Request ${operation} failed. ${JSON.stringify(responseJson.errors)}`, + { errorName: firstErrorName }, ); } return data; diff --git a/packages/core/turbo.json b/packages/core/turbo.json new file mode 100644 index 000000000..0657018cb --- /dev/null +++ b/packages/core/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "package:checks": { + "dependsOn": ["build"] + } + } +} diff --git a/packages/lightspark-sdk/package.json b/packages/lightspark-sdk/package.json index 978ab6770..6a51085bb 100644 --- a/packages/lightspark-sdk/package.json +++ b/packages/lightspark-sdk/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/lightsparkdev/js-sdk", "repository": { "type": "git", - "url": "https://github.com/lightsparkdev/js-sdk.git" + "url": "git+https://github.com/lightsparkdev/js-sdk.git" }, "bugs": { "url": "https://github.com/lightsparkdev/js-sdk/issues" @@ -44,6 +44,7 @@ "files": [ "src/*", "dist/*", + "dist/objects/*", "CHANGELOG.md" ], "scripts": { @@ -58,7 +59,7 @@ "lint:fix:continue": "eslint --fix . || exit 0", "lint:watch": "esw ./src -w --ext .ts,.tsx,.js --color", "lint": "eslint .", - "package-types": "yarn attw --pack .", + "package:checks": "yarn publint && yarn attw --pack .", "postversion": "yarn build", "test-cmd": "node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --bail", "test": "yarn test-cmd src/tests/*.test.ts", @@ -92,6 +93,7 @@ "jest": "^29.6.2", "prettier": "3.0.3", "prettier-plugin-organize-imports": "^3.2.4", + "publint": "^0.3.9", "ts-jest": "^29.1.1", "tsc-absolute": "^1.0.1", "tsup": "^8.2.4", diff --git a/packages/lightspark-sdk/turbo.json b/packages/lightspark-sdk/turbo.json new file mode 100644 index 000000000..0657018cb --- /dev/null +++ b/packages/lightspark-sdk/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "package:checks": { + "dependsOn": ["build"] + } + } +} diff --git a/packages/oauth/package.json b/packages/oauth/package.json index d6cdabd1a..78ea439df 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -14,7 +14,7 @@ "homepage": "https://github.com/lightsparkdev/js-sdk", "repository": { "type": "git", - "url": "https://github.com/lightsparkdev/js-sdk.git" + "url": "git+https://github.com/lightsparkdev/js-sdk.git" }, "bugs": { "url": "https://github.com/lightsparkdev/js-sdk/issues" @@ -47,7 +47,7 @@ "lint:fix": "eslint --fix .", "lint:watch": "esw ./src -w --ext .ts,.tsx,.js --color", "lint": "eslint .", - "package-types": "yarn attw --pack .", + "package:checks": "yarn publint && yarn attw --pack .", "postversion": "yarn build", "test": "echo \"TODO\"", "types:watch": "tsc-absolute --watch", @@ -69,6 +69,7 @@ "jest": "^29.6.2", "prettier": "3.0.3", "prettier-plugin-organize-imports": "^3.2.4", + "publint": "^0.3.9", "ts-jest": "^29.1.1", "tsc-absolute": "^1.0.1", "tsup": "^8.2.4", diff --git a/packages/oauth/turbo.json b/packages/oauth/turbo.json new file mode 100644 index 000000000..0657018cb --- /dev/null +++ b/packages/oauth/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "package:checks": { + "dependsOn": ["build"] + } + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 8dc72e494..13af4ec52 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,9 +43,9 @@ "import": "./dist/utils/index.js", "require": "./dist/utils/index.cjs" }, - "./static/": { - "import": "./dist/static/", - "require": "./dist/static/" + "./static/*": { + "import": "./dist/static/*", + "require": "./dist/static/*" }, "./router": { "import": "./dist/router.js", @@ -77,7 +77,7 @@ "lint:fix": "eslint --fix .", "lint:watch": "esw . -w --ext .ts,.tsx,.js --color", "lint": "eslint .", - "package-types": "yarn attw --pack .", + "package:checks": "yarn attw --pack .", "test": "echo \"ui package tests are located in apps/examples/ui-test-app\"", "types": "yarn tsc", "types:watch": "tsc-absolute --watch" @@ -91,7 +91,7 @@ "@simbathesailor/use-what-changed": "^2.0.0", "@svgr/core": "^8.1.0", "@tanstack/react-table": "^8.20.5", - "@uma-sdk/core": "^1.2.3", + "@uma-sdk/core": "^1.3.0", "@wojtekmaj/react-daterange-picker": "^5.5.0", "@wojtekmaj/react-datetimerange-picker": "^5.5.0", "@zxing/browser": "^0.1.1", @@ -101,7 +101,7 @@ "libphonenumber-js": "^1.11.1", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", - "next": "^13.4.19", + "next": "^13.5.10", "prismjs": "^1.29.0", "qrcode.react": "^4.0.1", "react": "^18.2.0", @@ -157,6 +157,13 @@ }, "files": [ "dist/*", + "dist/hooks/*", + "dist/icons/*", + "dist/types/*", + "dist/styles/*", + "dist/utils/*", + "dist/router.js", + "dist/static/*", "CHANGELOG.md" ], "engines": { diff --git a/packages/ui/src/components/Banner/Banner.tsx b/packages/ui/src/components/Banner/Banner.tsx index 874e424fe..3768c0f54 100644 --- a/packages/ui/src/components/Banner/Banner.tsx +++ b/packages/ui/src/components/Banner/Banner.tsx @@ -29,8 +29,11 @@ export type BannerProps = { onHeightChange?: (height: number) => void; minHeight?: number | "auto" | undefined; hPadding?: number | undefined; + vPadding?: number | undefined; + fixed?: boolean | undefined; right?: ReactNode | undefined; left?: ReactNode | undefined; + blurScroll?: boolean | undefined; }; export function Banner({ @@ -42,11 +45,14 @@ export function Banner({ onHeightChange, right, left, + fixed, bgProgressDuration, borderProgress, minHeight = 0, hPadding = 0, + vPadding = 0, borderColor, + blurScroll = false, }: BannerProps) { const [width, setWidth] = useState(70); const resizeProps = useMemo(() => ["height" as const], []); @@ -88,7 +94,6 @@ export function Banner({ {contentNode} @@ -104,6 +109,10 @@ export function Banner({ ref={ref} borderProgress={borderProgress} hPadding={hPadding} + vPadding={vPadding} + minHeight={minHeight} + fixed={fixed} + blurScroll={blurScroll} > {left} {innerContent} @@ -122,7 +131,6 @@ export function Banner({ const BannerInnerContent = styled.div<{ isVisible: boolean; maxMdContentJustify: MaxMdContentJustify; - minHeight: number | "auto"; }>` z-index: 1; padding: ${({ isVisible }) => (isVisible ? `0px 17.5px` : "0px")}; @@ -131,8 +139,6 @@ const BannerInnerContent = styled.div<{ flex-direction: column; align-items: center; justify-content: center; - min-height: ${({ minHeight }) => - typeof minHeight === "number" ? `${minHeight}px` : "auto"}; & > * { position: relative; @@ -153,10 +159,13 @@ const StyledBanner = styled.div<{ hasSideContent: boolean; borderProgress: number | undefined; hPadding: number; + vPadding: number; + minHeight: number | "auto"; + fixed: boolean | undefined; borderColor: ThemeOrColorKey | undefined; + blurScroll: boolean | undefined; }>` - position: fixed; - left: 0; + ${({ fixed }) => (fixed ? "position: fixed; left: 0;" : "")}; width: 100%; z-index: ${z.notificationBanner}; @@ -164,11 +173,20 @@ const StyledBanner = styled.div<{ font-weight: 500; background-color: ${({ colorProp, theme }) => colorProp ? getColor(theme, colorProp) : "none"}; + ${({ blurScroll }) => + blurScroll + ? ` + background: rgba(249, 249, 249, 0.8); + backdrop-filter: blur(32px); + ` + : ""} display: flex; justify-content: ${({ hasSideContent }) => hasSideContent ? "space-between" : "center"}; align-items: center; - padding: ${({ hPadding }) => `0 ${hPadding}px`}; + padding: ${({ hPadding, vPadding }) => `${vPadding}px ${hPadding}px`}; + min-height: ${({ minHeight }) => + typeof minHeight === "number" ? `${minHeight}px` : "auto"}; ${({ borderColor, theme }) => borderColor ? `border-bottom: 1px solid ${getColor(theme, borderColor)};` diff --git a/packages/ui/src/components/CardForm/CardForm.tsx b/packages/ui/src/components/CardForm/CardForm.tsx index b6ebadd60..581fcd7f2 100644 --- a/packages/ui/src/components/CardForm/CardForm.tsx +++ b/packages/ui/src/components/CardForm/CardForm.tsx @@ -23,14 +23,16 @@ import { type CardFormBorderRadius, type CardFormBorderWidth, type CardFormKind, + type CardFormPaddingBottom, + type CardFormPaddingTop, type CardFormPaddingX, - type CardFormPaddingY, type CardFormShadow, type CardFormTextAlign, type CardFormThemeKey, } from "../../styles/themeDefaults/cardForm.js"; import { getColor } from "../../styles/themes.js"; import { Spacing } from "../../styles/tokens/spacing.js"; +import { type TokenSizeKey } from "../../styles/tokens/typography.js"; import { pxToRems } from "../../styles/utils.js"; import { type NewRoutesType } from "../../types/index.js"; import { select } from "../../utils/emotion.js"; @@ -59,28 +61,47 @@ type CardFormProps = { disabled?: boolean; topContent?: ReactNode; title?: string; + titleSize?: TokenSizeKey; titleRightIcon?: | ComponentProps["rightIcon"] | undefined; + afterTitleMargin?: 24 | 40 | undefined; description?: ToReactNodesArgs | undefined; + full?: boolean; onSubmit?: (e: FormEvent) => void; hasChildForm?: boolean; wide?: boolean; kind?: CardFormKind; textAlign?: CardFormTextAlign; shadow?: CardFormShadow; + paddingTop?: CardFormPaddingTop | undefined; + paddingBottom?: CardFormPaddingBottom | undefined; belowFormContent?: ToReactNodesArgs | undefined; belowFormContentGap?: BelowCardFormContentGap | undefined; + forceMarginAfterSubtitle?: boolean; }; type ResolvePropsArgs = { kind: CardFormKind; shadow?: CardFormShadow | undefined; textAlign?: CardFormTextAlign | undefined; + paddingTop?: CardFormPaddingTop | undefined; + paddingBottom?: CardFormPaddingBottom | undefined; }; function resolveProps(args: ResolvePropsArgs, theme: Theme) { - const paddingY = resolveCardFormProp(undefined, args.kind, "paddingY", theme); + const paddingTop = resolveCardFormProp( + args.paddingTop, + args.kind, + "paddingTop", + theme, + ); + const paddingBottom = resolveCardFormProp( + args.paddingBottom, + args.kind, + "paddingBottom", + theme, + ); const paddingX = resolveCardFormProp(undefined, args.kind, "paddingX", theme); const textAlign = resolveCardFormProp( args.textAlign, @@ -133,7 +154,8 @@ function resolveProps(args: ResolvePropsArgs, theme: Theme) { ); const props = { - paddingY, + paddingTop, + paddingBottom, paddingX, shadow, borderRadius, @@ -154,7 +176,9 @@ export function CardForm({ disabled, topContent = null, title, + titleSize = "Large", description, + full = false, onSubmit, titleRightIcon, /* In some cases eg third party libs we can't avoid the need for child forms, so the @@ -164,12 +188,17 @@ export function CardForm({ kind = "primary", shadow: shadowProp, textAlign: textAlignProp, + paddingTop: paddingTopProp, + paddingBottom: paddingBottomProp, belowFormContent, belowFormContentGap = 0, + forceMarginAfterSubtitle = true, + afterTitleMargin = 40, }: CardFormProps) { const theme = useTheme(); const { - paddingY, + paddingTop, + paddingBottom, paddingX, shadow, borderRadius, @@ -181,7 +210,13 @@ export function CardForm({ smBorderWidth, defaultDescriptionTypographyMap, } = resolveProps( - { kind, textAlign: textAlignProp, shadow: shadowProp }, + { + kind, + textAlign: textAlignProp, + shadow: shadowProp, + paddingTop: paddingTopProp, + paddingBottom: paddingBottomProp, + }, theme, ); @@ -202,6 +237,7 @@ export function CardForm({ ? toReactNodesWithTypographyMap( description, defaultDescriptionTypographyMap, + false, ) : null; @@ -209,11 +245,16 @@ export function CardForm({ ? toReactNodes(belowFormContent) : null; + const CardFormContentTarget = full ? CardFormContentFull : CardFormContent; + const content = ( - + {topContent} {title && ( - + )} @@ -229,7 +271,7 @@ export function CardForm({ {formattedDescription} )} {children} - + ); const commonProps = { @@ -240,25 +282,34 @@ export function CardForm({ borderColor, textAlign, paddingX, - paddingY, + paddingTop, + paddingBottom, backgroundColor, smBackgroundColor, smBorderWidth, + forceMarginAfterSubtitle, }; + const Container = full ? CardFormContentFull : CardFormContainer; + return ( - + {hasChildForm ? ( {content} ) : ( - + {content} )} {belowFormContentNodes} - + ); } @@ -273,6 +324,13 @@ const CardFormContent = styled.div` align-self: center; `; +const CardFormContentFull = styled.div` + display: flex; + flex-direction: column; + align-self: center; + height: 100%; +`; + type BelowCardFormContentProps = { gap: BelowCardFormContentGap; }; @@ -287,7 +345,9 @@ const BelowCardFormContent = styled.div` margin-bottom: ${Spacing.px.xl}; `; -const CardFormSubtitle = styled.div``; +const CardFormSubtitle = styled.div` + padding: 0 ${Spacing.px.xs}; +`; export const CardFormButtonRow = styled.div<{ justify?: "left" | "center" | undefined; @@ -361,20 +421,26 @@ const StyledCardFormCheckboxParagraph = styled.div` type CardFormInsetProps = { wide: boolean; paddingX: number; - paddingY: number; + paddingTop: number; + paddingBottom: number; }; -const formInset = ({ wide, paddingX, paddingY }: CardFormInsetProps) => css` +const formInset = ({ + wide, + paddingX, + paddingTop, + paddingBottom, +}: CardFormInsetProps) => css` margin-left: auto; margin-right: auto; max-width: 100%; padding-right: ${paddingX}px; padding-left: ${paddingX}px; - padding-top: ${paddingY}px; - padding-bottom: ${paddingY}px; + padding-top: ${paddingTop}px; + padding-bottom: ${paddingBottom}px; ${bp.minSm(` - width: ${wide ? 700 : 505}px; + width: ${wide ? 700 : 408}px; `)} ${bp.sm(` @@ -397,7 +463,7 @@ const formInset = ({ wide, paddingX, paddingY }: CardFormInsetProps) => css` & ${CardFormFullTopContent} { ${bp.minSm(` - margin-top: -${paddingY}px; + margin-top: -${paddingTop}px; `)} } `; @@ -410,10 +476,12 @@ type StyledCardFormStyleProps = { borderColor: CardFormBorderColor; textAlign: CardFormTextAlign; paddingX: CardFormPaddingX; - paddingY: CardFormPaddingY; + paddingTop: CardFormPaddingTop; + paddingBottom: CardFormPaddingBottom; backgroundColor: CardFormBackgroundColor; smBackgroundColor: CardFormBackgroundColor; smBorderWidth: CardFormBorderWidth; + forceMarginAfterSubtitle: boolean | undefined; }; const StyledCardFormStyle = ({ @@ -425,13 +493,15 @@ const StyledCardFormStyle = ({ borderColor, textAlign, paddingX, - paddingY, + paddingTop, + paddingBottom, backgroundColor, smBackgroundColor, smBorderWidth, + forceMarginAfterSubtitle = true, }: StyledCardFormStyleProps & { theme: Theme }) => { return css` - ${formInset({ wide, paddingX, paddingY })} + ${formInset({ wide, paddingX, paddingTop, paddingBottom })} ${shadow === "soft" ? standardCardShadow : shadow === "hard" @@ -459,9 +529,11 @@ const StyledCardFormStyle = ({ text-align: ${textAlign}; } - ${CardFormSubtitle} + * { + ${forceMarginAfterSubtitle + ? `${CardFormSubtitle.toString()} + * { margin-top: 40px !important; - } + }` + : ""} ${CardFormButtonRow}, ${StyledButtonRowButton} { ${ButtonSelector()} { @@ -514,14 +586,20 @@ const StyledCardFormStyle = ({ const StyledCardForm = styled.form(StyledCardFormStyle); + const StyledCardFormDiv = styled.div(StyledCardFormStyle); -const CardHeadline = styled.div<{ hasTopContent: boolean }>` - ${({ hasTopContent }) => (hasTopContent ? "margin-top: 32px;" : "")} +const CardHeadline = styled.div<{ + hasTopContent: boolean; + afterTitleMargin: number; +}>` + padding: 0 ${Spacing.px.xs}; + + ${({ hasTopContent }) => (hasTopContent ? "margin-top: 24px;" : "")} - & + *:not(${CardFormSubtitle}) { - margin-top: 40px; + & + *:not(${CardFormSubtitle.toString()}) { + margin-top: ${({ afterTitleMargin }) => afterTitleMargin}px; } & + ${CardFormSubtitle} { diff --git a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx index e82cd45b1..5849a9bd8 100644 --- a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx +++ b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx @@ -64,6 +64,7 @@ export function SingleCodeInput({ theme, disabled: Boolean(disabled), hasError: false, + borderRadius: 8, }).styles } width: ${width}; diff --git a/packages/ui/src/components/Drawer.tsx b/packages/ui/src/components/Drawer.tsx index dacc579c8..7b65fde42 100644 --- a/packages/ui/src/components/Drawer.tsx +++ b/packages/ui/src/components/Drawer.tsx @@ -9,7 +9,7 @@ import { Button } from "./Button.js"; import { Icon } from "./Icon/Icon.js"; import { UnstyledButton } from "./UnstyledButton.js"; -type DrawerKind = "default" | "floating"; +export type DrawerKind = "default" | "floating"; interface Props { children?: React.ReactNode; @@ -18,8 +18,9 @@ interface Props { closeButton?: boolean; nonDismissable?: boolean; handleBack?: (() => void) | undefined; - padding?: string; + padding?: string | undefined; kind?: DrawerKind | undefined; + top?: number | undefined; } export const Drawer = (props: Props) => { @@ -105,6 +106,7 @@ export const Drawer = (props: Props) => { totalDeltaY={totalDeltaY} grabbing={grabbing} ref={drawerContainerRef} + top={props.top} > {props.grabber && !props.nonDismissable && ( @@ -170,6 +172,7 @@ const DrawerContainer = styled.div<{ isOpen: boolean; totalDeltaY: number; grabbing: boolean; + top?: number | undefined; }>` position: fixed; max-height: 100dvh; @@ -182,6 +185,8 @@ const DrawerContainer = styled.div<{ flex-direction: column; align-items: center; + ${(props) => props.top && `top: ${props.top}px;`} + // Only smooth transition when not grabbing, otherwise dragging will feel very laggy ${(props) => props.grabbing && props.isOpen @@ -233,7 +238,6 @@ const DrawerInnerContainer = styled.div<{ ? `${props.padding}` : `${Spacing.px["6xl"]} ${Spacing.px.xl} ${Spacing.px["2xl"]} ${Spacing.px.xl}`}; - overflow-y: auto; `; const Grabber = styled.div` diff --git a/packages/ui/src/components/Flex.tsx b/packages/ui/src/components/Flex.tsx index a45d4f52c..45ed54582 100644 --- a/packages/ui/src/components/Flex.tsx +++ b/packages/ui/src/components/Flex.tsx @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { isNumber } from "lodash-es"; import { type ElementType, type ReactNode } from "react"; type FlexProps = { @@ -35,6 +36,8 @@ type FlexProps = { | "pre-wrap" | "pre-line" | undefined; + height?: number | "auto" | "100%" | undefined; + width?: number | "auto" | "100%" | undefined; mt?: number | "auto" | undefined; mr?: number | "auto" | undefined; mb?: number | "auto" | undefined; @@ -44,6 +47,7 @@ type FlexProps = { pb?: number | undefined; pl?: number | undefined; gap?: number | undefined; + position?: "absolute" | "relative" | undefined; }; export function Flex({ @@ -67,6 +71,9 @@ export function Flex({ pr, pb, pl, + height, + width, + position, }: FlexProps) { const justify = justifyProp ? justifyProp : center ? "center" : "stretch"; const align = alignProp ? alignProp : center ? "center" : "stretch"; @@ -92,6 +99,9 @@ export function Flex({ pb={pb} pl={pl} gap={gap} + height={height} + width={width} + position={position} {...asButtonProps} > {children} @@ -128,6 +138,9 @@ type StyledFlexProps = { pl: FlexProps["pl"]; gap: number | undefined; as?: ElementType | undefined; + height?: FlexProps["height"]; + width?: FlexProps["width"]; + position?: FlexProps["position"]; }; export const StyledFlex = styled.div` @@ -166,4 +179,11 @@ export const StyledFlex = styled.div` ${({ pr }) => pr && `padding-right: ${pr}px;`} ${({ pb }) => pb && `padding-bottom: ${pb}px;`} ${({ pl }) => pl && `padding-left: ${pl}px;`} + + ${({ height }) => + height && + (isNumber(height) ? `height: ${height}px;` : `height: ${height};`)} + ${({ width }) => + width && (isNumber(width) ? `width: ${width}px;` : `width: ${width};`)} + ${({ position }) => position && `position: ${position};`} `; diff --git a/packages/ui/src/components/IconWithCircleBackground.tsx b/packages/ui/src/components/IconWithCircleBackground.tsx index 134fd4b1a..ae088f80a 100644 --- a/packages/ui/src/components/IconWithCircleBackground.tsx +++ b/packages/ui/src/components/IconWithCircleBackground.tsx @@ -1,22 +1,26 @@ import styled from "@emotion/styled"; import { Link } from "../router.js"; -import { getColor } from "../styles/themes.js"; +import { getColor, type ThemeOrColorKey } from "../styles/themes.js"; import { type NewRoutesType } from "../types/index.js"; import { Flex } from "./Flex.js"; import { Icon } from "./Icon/Icon.js"; import { type IconName } from "./Icon/types.js"; -type IconWidth = 40 | 30 | 16.5; +type IconWidth = 40 | 36 | 30 | 20 | 16.5 | 14; type IconWithCircleBackgroundProps = { iconName?: IconName; iconWidth?: IconWidth | undefined; + iconColor?: ThemeOrColorKey | undefined; to?: NewRoutesType | undefined; darkBg?: boolean; noBg?: boolean; + bgColor?: ThemeOrColorKey | undefined; shouldRotate?: boolean; onClick?: () => void; disabled?: boolean; + iconStrokeWidth?: number; + opaque?: boolean; }; export function IconWithCircleBackground({ @@ -28,6 +32,10 @@ export function IconWithCircleBackground({ shouldRotate = false, noBg = false, disabled = false, + bgColor, + iconColor, + iconStrokeWidth, + opaque = false, }: IconWithCircleBackgroundProps) { const content = ( @@ -67,14 +80,17 @@ type StyledIconWithCircleBackgroundProps = { darkBg: boolean; shouldRotate: boolean; noBg: boolean; + bgColor: ThemeOrColorKey | undefined; }; const StyledIconWithCircleBackground = styled.div` - background: ${({ theme, darkBg, noBg }) => + background: ${({ theme, darkBg, noBg, bgColor }) => darkBg ? `linear-gradient(291.4deg, #1C243F 0%, #21283A 100%)` : noBg ? "transparent" + : bgColor + ? getColor(theme, bgColor) : getColor(theme, "grayBlue94")}; border-radius: 50%; padding: ${({ size }) => getPadding(size)}px; @@ -105,5 +121,11 @@ function getPadding(size: IconWidth) { if (size === 40) { return 20; } + if (size === 36) { + return 14; + } + if (size === 20) { + return 10; + } return 16; } diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index c4263df32..8d6dce741 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -31,13 +31,12 @@ import { type ToReactNodesArgs, } from "../utils/toReactNodes/toReactNodes.js"; import { Button, ButtonSelector } from "./Button.js"; -import { Drawer } from "./Drawer.js"; +import { Drawer, type DrawerKind } from "./Drawer.js"; import { Icon } from "./Icon/Icon.js"; import { IconWithCircleBackground } from "./IconWithCircleBackground.js"; import { type LoadingKind } from "./Loading.js"; import { ProgressBar, type ProgressBarProps } from "./ProgressBar.js"; import { UnstyledButton } from "./UnstyledButton.js"; - type IconProps = ComponentProps; type ExtraAction = ComponentProps & { @@ -117,6 +116,9 @@ type ModalProps = { appendToElement?: HTMLElement; bottomContent?: ReactNode | undefined; topLeftIcon?: ComponentProps | undefined; + drawerKind?: DrawerKind; + drawerPadding?: number; + drawerCloseButton?: boolean; }; export function Modal({ @@ -152,6 +154,9 @@ export function Modal({ appendToElement, bottomContent, topLeftIcon, + drawerKind, + drawerPadding, + drawerCloseButton = true, }: ModalProps) { const visibleChangedRef = useRef(false); const [visibleChanged, setVisibleChanged] = useState(false); @@ -403,9 +408,12 @@ export function Modal({ content = ( onClickCloseButton()} - closeButton + closeButton={drawerCloseButton} nonDismissable={nonDismissable} handleBack={handleBack} + kind={drawerKind ?? "default"} + padding={drawerPadding !== undefined ? `${drawerPadding}px` : undefined} + top={top} > {modalContent} diff --git a/packages/ui/src/components/PhoneInput.tsx b/packages/ui/src/components/PhoneInput.tsx index 57aa81280..41b110d66 100644 --- a/packages/ui/src/components/PhoneInput.tsx +++ b/packages/ui/src/components/PhoneInput.tsx @@ -10,6 +10,7 @@ import { type ComponentProps, type FocusEvent, } from "react"; +import { type TextInputBorderRadius } from "../styles/fields.js"; import { countryCodesToNames } from "../utils/countryCodesToNames.js"; import { TextInput } from "./TextInput.js"; import { type PartialSimpleTypographyProps } from "./typography/types.js"; @@ -42,6 +43,7 @@ export type PhoneInputOnChangeArg = { type PhoneInputProps = { pxPerChar?: number; + borderRadius?: TextInputBorderRadius | undefined; onChange?: ({ number, countryCallingCode, @@ -62,6 +64,7 @@ export function PhoneInput({ error: errorProp, pxPerChar = 6, defaultCountryCode = "US", + borderRadius = 8 as TextInputBorderRadius, }: PhoneInputProps) { /* countryCode only controls the country used for parsing phone number input. User may still enter or paste the country phone code */ @@ -132,6 +135,7 @@ export function PhoneInput({ value: countryCode, onChange: onChangeSelect, width, + height: 54, }} pattern="[0-9,]*" inputMode="numeric" @@ -145,6 +149,9 @@ export function PhoneInput({ onChange={onChange} error={errorProp || error} typography={typography} + borderRadius={borderRadius} + borderWidth={0.5} + paddingY={13.5} /> ); } diff --git a/packages/ui/src/components/TextInput.tsx b/packages/ui/src/components/TextInput.tsx index 9e456404b..29634f9fd 100644 --- a/packages/ui/src/components/TextInput.tsx +++ b/packages/ui/src/components/TextInput.tsx @@ -102,12 +102,16 @@ export type TextInputProps = { onChange: (value: string) => void; /* A specified width is required to ensure left input padding is correct */ width: number; + /* A specified height is required to ensure the select is the same height as the input */ + height: number; } | undefined; borderRadius?: TextInputBorderRadius | undefined; + borderWidth?: number | undefined; width?: "full" | "short" | undefined; paddingX?: number; paddingY?: number; + marginTop?: number | undefined; // Outline that appears outside/offset when the input is focused activeOutline?: boolean; activeOutlineColor?: ThemeOrColorKey; @@ -246,6 +250,7 @@ export function TextInput(textInputProps: TextInputProps) { } }} borderRadius={props.borderRadius} + borderWidth={props.borderWidth} enterKeyHint={props.enterKeyHint} autoFocus={props.autoFocus} /> @@ -310,7 +315,7 @@ export function TextInput(textInputProps: TextInputProps) { const { select } = props; return ( - + {props.label ? ( {props.label} ) : null} @@ -318,6 +323,7 @@ export function TextInput(textInputProps: TextInputProps) { { select.onChange(event.target.value); @@ -346,6 +352,7 @@ export function TextInput(textInputProps: TextInputProps) { const TextInputSelect = styled.select<{ widthProp: number; + heightProp: number; typography: RequiredSimpleTypographyProps; }>` ${({ typography, theme }) => @@ -356,7 +363,7 @@ const TextInputSelect = styled.select<{ background-color: transparent; top: 0; left: ${selectLeftOffset}px; - height: 48px; + height: ${({ heightProp }) => `${heightProp}px`}; width: ${({ widthProp }) => `${widthProp}px`}; `; @@ -411,11 +418,11 @@ interface InputProps { activeOutlineColor?: ThemeOrColorKey | undefined; typography: RequiredSimpleTypographyProps; borderRadius?: TextInputBorderRadius | undefined; + borderWidth?: number | undefined; } const Input = styled.input` ${textInputStyle}; - // disable autofill styles in chrome https://stackoverflow.com/a/68240841/9808766 &:-webkit-autofill, &:-webkit-autofill:focus, @@ -473,9 +480,15 @@ export const TextInputHalfRow = styled.div` } `; -const StyledTextInput = styled.div<{ widthProp: string }>` +const StyledTextInput = styled.div<{ + widthProp: string; + marginTop: number | undefined; +}>` width: ${({ widthProp }) => widthProp}; position: relative; + /* Apply marginTop to every TextInput when specified */ + margin-top: ${({ marginTop }) => + marginTop !== undefined ? `${marginTop}px` : ""}; /* eg forms, should be left consistent: */ & + & { diff --git a/packages/ui/src/hooks/index.tsx b/packages/ui/src/hooks/index.tsx new file mode 100644 index 000000000..8df1054ae --- /dev/null +++ b/packages/ui/src/hooks/index.tsx @@ -0,0 +1,11 @@ +export { useClipboard } from "./useClipboard.js"; +export { useDebounce } from "./useDebounce.js"; +export { useDocumentTitle } from "./useDocumentTitle.js"; +export { default as useFields } from "./useFields.js"; +export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useLiveRef } from "./useLiveRef.js"; +export { useMaxScaleIOS } from "./useMaxScaleIOS.js"; +export { useNumberInput } from "./useNumberInput/useNumberInput.js"; +export { useQueryParams } from "./useQueryParams.js"; +export { useResizeObserver } from "./useResizeObserver.js"; +export { useWhatChanged } from "./useWhatChanged.js"; diff --git a/packages/ui/src/icons/Bank.tsx b/packages/ui/src/icons/Bank.tsx index 00853999d..a8d2693ef 100644 --- a/packages/ui/src/icons/Bank.tsx +++ b/packages/ui/src/icons/Bank.tsx @@ -6,12 +6,13 @@ export function Bank({ strokeWidth = "1.5", strokeLinecap = "square", strokeLinejoin = "round", + fill = "none", }: PathProps) { return ( + + + ); +} diff --git a/packages/ui/src/icons/Globe.tsx b/packages/ui/src/icons/Globe.tsx index 8519169b2..01db75310 100644 --- a/packages/ui/src/icons/Globe.tsx +++ b/packages/ui/src/icons/Globe.tsx @@ -5,13 +5,7 @@ export function Globe({ strokeLinecap = "round", }: PathProps) { return ( - + + + + ); } diff --git a/packages/ui/src/icons/Lock2.tsx b/packages/ui/src/icons/Lock2.tsx new file mode 100644 index 000000000..20bdba395 --- /dev/null +++ b/packages/ui/src/icons/Lock2.tsx @@ -0,0 +1,23 @@ +// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved + +import { type PathProps } from "./types.js"; + +export function Lock2({ strokeWidth = "1.4" }: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/ArrowUpDown.tsx b/packages/ui/src/icons/central/ArrowUpDown.tsx new file mode 100644 index 000000000..1c7e10670 --- /dev/null +++ b/packages/ui/src/icons/central/ArrowUpDown.tsx @@ -0,0 +1,25 @@ +import { type PathProps } from "../types.js"; + +export function ArrowUpDown({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps = {}) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/Leading.tsx b/packages/ui/src/icons/central/Leading.tsx new file mode 100644 index 000000000..8cf209549 --- /dev/null +++ b/packages/ui/src/icons/central/Leading.tsx @@ -0,0 +1,26 @@ +import { type PathProps } from "../types.js"; + +export function Leading({ + strokeWidth = "1.5", + strokeLinecap = "square", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index e1d4f7d67..d68a035f5 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -3,6 +3,7 @@ export { ArrowDownLeft as CentralArrowDownLeft } from "./ArrowDownLeft.js"; export { ArrowInbox as CentralArrowInbox } from "./ArrowInbox.js"; export { ArrowLeft as CentralArrowLeft } from "./ArrowLeft.js"; export { ArrowsRepeatLeftRight as CentralArrowsRepeatLeftRight } from "./ArrowsRepeatLeftRight.js"; +export { ArrowUpDown as CentralArrowUpDown } from "./ArrowUpDown.js"; export { ArrowUpRight as CentralArrowUpRight } from "./ArrowUpRight.js"; export { Bank as CentralBank } from "./Bank.js"; export { BarsThree as CentralBarsThree } from "./BarsThree.js"; @@ -35,6 +36,7 @@ export { FingerPrint1 as CentralFingerPrint1 } from "./FingerPrint1.js"; export { Globus as CentralGlobus } from "./Globus.js"; export { Group3 as CentralGroup3 } from "./Group3.js"; export { Key2 as CentralKey2 } from "./Key2.js"; +export { Leading as CentralLeading } from "./Leading.js"; export { Loader as CentralLoader } from "./Loader.js"; export { Lock as CentralLock } from "./Lock.js"; export { MagnifyingGlass as CentralMagnifyingGlass } from "./MagnifyingGlass.js"; diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx index 9223e591c..808b116fa 100644 --- a/packages/ui/src/icons/index.tsx +++ b/packages/ui/src/icons/index.tsx @@ -33,6 +33,7 @@ export { CheckmarkCircle } from "./CheckmarkCircle.js"; export { CheckmarkCircleTier1 } from "./CheckmarkCircleTier1.js"; export { CheckmarkCircleTier2 } from "./CheckmarkCircleTier2.js"; export { CheckmarkCircleTier3 } from "./CheckmarkCircleTier3.js"; +export { CheckmarkGreen } from "./CheckmarkGreen.js"; export { Chevron } from "./Chevron.js"; export { ChevronDown } from "./ChevronDown.js"; export { ChevronLeft } from "./ChevronLeft.js"; @@ -84,6 +85,7 @@ export { LinkIcon } from "./LinkIcon.js"; export { LoadingCircleLines } from "./LoadingCircleLines.js"; export { LoadingSpinner } from "./LoadingSpinner.js"; export { Lock } from "./Lock.js"; +export { Lock2 } from "./Lock2.js"; export { Logo } from "./Logo.js"; export { LogoBolt } from "./LogoBolt.js"; export { LogoIssuance } from "./LogoIssuance.js"; diff --git a/packages/ui/src/icons/types.tsx b/packages/ui/src/icons/types.tsx index dd7d11fbe..b6c25e7e0 100644 --- a/packages/ui/src/icons/types.tsx +++ b/packages/ui/src/icons/types.tsx @@ -8,4 +8,5 @@ export type PathProps = { strokeWidth?: PathStrokeWidth; strokeLinecap?: PathLinecap; strokeLinejoin?: PathLinejoin; + fill?: string; }; diff --git a/packages/ui/src/router.tsx b/packages/ui/src/router.tsx index f4698dc0f..e6affdeb6 100644 --- a/packages/ui/src/router.tsx +++ b/packages/ui/src/router.tsx @@ -142,6 +142,7 @@ export const LinkBase = forwardRef( size: typography.size, color: typography.color, children: text, + underline: typography.underline, }) : text; } diff --git a/packages/ui/src/styles/colors.tsx b/packages/ui/src/styles/colors.tsx index 81b3c4178..a32f95c37 100644 --- a/packages/ui/src/styles/colors.tsx +++ b/packages/ui/src/styles/colors.tsx @@ -19,6 +19,7 @@ const neutral = { gray91: "#171717", gray95: "#F2F2F2", gray98: "#F9F9F9", + gray99: "#EDEEF1", white: "#FFFFFF", }; @@ -40,10 +41,12 @@ const baseColors = { green33: "#179257", green35: "#19981E", // blue + blue10: "#E4EFFB", blue43: "#145BC6", blue22: "#0E2E60", blue39: "#0068C9", blue37: "#21529c", + blue50: "#2483D1", blue58: "#28BFFF", // less than 50% saturated blue grayBlue5: "#0c0d0f", @@ -74,6 +77,8 @@ const baseColors = { red42a10: "#D800271A", red42a20: "#D800272D", red42a30: "#D800273F", + errorText: "#E41C1B", + errorBackground: "#FEE2E1", // yellow primary, warning: primary, @@ -84,6 +89,10 @@ const baseColors = { // neutral secondary: neutral.black, gray: "#242526", + gray2: "#6D7685", + gray3: "#D9DBDF", + gray4: "#B6BAC3", + gray6: "#8E95A2", // transparent transparent: "transparent", transparenta02: "#00000005", diff --git a/packages/ui/src/styles/fields.tsx b/packages/ui/src/styles/fields.tsx index 3e5763c18..8189d0e15 100644 --- a/packages/ui/src/styles/fields.tsx +++ b/packages/ui/src/styles/fields.tsx @@ -2,9 +2,11 @@ import { css } from "@emotion/react"; import styled from "@emotion/styled"; import { useLayoutEffect, useRef, useState } from "react"; import { type PartialSimpleTypographyProps } from "../components/typography/types.js"; +import { colors } from "./colors.js"; import { standardBorderRadius, subtext } from "./common.js"; import { getColor, + isBridge, themeOr, type ThemeOrColorKey, type ThemeProp, @@ -52,12 +54,13 @@ export const inputBlockStyle = ({ export const textInputPlaceholderColor = ({ theme }: ThemeProp) => theme.c4Neutral; export const textInputFontWeight = 600; -export const textInputBorderRadiusPx = 8; export const textInputPaddingPx = 12; export const textInputPadding = `${textInputPaddingPx}px`; export const textInputBorderColor = ({ theme }: ThemeProp) => - themeOr(theme.c1Neutral, theme.c3Neutral)({ theme }); + isBridge(theme) + ? colors.gray4 + : themeOr(theme.c1Neutral, theme.c3Neutral)({ theme }); export const textInputBorderColorFocused = ({ theme }: ThemeProp) => themeOr(theme.hcNeutral, theme.hcNeutral)({ theme }); @@ -65,11 +68,15 @@ const textInputActiveStyles = ({ theme, paddingLeftPx, paddingRightPx, + paddingTopPx, + paddingBottomPx, activeOutline, activeOutlineColor, }: WithTheme<{ paddingLeftPx?: number | undefined; paddingRightPx?: number | undefined; + paddingTopPx?: number | undefined; + paddingBottomPx?: number | undefined; activeOutline?: boolean | undefined; activeOutlineColor?: ThemeOrColorKey | undefined; }>) => { @@ -90,6 +97,8 @@ const textInputActiveStyles = ({ padding: ${textInputPaddingPx - 1}px; ${paddingLeftPx ? `padding-left: ${paddingLeftPx - 1}px;` : ""} ${paddingRightPx ? `padding-right: ${paddingRightPx - 1}px;` : ""} + ${paddingTopPx ? `padding-top: ${paddingTopPx - 1}px;` : ""} + ${paddingBottomPx ? `padding-bottom: ${paddingBottomPx - 1}px;` : ""} `; }; @@ -99,7 +108,7 @@ export const defaultTextInputTypography = { color: "text", } as const; -export type TextInputBorderRadius = "round" | "default"; +export type TextInputBorderRadius = 8 | 16 | 999; export const textInputStyle = ({ theme, @@ -114,6 +123,7 @@ export const textInputStyle = ({ activeOutlineColor, typography, borderRadius, + borderWidth, }: WithTheme<{ // In some cases we want to show an active state when another element is focused. active?: boolean | undefined; @@ -127,10 +137,9 @@ export const textInputStyle = ({ activeOutlineColor?: ThemeOrColorKey | undefined; typography?: PartialSimpleTypographyProps | undefined; borderRadius?: TextInputBorderRadius | undefined; + borderWidth?: number | undefined; }>) => css` - border-radius: ${borderRadius === "round" - ? "999" - : textInputBorderRadiusPx}px; + border-radius: ${borderRadius ?? 8}px; background-color: ${disabled ? theme.vlcNeutral : theme.inputBackground}; cursor: ${disabled ? "not-allowed" : "auto"}; box-sizing: border-box; @@ -151,7 +160,11 @@ export const textInputStyle = ({ ? `padding-bottom: ${paddingBottomPx - (hasError ? 1 : 0)}px;` : ""} border-style: solid; - border-width: ${hasError ? "2" : "1"}px; + border-width: ${hasError + ? "2" + : borderWidth !== undefined + ? borderWidth + : "1"}px; border-color: ${hasError ? theme.danger : textInputBorderColor({ theme })}; line-height: 22px; outline: none; @@ -173,6 +186,8 @@ export const textInputStyle = ({ theme, paddingLeftPx, paddingRightPx, + paddingTopPx, + paddingBottomPx, activeOutline, activeOutlineColor, })} @@ -183,6 +198,8 @@ export const textInputStyle = ({ theme, paddingLeftPx, paddingRightPx, + paddingTopPx, + paddingBottomPx, activeOutline, activeOutlineColor, })} diff --git a/packages/ui/src/styles/themeDefaults/cardForm.tsx b/packages/ui/src/styles/themeDefaults/cardForm.tsx index d6c9c7fa6..bb4092694 100644 --- a/packages/ui/src/styles/themeDefaults/cardForm.tsx +++ b/packages/ui/src/styles/themeDefaults/cardForm.tsx @@ -2,17 +2,20 @@ import type { ThemeOrColorKey } from "../themes.js"; import type { TokenSizeKey, TypographyTypeKey } from "../tokens/typography.js"; export type CardFormPaddingY = 40 | 56; -export type CardFormPaddingX = 40 | 56; +export type CardFormPaddingX = 32 | 40 | 56; +export type CardFormPaddingTop = 24 | 32 | 40 | 56; +export type CardFormPaddingBottom = 32 | 40 | 56 | 64; export type CardFormBorderWidth = 0 | 1; -export type CardFormBorderRadius = 8 | 24; +export type CardFormBorderRadius = 8 | 24 | 32; export type CardFormShadow = "soft" | "hard" | "none"; export type CardFormTextAlign = "center" | "left"; export type CardFormBorderColor = "vlcNeutral" | "grayBlue94"; export type CardFormBackgroundColor = "bg" | "white"; const cardFormThemeBaseProps = { - paddingY: 56 as CardFormPaddingY, paddingX: 56 as CardFormPaddingX, + paddingTop: 56 as CardFormPaddingTop, + paddingBottom: 56 as CardFormPaddingBottom, shadow: "soft" as CardFormShadow, borderRadius: 24 as CardFormBorderRadius, borderWidth: 0 as CardFormBorderWidth, @@ -57,13 +60,19 @@ export const defaultCardFormTheme = { kinds: { primary: {}, secondary: { - paddingY: 40, + paddingTop: 40, + paddingBottom: 40, paddingX: 40, shadow: "none", borderRadius: 8, borderWidth: 1, borderColor: "vlcNeutral", }, - tertiary: { paddingY: 56, paddingX: 40, shadow: "hard" }, + tertiary: { + paddingTop: 56, + paddingBottom: 56, + paddingX: 40, + shadow: "hard", + }, } as Partial, }; diff --git a/packages/ui/src/styles/themes.tsx b/packages/ui/src/styles/themes.tsx index 68388a5c1..f25f05fda 100644 --- a/packages/ui/src/styles/themes.tsx +++ b/packages/ui/src/styles/themes.tsx @@ -532,14 +532,14 @@ const bridgeBaseSettings = { buttons: merge(buttonsThemeBase, { defaultTypographyType: "Title", defaultSize: "Medium", - defaultBorderRadius: 8, + defaultBorderRadius: 999, defaultPaddingsY: { - ExtraSmall: 14, - Small: 14, - Schmedium: 14, - Medium: 14, - Mlarge: 14, - Large: 14, + ExtraSmall: 13, + Small: 13, + Schmedium: 13, + Medium: 13, + Mlarge: 13, + Large: 13, }, kinds: { primary: { @@ -624,6 +624,7 @@ const bridgeBaseSettings = { cardForm: merge(cardFormThemeBase, { backgroundColor: "white", smBackgroundColor: "bg", + borderRadius: 32, defaultDescriptionTypographyMap: { default: { type: "Body", @@ -638,16 +639,21 @@ const bridgeBaseSettings = { }, kinds: { primary: { - paddingY: 56, + borderWidth: 1, + smBorderWidth: 0, + borderColor: "grayBlue94", + paddingTop: 24, + paddingBottom: 64, paddingX: 40, - shadow: "hard", + shadow: "none", }, secondary: { borderWidth: 1, smBorderWidth: 0, borderColor: "grayBlue94", - paddingY: 40, - paddingX: 40, + paddingTop: 32, + paddingBottom: 64, + paddingX: 32, shadow: "none", }, }, @@ -664,6 +670,7 @@ const bridgeLightTheme = extend(lightTheme, { smBg: colors.gray98, text: colors.grayBlue9, secondary: colors.grayBlue43, + tertiary: colors.gray3, inputBackground: colors.white, danger: colors.red50, }); @@ -811,6 +818,9 @@ export const isLight = (theme: Theme) => Themes.UmaAuthSdkLight, ].includes(theme.type); +export const isBridge = (theme: Theme) => + [Themes.BridgeLight, Themes.BridgeDark].includes(theme.type); + export const themeOr = (lightValue: string, darkValue: string) => ({ theme }: { theme: Theme }) => { diff --git a/packages/ui/src/styles/tokens/typography.ts b/packages/ui/src/styles/tokens/typography.ts index 58e7897b8..7965a8a3f 100644 --- a/packages/ui/src/styles/tokens/typography.ts +++ b/packages/ui/src/styles/tokens/typography.ts @@ -211,8 +211,8 @@ const LETTER_SPACING = { loose: ".1em", }, [TypographyGroup.Bridge]: { - tight: "-2%", - normal: "0%", + tight: "-0.48px", + normal: "0", wide: "10%", }, }; diff --git a/packages/ui/src/utils/toReactNodes/setReactNodesTypography.tsx b/packages/ui/src/utils/toReactNodes/setReactNodesTypography.tsx index 496ee12f1..a5ca2c0ae 100644 --- a/packages/ui/src/utils/toReactNodes/setReactNodesTypography.tsx +++ b/packages/ui/src/utils/toReactNodes/setReactNodesTypography.tsx @@ -126,6 +126,7 @@ function setTextNodeTypography( export function toReactNodesWithTypographyMap( nodesArg: ToReactNodesArgs, nodesTypographyMap: SetReactNodesTypographyMap, + replaceExistingTypography = true, ) { if (!nodesArg) { return null; @@ -134,6 +135,7 @@ export function toReactNodesWithTypographyMap( const nodesWithTypography = setReactNodesTypography( nodesArg, nodesTypographyMap, + replaceExistingTypography, ); const nodes = toReactNodes(nodesWithTypography); diff --git a/packages/ui/turbo.json b/packages/ui/turbo.json new file mode 100644 index 000000000..0657018cb --- /dev/null +++ b/packages/ui/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "package:checks": { + "dependsOn": ["build"] + } + } +} diff --git a/turbo.json b/turbo.json index 2f4e7ff98..c45cf823f 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,6 @@ { "$schema": "https://turbo.build/schema.json", - "pipeline": { + "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", "build/**"] @@ -14,7 +14,9 @@ "build-sb": { "outputs": ["storybook-static/**"] }, - "circular-deps": {}, + "circular-deps": { + "dependsOn": [] + }, "clean": { "cache": false }, @@ -26,8 +28,12 @@ "dependsOn": ["^build"], "outputs": ["docs/**"] }, - "format": {}, - "format:fix": {}, + "format": { + "dependsOn": [] + }, + "format:fix": { + "dependsOn": [] + }, "gql-codegen": { /* Always run codegen since it depends on files external to the workspace: */ "cache": false @@ -42,8 +48,8 @@ "cache": false, "persistent": true }, - "package-types": { - "dependsOn": ["build", "^build"] + "package:checks": { + "dependsOn": [] }, "start": { "cache": false, diff --git a/yarn.lock b/yarn.lock index df275e3d4..a778e1db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2149,6 +2149,7 @@ __metadata: lodash-es: "npm:^4.17.21" prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" + publint: "npm:^0.3.9" secp256k1: "npm:^5.0.1" ts-jest: "npm:^29.1.1" tsc-absolute: "npm:^1.0.1" @@ -2253,6 +2254,7 @@ __metadata: jest: "npm:^29.6.2" prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" + publint: "npm:^0.3.9" ts-jest: "npm:^29.1.1" tsc-absolute: "npm:^1.0.1" tsup: "npm:^8.2.4" @@ -2334,6 +2336,7 @@ __metadata: jest: "npm:^29.6.2" prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" + publint: "npm:^0.3.9" ts-jest: "npm:^29.1.1" tsc-absolute: "npm:^1.0.1" tsup: "npm:^8.2.4" @@ -2441,7 +2444,7 @@ __metadata: "@testing-library/user-event": "npm:^14.4.3" "@types/jest": "npm:^29.5.3" "@types/prismjs": "npm:^1.26.0" - "@uma-sdk/core": "npm:^1.2.3" + "@uma-sdk/core": "npm:^1.3.0" "@wojtekmaj/react-daterange-picker": "npm:^5.5.0" "@wojtekmaj/react-datetimerange-picker": "npm:^5.5.0" "@zxing/browser": "npm:^0.1.1" @@ -2460,7 +2463,7 @@ __metadata: lodash-es: "npm:^4.17.21" madge: "npm:^6.1.0" nanoid: "npm:^4.0.0" - next: "npm:^13.4.19" + next: "npm:^13.5.10" prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" prismjs: "npm:^1.29.0" @@ -2492,7 +2495,7 @@ __metadata: "@lightsparkdev/tsconfig": "npm:0.0.1" "@types/chalk": "npm:^2.2.0" "@types/node": "npm:^20.2.5" - "@uma-sdk/core": "npm:^1.2.3" + "@uma-sdk/core": "npm:^1.3.0" chalk: "npm:^5.3.0" commander: "npm:^11.0.0" eslint: "npm:^8.3.0" @@ -2519,8 +2522,9 @@ __metadata: "@types/express": "npm:^4.17.21" "@types/supertest": "npm:^2.0.14" "@types/uuid": "npm:^9.0.7" - "@uma-sdk/core": "npm:^1.2.3" + "@uma-sdk/core": "npm:^1.3.0" express: "npm:^4.18.2" + express-async-handler: "npm:^1.2.0" jest: "npm:^29.6.2" nodemon: "npm:^2.0.22" prettier: "npm:3.0.3" @@ -2688,10 +2692,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.5.6": - version: 13.5.6 - resolution: "@next/env@npm:13.5.6" - checksum: 10/c81bd6052db366407da701e4e431becbc80ef36a88bec7883b0266cdfeb45a7da959d37c38e1a816006cd2da287e5ff5b928bdb71025e3d4aa59e07dea3edd59 +"@next/env@npm:13.5.11": + version: 13.5.11 + resolution: "@next/env@npm:13.5.11" + checksum: 10/2d34ec742e28b4da54b7bfe62eb27c1c90f927f0dfe387a0af8c1a535faf75d80475e58a33472bdf722bab02349be074935e2bdf512b5fd0a46dab364700dd3d languageName: node linkType: hard @@ -2704,65 +2708,65 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-darwin-arm64@npm:13.5.6" +"@next/swc-darwin-arm64@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-darwin-arm64@npm:13.5.9" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-darwin-x64@npm:13.5.6" +"@next/swc-darwin-x64@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-darwin-x64@npm:13.5.9" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-linux-arm64-gnu@npm:13.5.6" +"@next/swc-linux-arm64-gnu@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-linux-arm64-gnu@npm:13.5.9" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-linux-arm64-musl@npm:13.5.6" +"@next/swc-linux-arm64-musl@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-linux-arm64-musl@npm:13.5.9" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-linux-x64-gnu@npm:13.5.6" +"@next/swc-linux-x64-gnu@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-linux-x64-gnu@npm:13.5.9" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-linux-x64-musl@npm:13.5.6" +"@next/swc-linux-x64-musl@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-linux-x64-musl@npm:13.5.9" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-win32-arm64-msvc@npm:13.5.6" +"@next/swc-win32-arm64-msvc@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-win32-arm64-msvc@npm:13.5.9" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-win32-ia32-msvc@npm:13.5.6" +"@next/swc-win32-ia32-msvc@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-win32-ia32-msvc@npm:13.5.9" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.5.6": - version: 13.5.6 - resolution: "@next/swc-win32-x64-msvc@npm:13.5.6" +"@next/swc-win32-x64-msvc@npm:13.5.9": + version: 13.5.9 + resolution: "@next/swc-win32-x64-msvc@npm:13.5.9" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3209,6 +3213,13 @@ __metadata: languageName: node linkType: hard +"@publint/pack@npm:^0.1.2": + version: 0.1.2 + resolution: "@publint/pack@npm:0.1.2" + checksum: 10/83e1de31ae29a0e651f7f91ebe6ad1fdf8cbb61d1eb056476586a234d05fa6fde9f34d3a0e36fbf18a2e9affa1082f758833242fd285637d303130f1a286b928 + languageName: node + linkType: hard + "@remix-run/router@npm:1.6.2": version: 1.6.2 resolution: "@remix-run/router@npm:1.6.2" @@ -4599,14 +4610,14 @@ __metadata: languageName: node linkType: hard -"@uma-sdk/core@npm:^1.2.3": - version: 1.2.3 - resolution: "@uma-sdk/core@npm:1.2.3" +"@uma-sdk/core@npm:^1.3.0": + version: 1.3.0 + resolution: "@uma-sdk/core@npm:1.3.0" dependencies: eciesjs: "npm:^0.4.4" secp256k1: "npm:^5.0.0" zod: "npm:^3.22.2" - checksum: 10/593630b7394336a214c82864709ecc3ba5071b61dfd66be582ffee0c8b322fe6bbae3b80b59130ca0fc8087e6e257d2e49cf962c95893dde40c8d830681dfdae + checksum: 10/457bf388cf1129c70fd066ca1c5b72513015e4383f7e838ba6c60c37765e18c61cedf55b30f51cda89da6aa82dfedc327f4dea46bc47d3bd5fbc5af752656b44 languageName: node linkType: hard @@ -8433,6 +8444,13 @@ __metadata: languageName: node linkType: hard +"express-async-handler@npm:^1.2.0": + version: 1.2.0 + resolution: "express-async-handler@npm:1.2.0" + checksum: 10/214a7f383db1946d0c17fa2731164082c251ce2ad173d29cb048eeaa20f27a3d36e27c9889e515780b583168ab4ceb43285260562d4abb32d8f68f8543cf6735 + languageName: node + linkType: hard + "express@npm:^4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" @@ -10592,7 +10610,7 @@ __metadata: "@octokit/auth-action": "npm:^4.0.1" octokit: "npm:^4.0.2" ts-prune: "npm:^0.10.3" - turbo: "npm:^1.13.3" + turbo: "npm:^2.4.4" languageName: unknown linkType: soft @@ -11654,6 +11672,13 @@ __metadata: languageName: node linkType: hard +"mri@npm:^1.1.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 10/6775a1d2228bb9d191ead4efc220bd6be64f943ad3afd4dcb3b3ac8fc7b87034443f666e38805df38e8d047b29f910c3cc7810da0109af83e42c82c73bd3f6bc + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -11732,20 +11757,20 @@ __metadata: languageName: node linkType: hard -"next@npm:^13.4.19": - version: 13.5.6 - resolution: "next@npm:13.5.6" +"next@npm:^13.5.10": + version: 13.5.11 + resolution: "next@npm:13.5.11" dependencies: - "@next/env": "npm:13.5.6" - "@next/swc-darwin-arm64": "npm:13.5.6" - "@next/swc-darwin-x64": "npm:13.5.6" - "@next/swc-linux-arm64-gnu": "npm:13.5.6" - "@next/swc-linux-arm64-musl": "npm:13.5.6" - "@next/swc-linux-x64-gnu": "npm:13.5.6" - "@next/swc-linux-x64-musl": "npm:13.5.6" - "@next/swc-win32-arm64-msvc": "npm:13.5.6" - "@next/swc-win32-ia32-msvc": "npm:13.5.6" - "@next/swc-win32-x64-msvc": "npm:13.5.6" + "@next/env": "npm:13.5.11" + "@next/swc-darwin-arm64": "npm:13.5.9" + "@next/swc-darwin-x64": "npm:13.5.9" + "@next/swc-linux-arm64-gnu": "npm:13.5.9" + "@next/swc-linux-arm64-musl": "npm:13.5.9" + "@next/swc-linux-x64-gnu": "npm:13.5.9" + "@next/swc-linux-x64-musl": "npm:13.5.9" + "@next/swc-win32-arm64-msvc": "npm:13.5.9" + "@next/swc-win32-ia32-msvc": "npm:13.5.9" + "@next/swc-win32-x64-msvc": "npm:13.5.9" "@swc/helpers": "npm:0.5.2" busboy: "npm:1.6.0" caniuse-lite: "npm:^1.0.30001406" @@ -11783,7 +11808,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/ec6defc7958b575d93306a2dcb05b7b14e27474e226ec350f412d03832407a5ae7139c21f7c7437b93cca2c8a79d7197c468308d82a903b2a86d579f84ccac9f + checksum: 10/9f116a7391bb81b6c7b96b825b68be2be849ef3ac85a6b9d0939f0ca8e0b5aa1b53db2a96f6b44fdf0b3d31ea0a2b7150218ed033660b99780f820db1baba709 languageName: node linkType: hard @@ -12290,6 +12315,15 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^0.2.9": + version: 0.2.11 + resolution: "package-manager-detector@npm:0.2.11" + dependencies: + quansync: "npm:^0.2.7" + checksum: 10/2c1a8da0e5895f0be06a8e1f4b4336fb78a19167ca3932dbaeca7260f948e67cf53b32585a13f8108341e7a468b38b4f2a8afc7b11691cb2d856ecd759d570fb + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -12472,6 +12506,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -12828,6 +12869,20 @@ __metadata: languageName: node linkType: hard +"publint@npm:^0.3.9": + version: 0.3.9 + resolution: "publint@npm:0.3.9" + dependencies: + "@publint/pack": "npm:^0.1.2" + package-manager-detector: "npm:^0.2.9" + picocolors: "npm:^1.1.1" + sade: "npm:^1.8.1" + bin: + publint: src/cli.js + checksum: 10/bfa96a78e38df964422602d22afd52561a47591bc5e06ffe39e0029ee0953205ad379506dd643dfbbf89f443b7ae753150b5e6cc03872c2fc852f9a755533c48 + languageName: node + linkType: hard + "punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -12878,6 +12933,13 @@ __metadata: languageName: node linkType: hard +"quansync@npm:^0.2.7": + version: 0.2.10 + resolution: "quansync@npm:0.2.10" + checksum: 10/b54d955de867e104025f2666d52b2b67befe4e0f184a96acc9adcbdc572e46dce49c69d1e79f99413beae8a974a576383806a05f85f9a826865dc589ee1bcaf2 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -13720,6 +13782,15 @@ __metadata: languageName: node linkType: hard +"sade@npm:^1.8.1": + version: 1.8.1 + resolution: "sade@npm:1.8.1" + dependencies: + mri: "npm:^1.1.0" + checksum: 10/1c67ba03c94083e0ae307ff5564ecb86c2104c0f558042fdaa40ea0054f91a63a9783f14069870f2f784336adabb70f90f22a84dc457b5a25e859aaadefe0910 + languageName: node + linkType: hard + "safe-array-concat@npm:^1.0.1": version: 1.0.1 resolution: "safe-array-concat@npm:1.0.1" @@ -15106,58 +15177,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-darwin-64@npm:1.13.3" +"turbo-darwin-64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-darwin-64@npm:2.4.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-darwin-arm64@npm:1.13.3" +"turbo-darwin-arm64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-darwin-arm64@npm:2.4.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-linux-64@npm:1.13.3" +"turbo-linux-64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-linux-64@npm:2.4.4" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-linux-arm64@npm:1.13.3" +"turbo-linux-arm64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-linux-arm64@npm:2.4.4" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-windows-64@npm:1.13.3" +"turbo-windows-64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-windows-64@npm:2.4.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:1.13.3": - version: 1.13.3 - resolution: "turbo-windows-arm64@npm:1.13.3" +"turbo-windows-arm64@npm:2.4.4": + version: 2.4.4 + resolution: "turbo-windows-arm64@npm:2.4.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^1.13.3": - version: 1.13.3 - resolution: "turbo@npm:1.13.3" - dependencies: - turbo-darwin-64: "npm:1.13.3" - turbo-darwin-arm64: "npm:1.13.3" - turbo-linux-64: "npm:1.13.3" - turbo-linux-arm64: "npm:1.13.3" - turbo-windows-64: "npm:1.13.3" - turbo-windows-arm64: "npm:1.13.3" +"turbo@npm:^2.4.4": + version: 2.4.4 + resolution: "turbo@npm:2.4.4" + dependencies: + turbo-darwin-64: "npm:2.4.4" + turbo-darwin-arm64: "npm:2.4.4" + turbo-linux-64: "npm:2.4.4" + turbo-linux-arm64: "npm:2.4.4" + turbo-windows-64: "npm:2.4.4" + turbo-windows-arm64: "npm:2.4.4" dependenciesMeta: turbo-darwin-64: optional: true @@ -15173,7 +15244,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/de96309b3c4c28b51d9436cdec57e3df452dea4ec4512e6c3b7f6596265d4b22d2c16b9c8a674cc1bafa3cc2eea9feb89a807526d6d820f9bca0522d8d86df12 + checksum: 10/ea60911d280e85068ec31c58cf079e5eb423db9dfa3fc1f1fdb5456debb0c6395ef20040384b4d6400e22cfbc1590381a61c4be1bdc7a06bbdf6b9b92573c42a languageName: node linkType: hard