From b54e567196adc9e23151074ac7674779badfe4cf Mon Sep 17 00:00:00 2001 From: Matthew Rheaume Date: Mon, 8 Dec 2025 18:11:18 -0500 Subject: [PATCH 01/19] [trivial] Updated description for `tasks.site`. (#22343) ## Reason This was copied from minikube so they were rendering on the same line in shell completion. ## Test Plan N/A. GitOrigin-RevId: ba66c0264c979d30eea5a1ee8ae14477b0b278d1 --- .mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mise.toml b/.mise.toml index f1755ec6d..8f90efb7e 100644 --- a/.mise.toml +++ b/.mise.toml @@ -45,7 +45,7 @@ description = "Start ops frontend connected to minikube" run = "VITE_PROXY_TARGET=http://app.minikube.local yarn start ops" [tasks.site] -description = "Start CN2 frontend connected to minikube" +description = "Start CN2 frontend development server" run = "yarn start site" [tasks.site-minikube] From cd16434e02116409dfc64343f2eb6062adc18230 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Mon, 8 Dec 2025 17:18:47 -0800 Subject: [PATCH 02/19] [umaaas] add send and receive failure reason filters for operations (#22309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason Sofi wants the ability to filter operations by failure reason ## Overview - since we have separate enums for send and receive operation failure reasons, I split it into two filters - makes send/receive specific queries if send/receive failure reason filter is present - common filter logic is abstracted - adds callback to DataManagerTable to handle reacting to filter state changes - makes "type" and failure reason filters mutually exclusive since it doesn't make sense to have multiple applied at the same time - updates unit tests ## Test Plan - unit tests - works locally ![Screenshot 2025-12-05 at 5.28.58 PM.png](https://app.graphite.com/user-attachments/assets/a686b759-0037-45b3-b54e-b3debc6030b8.png) ![Screenshot 2025-12-05 at 5.29.02 PM.png](https://app.graphite.com/user-attachments/assets/1cbbcc5f-89e6-4b32-aeb9-9487c397108c.png) GitOrigin-RevId: f74ca29c2593d95f55b52ce518cc26e9ad8cf1a3 --- .../DataManagerTable/DataManagerTable.tsx | 195 +++++++++--------- 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx b/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx index 429081f78..962162474 100644 --- a/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx +++ b/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx @@ -94,6 +94,15 @@ interface FilterOptions< ) => QueryVariablesType; refetch: (fetchVariables: QueryVariablesType) => Promise; initialQueryVariables: QueryVariablesType; + /** + * Called when a filter state changes. + * Use setFilterStates to modify other filters (e.g., for mutual exclusivity). + */ + onFilterStateChange?: ( + changedKey: keyof T, + newState: FilterState, + setFilterStates: Dispatch>>, + ) => void; } interface ShowMoreOptions { @@ -860,6 +869,19 @@ export function DataManagerTable< } }; + const createFilterStateUpdater = + (filter: Filter) => (state: FilterState) => { + setFilterStates((prevState) => ({ + ...prevState, + [filter.accessorKey]: state, + })); + props.filterOptions?.onFilterStateChange?.( + filter.accessorKey, + state, + setFilterStates, + ); + }; + const filterSections = props.filterOptions ? props.filterOptions.filters.map((filter: Filter) => { switch (filter.type) { @@ -867,12 +889,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} state={filterStates[filter.accessorKey] as DateFilterState} />
@@ -881,12 +898,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} options={filter.enumValues} label={filter.label} placeholder={filter.placeholder} @@ -899,12 +911,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} placeholder={filter.placeholder} state={filterStates[filter.accessorKey] as StringFilterState} @@ -915,12 +922,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} placeholder={filter.placeholder} state={filterStates[filter.accessorKey] as IdFilterState} @@ -931,12 +933,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} state={filterStates[filter.accessorKey] as BooleanFilterState} /> @@ -946,12 +943,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} state={ filterStates[filter.accessorKey] as CurrencyFilterState @@ -1087,8 +1079,20 @@ export function DataManagerTable< }} onUpdateFilter={(newFilterState) => { setFilterStates((prevStates) => { - const newStates = { ...prevStates }; + let newStates = { ...prevStates }; newStates[filter.accessorKey] = newFilterState; + // Call onFilterStateChange to allow mutual exclusivity logic + props.filterOptions?.onFilterStateChange?.( + filter.accessorKey, + newFilterState, + (updater) => { + if (typeof updater === "function") { + newStates = updater(newStates); + } else { + newStates = updater; + } + }, + ); void handleApplyFilters( newStates, props.filterOptions!, @@ -1119,6 +1123,7 @@ export function DataManagerTable< setShowFilterEditor, customDropdownComponent: props.customComponents?.dropdownComponent, + onFilterStateChange: props.filterOptions.onFilterStateChange, })} isOpen={showFilterEditor} onOpen={() => { @@ -1411,6 +1416,7 @@ function getPillDropdownItems< pageSize, setShowFilterEditor, customDropdownComponent, + onFilterStateChange, }: { filterOptions: FilterOptions; filterStates: DataManagerTableState; @@ -1425,7 +1431,31 @@ function getPillDropdownItems< customDropdownComponent?: | React.ComponentType> | undefined; + onFilterStateChange?: FilterOptions< + T, + QueryVariablesType, + QueryResultType + >["onFilterStateChange"]; }) { + // Helper to apply a new filter state with mutual exclusivity support + const applyFilterState = (filter: Filter, newFilterState: FilterState) => { + setFilterStates((prevStates: DataManagerTableState) => { + let newStates = { ...prevStates }; + newStates[filter.accessorKey] = newFilterState; + // Call onFilterStateChange for mutual exclusivity logic + onFilterStateChange?.(filter.accessorKey, newFilterState, (updater) => { + if (typeof updater === "function") { + newStates = updater(newStates); + } else { + newStates = updater; + } + }); + void handleApplyFilters(newStates, filterOptions, pageSize); + setShowFilterEditor(false); + return newStates; + }); + }; + const getDropdownItemForFilter = (filter: Filter): DropdownItemType => { let filterSubDropdownOptions: DropdownItemType[] = []; if (filter.type === FilterType.ENUM) { @@ -1448,18 +1478,12 @@ function getPillDropdownItems< updatedAppliedValues = [...optionValues]; } - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); }, })); } @@ -1487,30 +1511,18 @@ function getPillDropdownItems< ? [...state.appliedValues] : []; - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); } else if (filter.type === FilterType.BOOLEAN) { - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: filter.value as boolean, - isApplied: true, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: filter.value as boolean, + isApplied: true, + } as unknown as FilterState); } else if (filter.type === FilterType.ID) { const state = filterStates[filter.accessorKey] as IdFilterState; let updatedAppliedValues: string[] = []; @@ -1524,34 +1536,23 @@ function getPillDropdownItems< : state.appliedValues ? [...state.appliedValues] : []; - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); } else if (filter.type === FilterType.CURRENCY) { // TODO: Currency filter state, design tbd } else if (filter.type === FilterType.DATE) { - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - preset: DatePreset.Custom, - start: null, - end: null, - isApplied: true, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + preset: DatePreset.Custom, + start: null, + end: null, + isApplied: true, + } as unknown as FilterState); } }, subDropdown: From 25f1d8d9c617c7f3757c7cb564796a6bbc9db8ac Mon Sep 17 00:00:00 2001 From: Aaryaman Bhute <35084309+AaryamanBhute@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:55:22 -0800 Subject: [PATCH 03/19] [Bridge] Update phone number regex to accept 7-15 digits (#22382) ## Reason The current phone number validation regex is too restrictive, only accepting US 10-digit phone numbers that start with digits 2-9. This prevents international phone numbers and other valid formats from being accepted. ## Overview Updated the phone number validation regex to accept phone numbers with 7-15 digits, accommodating international phone number formats and a wider range of valid phone numbers. ## Test Plan Tested by verifying that: - Valid international phone numbers with varying lengths (7-15 digits) are now accepted - Phone numbers with fewer than 7 or more than 15 digits are rejected - The form validation works correctly with the new regex pattern GitOrigin-RevId: 4ca3c2f5cd975b5760cf67dcd8677c88a0594c10 --- packages/ui/src/hooks/useFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/hooks/useFields.tsx b/packages/ui/src/hooks/useFields.tsx index db9b67552..567009b22 100644 --- a/packages/ui/src/hooks/useFields.tsx +++ b/packages/ui/src/hooks/useFields.tsx @@ -36,7 +36,7 @@ const defaultMsgs = { }; const regexp = { - phone: /^[2-9]{1}[0-9]{9}$/, + phone: /^[0-9]{7,15}$/, postalCode: /(^\d{5}$)|(^\d{5}-\d{4}$)/, email: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, state: From bc3600acd3e880c683dce1e7659dd821f31a356e Mon Sep 17 00:00:00 2001 From: Matthew Rheaume Date: Wed, 10 Dec 2025 14:50:23 -0500 Subject: [PATCH 04/19] Added `failHtlcs` to `LightsparkClient`. (#22393) ## Reason Shakepay needs this for remote signing so that they can fail back HTLCs that don't wish to claim. ## Test Plan Unit tests. GitOrigin-RevId: 846ac06b9ceb53bb9512dd2025219e3006f2be3e --- packages/lightspark-sdk/src/client.ts | 25 +++++++++++++++++++ .../lightspark-sdk/src/graphql/FailHtlcs.ts | 19 ++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/lightspark-sdk/src/graphql/FailHtlcs.ts diff --git a/packages/lightspark-sdk/src/client.ts b/packages/lightspark-sdk/src/client.ts index f54ff438d..d791de69f 100644 --- a/packages/lightspark-sdk/src/client.ts +++ b/packages/lightspark-sdk/src/client.ts @@ -45,6 +45,7 @@ import { CreateUmaInvitationWithPayment } from "./graphql/CreateUmaInvitationWit import { CreateUmaInvoice } from "./graphql/CreateUmaInvoice.js"; import { DecodeInvoice } from "./graphql/DecodeInvoice.js"; import { DeleteApiToken } from "./graphql/DeleteApiToken.js"; +import { FailHtlcs } from "./graphql/FailHtlcs.js"; import { FetchUmaInvitation } from "./graphql/FetchUmaInvitation.js"; import { FundNode } from "./graphql/FundNode.js"; import { IncomingPaymentsForInvoice } from "./graphql/IncomingPaymentsForInvoice.js"; @@ -76,6 +77,8 @@ import type ComplianceProvider from "./objects/ComplianceProvider.js"; import type CreateApiTokenOutput from "./objects/CreateApiTokenOutput.js"; import type CurrencyAmount from "./objects/CurrencyAmount.js"; import { CurrencyAmountFromJson } from "./objects/CurrencyAmount.js"; +import type FailHtlcsOutput from "./objects/FailHtlcsOutput.js"; +import { FailHtlcsOutputFromJson } from "./objects/FailHtlcsOutput.js"; import type FeeEstimate from "./objects/FeeEstimate.js"; import { FeeEstimateFromJson } from "./objects/FeeEstimate.js"; import type IncomingPayment from "./objects/IncomingPayment.js"; @@ -736,6 +739,28 @@ class LightsparkClient { return InvoiceFromJson(invoiceJson); } + /** + * Fails all pending HTLCs (Hash Time Locked Contracts) for a given invoice. + * + * @param invoiceId The ID of the invoice for which to fail HTLCs. + * @param cancelInvoice Whether to also cancel the invoice after failing the HTLCs. + * @returns The output containing the invoice ID, or undefined if the operation failed. + */ + public async failHtlcs( + invoiceId: string, + cancelInvoice: boolean, + ): Promise { + const response = await this.requester.makeRawRequest(FailHtlcs, { + invoice_id: invoiceId, + cancel_invoice: cancelInvoice, + }); + const output = response.fail_htlcs; + if (!output) { + return undefined; + } + return FailHtlcsOutputFromJson(output); + } + /** * Decodes an encoded lightning invoice string. * diff --git a/packages/lightspark-sdk/src/graphql/FailHtlcs.ts b/packages/lightspark-sdk/src/graphql/FailHtlcs.ts new file mode 100644 index 000000000..a3f14c24f --- /dev/null +++ b/packages/lightspark-sdk/src/graphql/FailHtlcs.ts @@ -0,0 +1,19 @@ +// Copyright ©, 2023-present, Lightspark Group, Inc. - All Rights Reserved + +import { FRAGMENT as FailHtlcsOutputFragment } from "../objects/FailHtlcsOutput.js"; + +export const FailHtlcs = ` + mutation FailHtlcs( + $invoice_id: ID!, + $cancel_invoice: Boolean! + ) { + fail_htlcs(input: { + invoice_id: $invoice_id, + cancel_invoice: $cancel_invoice + }) { + ...FailHtlcsOutputFragment + } + } + +${FailHtlcsOutputFragment} +`; From 8568a2bafb0ba91e6b963a6f8cf82be5bfdd83ee Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Wed, 10 Dec 2025 14:32:38 -0800 Subject: [PATCH 05/19] [ops] add hardcore mode easter egg (#22425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason No reason [https://app.graphite.com/github/pr/lightsparkdev/webdev/22418/[ops]-add-core-service-select-in-header-instead-of-toggles#comment-PRRC_kwDOHaFZpc6bdfkz](https://app.graphite.com/github/pr/lightsparkdev/webdev/22418/%5Bops%5D-add-core-service-select-in-header-instead-of-toggles#comment-PRRC_kwDOHaFZpc6bdfkz) ![Screenshot 2025-12-10 at 2.10.30 PM.png](https://app.graphite.com/user-attachments/assets/aba43d46-4610-40c8-9e46-5aa43deaf8f2.png) ![Screenshot 2025-12-10 at 2.10.57 PM.png](https://app.graphite.com/user-attachments/assets/2558a738-ccdc-4f2b-82b8-d13ce555095e.png) GitOrigin-RevId: b23d54f17da86fbe14386c1d3b7e554caaa261e8 --- packages/ui/src/styles/colors.tsx | 8 + .../ui/src/styles/themeDefaults/buttons.tsx | 2 +- .../ui/src/styles/themeDefaults/cardForm.tsx | 2 +- packages/ui/src/styles/themes.tsx | 97 ++++ packages/ui/src/styles/tokens/typography.ts | 461 ++++++++++++++++++ 5 files changed, 568 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/styles/colors.tsx b/packages/ui/src/styles/colors.tsx index c4fba9b31..7f532a1a3 100644 --- a/packages/ui/src/styles/colors.tsx +++ b/packages/ui/src/styles/colors.tsx @@ -188,6 +188,14 @@ const baseColors = { transparent: "transparent", transparenta02: "#00000005", transparenta08: "#00000014", + // hardcore radioactive green + radioactiveGreen: "#39FF14", + radioactiveGreenDark: "#2ECC0F", + radioactiveGreenDarker: "#23990B", + radioactiveGreenLight: "#5FFF45", + radioactiveGreen10: "#39FF1419", + radioactiveGreen20: "#39FF1433", + radioactiveGreen30: "#39FF144D", } as const; /* We only want `as const` to affect keys, the values should be widened to strings: */ diff --git a/packages/ui/src/styles/themeDefaults/buttons.tsx b/packages/ui/src/styles/themeDefaults/buttons.tsx index 85fd06323..c77c1014a 100644 --- a/packages/ui/src/styles/themeDefaults/buttons.tsx +++ b/packages/ui/src/styles/themeDefaults/buttons.tsx @@ -5,7 +5,7 @@ import { type TokenSizeKey } from "../tokens/typography.js"; export type PaddingYKey = "short" | "regular"; export type PaddingY = number | { [key in PaddingYKey]: number }; -export const buttonBorderRadiuses = [4, 8, 32, 999, "100%"] as const; +export const buttonBorderRadiuses = [0, 4, 8, 32, 999, "100%"] as const; export type ButtonBorderRadius = (typeof buttonBorderRadiuses)[number]; export const allowedButtonTypographyTypes = [ "Body", diff --git a/packages/ui/src/styles/themeDefaults/cardForm.tsx b/packages/ui/src/styles/themeDefaults/cardForm.tsx index e714f9480..1f71d4db7 100644 --- a/packages/ui/src/styles/themeDefaults/cardForm.tsx +++ b/packages/ui/src/styles/themeDefaults/cardForm.tsx @@ -7,7 +7,7 @@ export type CardFormPaddingX = 0 | 24 | 32 | 40 | 56; export type CardFormPaddingTop = 0 | 24 | 32 | 40 | 56; export type CardFormPaddingBottom = 0 | 24 | 32 | 40 | 56 | 64; export type CardFormBorderWidth = 0 | 1; -export type CardFormBorderRadius = 8 | 24 | 32; +export type CardFormBorderRadius = 0 | 8 | 24 | 32; export type CardFormShadow = "soft" | "hard" | "none"; export type CardFormTextAlign = "center" | "left"; export type CardFormBorderColor = "vlcNeutral" | "grayBlue94" | "gray3"; diff --git a/packages/ui/src/styles/themes.tsx b/packages/ui/src/styles/themes.tsx index 8b34de430..1ec31b30a 100644 --- a/packages/ui/src/styles/themes.tsx +++ b/packages/ui/src/styles/themes.tsx @@ -41,6 +41,7 @@ export enum Themes { BridgeDark = "bridgeDark", NageLight = "nageLight", NageDark = "nageDark", + Hardcore = "hardcore", } export function isTheme(theme: unknown): theme is Themes { @@ -869,6 +870,100 @@ const nageDarkTheme = extend(darkTheme, { type: Themes.NageDark, }); +const hardcoreButtons = merge(buttonsThemeBase, { + defaultTypographyType: "Title", + defaultSize: "Medium", + defaultBorderRadius: 0, + kinds: { + primary: { + defaultColor: "black", + defaultBackgroundColor: "radioactiveGreen", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreenLight", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "radioactiveGreenDark", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + secondary: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "transparent", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreen10", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "radioactiveGreen20", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + tertiary: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "gray10", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "gray15", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "gray20", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + ghost: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "transparent", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreen10", + defaultHoverBorderColor: "radioactiveGreenLight", + }, + danger: { + defaultColor: "black", + defaultBackgroundColor: "danger", + defaultBorderColor: "danger", + defaultHoverBackgroundColor: "red50", + defaultHoverBorderColor: "red50", + }, + }, +}); + +const hardcoreBaseTheme: BaseTheme = { + ...darkBaseTheme, + type: Themes.Hardcore, + bg: colors.black, + smBg: colors.black, + text: colors.radioactiveGreen, + hcNeutral: colors.radioactiveGreen, + mcNeutral: colors.radioactiveGreenDark, + lcNeutral: colors.radioactiveGreenDarker, + vlcNeutral: colors.gray15, + secondary: colors.radioactiveGreenDark, + tertiary: colors.radioactiveGreen, + primary: colors.radioactiveGreen, + link: colors.radioactiveGreenLight, + linkLight: colors.radioactiveGreen10, + success: colors.radioactiveGreen, + info: colors.radioactiveGreen, + c05Neutral: colors.radioactiveGreenDarker, + c1Neutral: colors.radioactiveGreenDarker, + c15Neutral: colors.radioactiveGreenDark, + c2Neutral: colors.radioactiveGreenDark, + c3Neutral: colors.radioactiveGreen, + c4Neutral: colors.radioactiveGreenLight, + border: colors.radioactiveGreen30, + inputBackground: colors.black, + typography: getTypography(TypographyGroup.Hardcore, { + main: "Roboto Mono", + code: "Roboto Mono", + }), + buttons: hardcoreButtons, + badge: { + bg: "c15Neutral" as ThemeOrColorKey, + }, +}; + +const hardcoreTheme = extend(hardcoreBaseTheme, { + header: extendBase(hardcoreBaseTheme, {}), + nav: extendBase(hardcoreBaseTheme, {}), + content: extendBase(hardcoreBaseTheme, {}), + controls: extendBase(hardcoreBaseTheme, { + bg: colors.gray10, + smBg: colors.gray10, + }), +}); + export const themes = { [Themes.Light]: lightTheme, [Themes.Dark]: darkTheme, @@ -882,6 +977,7 @@ export const themes = { [Themes.BridgeDark]: bridgeDarkTheme, [Themes.NageLight]: nageLightTheme, [Themes.NageDark]: nageDarkTheme, + [Themes.Hardcore]: hardcoreTheme, } as const; function extend(obj: BaseTheme, rest: Partial) { @@ -1000,6 +1096,7 @@ export const isDark = (theme: Theme) => Themes.LightsparkDocsDark, Themes.UmameDocsDark, Themes.UmaAuthSdkDark, + Themes.Hardcore, ].includes(theme.type); export const isLight = (theme: Theme) => [ diff --git a/packages/ui/src/styles/tokens/typography.ts b/packages/ui/src/styles/tokens/typography.ts index 22e75bde7..e96ae04e5 100644 --- a/packages/ui/src/styles/tokens/typography.ts +++ b/packages/ui/src/styles/tokens/typography.ts @@ -8,6 +8,7 @@ export enum TypographyGroup { UmameDocs = "UmameDocs", Bridge = "Bridge", Nage = "Nage", + Hardcore = "Hardcore", } export type TypographyTokenGroups = ReturnType; @@ -109,6 +110,22 @@ const LINE_HEIGHTS = { xl: "24px", xs: "16px", }, + [TypographyGroup.Hardcore]: { + "8xl": "72px", + "7xl": "60px", + "6xl": "44px", + "5xl": "40px", + "4xl": "36px", + "3xl": "32px", + "3xs": "12px", + "2xs": "14px", + "2xl": "28px", + lg: "22px", + md: "20px", + sm: "18px", + xl: "24px", + xs: "16px", + }, } as const; export type LineHeightKey = keyof (typeof LINE_HEIGHTS)[TypographyGroup]; @@ -218,6 +235,19 @@ const FONT_SIZE = { "6xl": "48px", "7xl": "64px", }, + [TypographyGroup.Hardcore]: { + "2xs": "10px", + xs: "12px", + md: "14px", + lg: "16px", + xl: "20px", + "2xl": "24px", + "3xl": "28px", + "4xl": "32px", + "5xl": "36px", + "6xl": "48px", + "7xl": "64px", + }, }; const LETTER_SPACING = { @@ -259,6 +289,11 @@ const LETTER_SPACING = { wide: "0.2px", wider: "0.7px", }, + [TypographyGroup.Hardcore]: { + tight: "-0.2px", + normal: "0", + wide: "0.2px", + }, }; const TEXT_CASE = { @@ -5433,6 +5468,432 @@ function getTypographyTypes(fontFamilies: FontFamilies): TypographyTypes { }, }, }, + [TypographyGroup.Hardcore]: { + Mobile: { + [TypographyType.Display]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["7xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["6xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["6xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["5xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["5xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["4xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Headline]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["4xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["3xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["3xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["2xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Title]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Body]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Label]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xs"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Code]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + }, + Desktop: { + [TypographyType.Display]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["8xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["7xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["7xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["6xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["6xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["5xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Headline]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["5xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["4xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["4xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["3xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["3xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Title]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["2xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Body]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Label]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Code]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + }, + }, }; } From 06eb143cb7768ad3fac43bcc3e010b58d0fe5b64 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Fri, 12 Dec 2025 11:29:55 -0800 Subject: [PATCH 06/19] [striga] fix verify-identity modal not being scrollable (#22469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason sumsub kyc modal was overflowing and wasn't scrollable completely ## Overview - makes Drawer more scrollable and fixes paddings (unrelated) - fixes kycsumsubmodal scrolling ## Test Plan ![Screenshot 2025-12-12 at 11.12.14 AM.png](https://app.graphite.com/user-attachments/assets/567e1867-1c78-406a-bd19-9d5d6cadaa23.png) GitOrigin-RevId: 5f1f9e30d7bca379c65fe1414722d0bd9de418ff --- packages/ui/src/components/Drawer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Drawer.tsx b/packages/ui/src/components/Drawer.tsx index 7482bbfa8..355792c6b 100644 --- a/packages/ui/src/components/Drawer.tsx +++ b/packages/ui/src/components/Drawer.tsx @@ -190,6 +190,7 @@ const DrawerContainer = styled.div<{ display: flex; flex-direction: column; align-items: center; + overflow: scroll; ${(props) => props.top && `top: ${props.top}px;`} @@ -234,7 +235,9 @@ const DrawerInnerContainer = styled.div<{ width: ${(props) => props.kind === "floating" ? `calc(100% - ${Spacing.md * 2}px)` : "100%"}; min-width: 320px; - ${(props) => (props.kind === "floating" ? `bottom: ${Spacing.px.md};` : "")} + ${(props) => (props.kind === "floating" ? `bottom: 0;` : "")} + margin-bottom: ${Spacing.px.md}; + margin-top: ${Spacing.px.md}; height: auto; border-radius: ${(props) => props.kind === "floating" From 88fd808a8f15af073cec9f523f6062a2226b0e1d Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 15 Dec 2025 11:03:59 -0800 Subject: [PATCH 07/19] Add new currencies, mwk, xof, xaf, and rwf (#22501) ## Reason Add 4 new African currencies GitOrigin-RevId: a5d4983b76a0522ebca873576b349ee047a0cb60 --- packages/core/src/utils/currency.ts | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 45381bbca..53c79abca 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -39,6 +39,10 @@ export const CurrencyUnit = { TZS: "TZS", UGX: "UGX", BWP: "BWP", + XOF: "XOF", + XAF: "XAF", + MWK: "MWK", + RWF: "RWF", USDT: "USDT", USDC: "USDC", @@ -102,6 +106,10 @@ const standardUnitConversionObj = { [CurrencyUnit.TZS]: (v: number) => v, [CurrencyUnit.UGX]: (v: number) => v, [CurrencyUnit.BWP]: (v: number) => v, + [CurrencyUnit.XOF]: (v: number) => v, + [CurrencyUnit.XAF]: (v: number) => v, + [CurrencyUnit.MWK]: (v: number) => v, + [CurrencyUnit.RWF]: (v: number) => v, [CurrencyUnit.USDT]: (v: number) => v, [CurrencyUnit.USDC]: (v: number) => v, }; @@ -149,6 +157,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toBitcoinConversion, [CurrencyUnit.UGX]: toBitcoinConversion, [CurrencyUnit.BWP]: toBitcoinConversion, + [CurrencyUnit.XOF]: toBitcoinConversion, + [CurrencyUnit.XAF]: toBitcoinConversion, + [CurrencyUnit.MWK]: toBitcoinConversion, + [CurrencyUnit.RWF]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -180,6 +192,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMicrobitcoinConversion, [CurrencyUnit.UGX]: toMicrobitcoinConversion, [CurrencyUnit.BWP]: toMicrobitcoinConversion, + [CurrencyUnit.XOF]: toMicrobitcoinConversion, + [CurrencyUnit.XAF]: toMicrobitcoinConversion, + [CurrencyUnit.MWK]: toMicrobitcoinConversion, + [CurrencyUnit.RWF]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -211,6 +227,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMillibitcoinConversion, [CurrencyUnit.UGX]: toMillibitcoinConversion, [CurrencyUnit.BWP]: toMillibitcoinConversion, + [CurrencyUnit.XOF]: toMillibitcoinConversion, + [CurrencyUnit.XAF]: toMillibitcoinConversion, + [CurrencyUnit.MWK]: toMillibitcoinConversion, + [CurrencyUnit.RWF]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -242,6 +262,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMillisatoshiConversion, [CurrencyUnit.UGX]: toMillisatoshiConversion, [CurrencyUnit.BWP]: toMillisatoshiConversion, + [CurrencyUnit.XOF]: toMillisatoshiConversion, + [CurrencyUnit.XAF]: toMillisatoshiConversion, + [CurrencyUnit.MWK]: toMillisatoshiConversion, + [CurrencyUnit.RWF]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -273,6 +297,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toNanobitcoinConversion, [CurrencyUnit.UGX]: toNanobitcoinConversion, [CurrencyUnit.BWP]: toNanobitcoinConversion, + [CurrencyUnit.XOF]: toNanobitcoinConversion, + [CurrencyUnit.XAF]: toNanobitcoinConversion, + [CurrencyUnit.MWK]: toNanobitcoinConversion, + [CurrencyUnit.RWF]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -304,6 +332,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toSatoshiConversion, [CurrencyUnit.UGX]: toSatoshiConversion, [CurrencyUnit.BWP]: toSatoshiConversion, + [CurrencyUnit.XOF]: toSatoshiConversion, + [CurrencyUnit.XAF]: toSatoshiConversion, + [CurrencyUnit.MWK]: toSatoshiConversion, + [CurrencyUnit.RWF]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -328,6 +360,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: standardUnitConversionObj, [CurrencyUnit.UGX]: standardUnitConversionObj, [CurrencyUnit.BWP]: standardUnitConversionObj, + [CurrencyUnit.XOF]: standardUnitConversionObj, + [CurrencyUnit.XAF]: standardUnitConversionObj, + [CurrencyUnit.MWK]: standardUnitConversionObj, + [CurrencyUnit.RWF]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -412,6 +448,10 @@ export type CurrencyMap = { [CurrencyUnit.TZS]: number; [CurrencyUnit.UGX]: number; [CurrencyUnit.BWP]: number; + [CurrencyUnit.XOF]: number; + [CurrencyUnit.XAF]: number; + [CurrencyUnit.MWK]: number; + [CurrencyUnit.RWF]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -446,6 +486,10 @@ export type CurrencyMap = { [CurrencyUnit.TZS]: string; [CurrencyUnit.UGX]: string; [CurrencyUnit.BWP]: string; + [CurrencyUnit.XOF]: string; + [CurrencyUnit.XAF]: string; + [CurrencyUnit.MWK]: string; + [CurrencyUnit.RWF]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -661,6 +705,10 @@ function convertCurrencyAmountValues( tzs: CurrencyUnit.TZS, ugx: CurrencyUnit.UGX, bwp: CurrencyUnit.BWP, + xof: CurrencyUnit.XOF, + xaf: CurrencyUnit.XAF, + mwk: CurrencyUnit.MWK, + rwf: CurrencyUnit.RWF, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -740,6 +788,10 @@ export function mapCurrencyAmount( tzs, ugx, bwp, + xof, + xaf, + mwk, + rwf, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -769,6 +821,10 @@ export function mapCurrencyAmount( [CurrencyUnit.TZS]: tzs, [CurrencyUnit.UGX]: ugx, [CurrencyUnit.BWP]: bwp, + [CurrencyUnit.XOF]: xof, + [CurrencyUnit.XAF]: xaf, + [CurrencyUnit.MWK]: mwk, + [CurrencyUnit.RWF]: rwf, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -884,6 +940,22 @@ export function mapCurrencyAmount( value: bwp, unit: CurrencyUnit.BWP, }), + [CurrencyUnit.XOF]: formatCurrencyStr({ + value: xof, + unit: CurrencyUnit.XOF, + }), + [CurrencyUnit.XAF]: formatCurrencyStr({ + value: xaf, + unit: CurrencyUnit.XAF, + }), + [CurrencyUnit.MWK]: formatCurrencyStr({ + value: mwk, + unit: CurrencyUnit.MWK, + }), + [CurrencyUnit.RWF]: formatCurrencyStr({ + value: rwf, + unit: CurrencyUnit.RWF, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1006,6 +1078,14 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "UGX"; case CurrencyUnit.BWP: return "BWP"; + case CurrencyUnit.XOF: + return "XOF"; + case CurrencyUnit.XAF: + return "XAF"; + case CurrencyUnit.MWK: + return "MWK"; + case CurrencyUnit.RWF: + return "RWF"; } return "Unsupported CurrencyUnit"; }; From 0b6cef3e64acbf73c87b6a1d800288ef6c61e75a Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Mon, 15 Dec 2025 15:15:40 -0800 Subject: [PATCH 08/19] [uma-bridge] fix overflowing email verification button text (#22532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason we were seeing long button text stretch the button past modal screens ## Overview - adds overflow hidden to button so that ellipsis works properly for full width - updates translations for "Open email app" so that it fits ## Test Plan ![Screenshot 2025-12-15 at 3.00.30 PM.png](https://app.graphite.com/user-attachments/assets/ec7f3f35-f588-402a-a93c-8d59fd8bc03a.png) GitOrigin-RevId: 3426847249ce63cc99bab48c8a4d41036140e6bb --- packages/ui/src/components/Button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index 044ff526b..18aa808ec 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -340,6 +340,7 @@ export const Button = forwardRef< alignItems: "center", justifyContent, gap: gap ? `${gap}px` : undefined, + overflow: "hidden", }} > {iconSide === "left" && currentIcon} From 87f0ed9384af32492b0e4208e30f4926fe0d13bd Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Mon, 15 Dec 2025 16:12:06 -0800 Subject: [PATCH 09/19] [striga] update kycsumsubmodal scroll button (#22534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason Updated design to be clearer ## Overview - adds scroll up button - adds text - adds new icons ## Test Plan ![Screenshot 2025-12-15 at 3.36.03 PM.png](https://app.graphite.com/user-attachments/assets/751da12c-5ad1-4967-a855-f6c5c4472231.png) ![Screenshot 2025-12-15 at 3.36.09 PM.png](https://app.graphite.com/user-attachments/assets/8e8fc348-9859-48e7-ad12-c39ff21bbdcd.png) GitOrigin-RevId: 61a0ed2d6f115e014456a414fd4f36a424f10e34 --- .../src/icons/central/ChevronDoubleDown.tsx | 25 +++++++++++++++++++ .../ui/src/icons/central/ChevronDoubleUp.tsx | 25 +++++++++++++++++++ packages/ui/src/icons/central/index.tsx | 2 ++ 3 files changed, 52 insertions(+) create mode 100644 packages/ui/src/icons/central/ChevronDoubleDown.tsx create mode 100644 packages/ui/src/icons/central/ChevronDoubleUp.tsx diff --git a/packages/ui/src/icons/central/ChevronDoubleDown.tsx b/packages/ui/src/icons/central/ChevronDoubleDown.tsx new file mode 100644 index 000000000..ab351638e --- /dev/null +++ b/packages/ui/src/icons/central/ChevronDoubleDown.tsx @@ -0,0 +1,25 @@ +import { type PathProps } from "../types.js"; + +export function ChevronDoubleDown({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/ChevronDoubleUp.tsx b/packages/ui/src/icons/central/ChevronDoubleUp.tsx new file mode 100644 index 000000000..d60b89aba --- /dev/null +++ b/packages/ui/src/icons/central/ChevronDoubleUp.tsx @@ -0,0 +1,25 @@ +import { type PathProps } from "../types.js"; + +export function ChevronDoubleUp({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index 66327a807..1b97f4370 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -30,6 +30,8 @@ export { Checkmark2Small as CentralCheckmark2Small } from "./Checkmark2Small.js" export { ChevronBottom as CentralChevronBottom } from "./ChevronBottom.js"; export { ChevronBottomSm as CentralChevronBottomSm } from "./ChevronBottomSm.js"; export { ChevronBottomXs as CentralChevronBottomXs } from "./ChevronBottomXs.js"; +export { ChevronDoubleDown as CentralChevronDoubleDown } from "./ChevronDoubleDown.js"; +export { ChevronDoubleUp as CentralChevronDoubleUp } from "./ChevronDoubleUp.js"; export { ChevronDownSmall as CentralChevronDownSmall } from "./ChevronDownSmall.js"; export { ChevronLeft as CentralChevronLeft } from "./ChevronLeft.js"; export { ChevronLeftSm as CentralChevronLeftSm } from "./ChevronLeftSm.js"; From dcc9c37251edd72a5664fedd82e794e70fd7dbd4 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Tue, 16 Dec 2025 17:19:17 -0800 Subject: [PATCH 10/19] [striga] link bank UI fixes (#22577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Overview - minor fixes for the link bank UI and copy changes ## Test Plan ![Screenshot 2025-12-16 at 2.59.21 PM.png](https://app.graphite.com/user-attachments/assets/cbbe3bd3-a8ce-4a20-9896-e0370508ecb3.png) ![Screenshot 2025-12-16 at 2.59.09 PM.png](https://app.graphite.com/user-attachments/assets/9be54352-cfb0-4b07-bba7-446a934d30de.png) ![Screenshot 2025-12-16 at 3.05.17 PM.png](https://app.graphite.com/user-attachments/assets/b440d147-4446-4bbd-af36-b4fa28534aff.png) ![Screenshot 2025-12-16 at 3.00.47 PM.png](https://app.graphite.com/user-attachments/assets/51ccb0ac-d951-4648-bbff-269eeb46dce9.png) GitOrigin-RevId: 7c380f30fe882496802e22fee1f1480348c20f35 --- packages/ui/src/components/Flex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Flex.tsx b/packages/ui/src/components/Flex.tsx index 0029bacfc..f20be4e6c 100644 --- a/packages/ui/src/components/Flex.tsx +++ b/packages/ui/src/components/Flex.tsx @@ -37,7 +37,7 @@ type FlexProps = { | "pre-line" | undefined; height?: number | "auto" | "100%" | undefined; - width?: number | "auto" | "100%" | undefined; + width?: number | "auto" | "50%" | "100%" | undefined; mt?: number | "auto" | undefined; mr?: number | "auto" | undefined; mb?: number | "auto" | undefined; From 2a006850ac97374901a0d89e97844d1772f90e74 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Wed, 17 Dec 2025 17:47:13 -0800 Subject: [PATCH 11/19] [uma-bridge] fix mobile drawer scroll bleed (#22593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason On iOS there's a usability issue with Drawers where there's "scroll bleed" causing - scroll to go below the modal overlay/background - the drawer starts too high ## Overview - fixes the scroll bleed with a new hook that locks scroll - moves drawer to be above keyboard with another hook that listens for the offset - note: top overrides bottom set by the keyboard offset so modals with top will stick to the top which is intentional - refactors NationalitySelector so that VerifyIdentityModal can show its contents within the same modal - various other fixes downside is you can no longer touch move the outer drawer container so if it's too large the user won't be able to see the top ## Test Plan [Screen Recording 2025-12-16 at 7.06.59 PM.mov (uploaded via Graphite) ](https://app.graphite.com/user-attachments/video/1e9de009-90f5-4f2e-9cd9-c03bf055601f.mov) GitOrigin-RevId: 24e1bdeb942ac5138d027b82069f90d312c09234 --- packages/ui/src/components/Drawer.tsx | 22 ++-- packages/ui/src/components/Modal.tsx | 4 +- packages/ui/src/hooks/index.tsx | 2 + packages/ui/src/hooks/useIOSScrollLock.tsx | 127 ++++++++++++++++++++ packages/ui/src/hooks/useKeyboardOffset.tsx | 36 ++++++ 5 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 packages/ui/src/hooks/useIOSScrollLock.tsx create mode 100644 packages/ui/src/hooks/useKeyboardOffset.tsx diff --git a/packages/ui/src/components/Drawer.tsx b/packages/ui/src/components/Drawer.tsx index 355792c6b..4a15e14a5 100644 --- a/packages/ui/src/components/Drawer.tsx +++ b/packages/ui/src/components/Drawer.tsx @@ -2,6 +2,8 @@ import styled from "@emotion/styled"; import { useRef, useState } from "react"; +import { useIOSScrollLock } from "../hooks/useIOSScrollLock.js"; +import { useKeyboardOffset } from "../hooks/useKeyboardOffset.js"; import { standardFocusOutline } from "../styles/common.js"; import { Spacing } from "../styles/tokens/spacing.js"; import { z } from "../styles/z-index.js"; @@ -33,6 +35,10 @@ export const Drawer = (props: Props) => { const [grabbing, setGrabbing] = useState(false); const drawerContainerRef = useRef(null); + useIOSScrollLock(); + + const keyboardOffset = useKeyboardOffset(); + const handleClose = () => { if (props.nonDismissable) { return; @@ -99,6 +105,7 @@ export const Drawer = (props: Props) => { isOpen={isOpen} fractionVisible={fractionVisible} onClick={handleClose} + onTouchMove={(e) => e.preventDefault()} /> { totalDeltaY={totalDeltaY} grabbing={grabbing} ref={drawerContainerRef} + keyboardOffset={keyboardOffset} + data-drawer-scrollable top={props.top} > { const Background = styled.div<{ isOpen: boolean; fractionVisible: number }>` position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; + inset: 0; + max-height: 100dvh; + overflow: hidden; background-color: rgba(0, 0, 0, 0.5); z-index: ${z.modalOverlay}; @@ -178,20 +186,18 @@ const DrawerContainer = styled.div<{ isOpen: boolean; totalDeltaY: number; grabbing: boolean; + keyboardOffset: number; top?: number | undefined; }>` position: fixed; max-height: 100dvh; width: 100%; - right: 0; - bottom: 0; + bottom: ${(props) => props.keyboardOffset ?? 0}px; transform: translateY(${(props) => `${props.totalDeltaY}px`}); z-index: ${z.modalContainer}; display: flex; flex-direction: column; align-items: center; - overflow: scroll; - ${(props) => props.top && `top: ${props.top}px;`} // Only smooth transition when not grabbing, otherwise dragging will feel very laggy diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 2c986812c..77a8b6eb4 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -420,9 +420,9 @@ export function Modal({ handleBack={handleBack} kind={drawerKind ?? "default"} padding={drawerPadding !== undefined ? `${drawerPadding}px` : undefined} - top={top} alignBottom={drawerAlignBottom} disableTouchMove={drawerDisableTouchMove ?? false} + top={top} > {modalContent} @@ -500,7 +500,7 @@ const ModalOverlay = styled.div<{ left: 0; z-index: ${z.modalOverlay}; width: 100vw; - height: 100vh; + height: 100dvh; background: ${({ overlayBackground, theme }) => overlayBackground ? getColor(theme, overlayBackground) diff --git a/packages/ui/src/hooks/index.tsx b/packages/ui/src/hooks/index.tsx index 8df1054ae..43ba6ecce 100644 --- a/packages/ui/src/hooks/index.tsx +++ b/packages/ui/src/hooks/index.tsx @@ -2,7 +2,9 @@ export { useClipboard } from "./useClipboard.js"; export { useDebounce } from "./useDebounce.js"; export { useDocumentTitle } from "./useDocumentTitle.js"; export { default as useFields } from "./useFields.js"; +export { useIOSScrollLock } from "./useIOSScrollLock.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useKeyboardOffset } from "./useKeyboardOffset.js"; export { useLiveRef } from "./useLiveRef.js"; export { useMaxScaleIOS } from "./useMaxScaleIOS.js"; export { useNumberInput } from "./useNumberInput/useNumberInput.js"; diff --git a/packages/ui/src/hooks/useIOSScrollLock.tsx b/packages/ui/src/hooks/useIOSScrollLock.tsx new file mode 100644 index 000000000..5edd3b88a --- /dev/null +++ b/packages/ui/src/hooks/useIOSScrollLock.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect } from "react"; + +/** + * Find the closest scrollable ancestor element + */ +function getScrollableAncestor( + element: HTMLElement | null, +): HTMLElement | null { + while (element && element !== document.body) { + const style = window.getComputedStyle(element); + const overflowY = style.overflowY; + const isScrollable = + (overflowY === "auto" || overflowY === "scroll") && + element.scrollHeight > element.clientHeight; + if (isScrollable) { + return element; + } + element = element.parentElement; + } + return null; +} + +/** + * iOS scroll lock hook - prevents body scrolling when a modal/drawer is open. + * This is necessary because iOS Safari doesn't respect overflow:hidden on body. + * + * Uses data-drawer-scrollable attribute to identify scrollable containers. + */ +export function useIOSScrollLock() { + useEffect(() => { + const scrollY = window.scrollY; + const body = document.body; + const html = document.documentElement; + + const originalBodyStyles = { + position: body.style.position, + top: body.style.top, + left: body.style.left, + right: body.style.right, + overflow: body.style.overflow, + width: body.style.width, + height: body.style.height, + }; + const originalHtmlStyles = { + overflow: html.style.overflow, + height: html.style.height, + }; + + // Lock the body + body.style.position = "fixed"; + body.style.top = `-${scrollY}px`; + body.style.left = "0"; + body.style.right = "0"; + body.style.overflow = "hidden"; + body.style.width = "100%"; + body.style.height = "100%"; + + // Also lock html element for iOS + html.style.overflow = "hidden"; + html.style.height = "100%"; + + // Track touch start for direction detection + let touchStartY = 0; + + const handleTouchStart = (e: TouchEvent) => { + touchStartY = e.touches[0].clientY; + }; + + const handleTouchMove = (e: TouchEvent) => { + const target = e.target as HTMLElement; + + // Check if inside the drawer + if (!target.closest("[data-drawer-scrollable]")) { + e.preventDefault(); + return; + } + + // Find actual scrollable element + const scrollable = getScrollableAncestor(target); + if (!scrollable) { + // No scrollable content, prevent to avoid bleed + e.preventDefault(); + return; + } + + // Check scroll boundaries + const { scrollTop, scrollHeight, clientHeight } = scrollable; + const touchY = e.touches[0].clientY; + const isScrollingUp = touchY > touchStartY; + const isScrollingDown = touchY < touchStartY; + const isAtTop = scrollTop <= 0; + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; + + // Prevent if at boundary and trying to scroll past it + if ((isAtTop && isScrollingUp) || (isAtBottom && isScrollingDown)) { + e.preventDefault(); + } + }; + + document.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + document.addEventListener("touchmove", handleTouchMove, { passive: false }); + + return () => { + // Restore original styles + body.style.position = originalBodyStyles.position; + body.style.top = originalBodyStyles.top; + body.style.left = originalBodyStyles.left; + body.style.right = originalBodyStyles.right; + body.style.overflow = originalBodyStyles.overflow; + body.style.width = originalBodyStyles.width; + body.style.height = originalBodyStyles.height; + + html.style.overflow = originalHtmlStyles.overflow; + html.style.height = originalHtmlStyles.height; + + // Restore scroll position + window.scrollTo(0, scrollY); + + document.removeEventListener("touchstart", handleTouchStart); + document.removeEventListener("touchmove", handleTouchMove); + }; + }, []); +} diff --git a/packages/ui/src/hooks/useKeyboardOffset.tsx b/packages/ui/src/hooks/useKeyboardOffset.tsx new file mode 100644 index 000000000..9a3e4061e --- /dev/null +++ b/packages/ui/src/hooks/useKeyboardOffset.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; + +/** + * Hook to handle iOS keyboard viewport changes. + * Returns the offset needed to keep elements above the keyboard. + */ +export function useKeyboardOffset() { + const [keyboardOffset, setKeyboardOffset] = useState(0); + + useEffect(() => { + const viewport = window.visualViewport; + if (!viewport) return; + + const handleResize = () => { + // Calculate the difference between window height and visual viewport height + // This difference is the keyboard height + const offset = window.innerHeight - viewport.height; + setKeyboardOffset(Math.max(0, offset)); + }; + + // Initial check + handleResize(); + + viewport.addEventListener("resize", handleResize); + viewport.addEventListener("scroll", handleResize); + + return () => { + viewport.removeEventListener("resize", handleResize); + viewport.removeEventListener("scroll", handleResize); + }; + }, []); + + return keyboardOffset; +} From 7f5ef9512268d6484c0fb0ea8f6ded8a6290f3ea Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 17 Dec 2025 21:33:54 -0500 Subject: [PATCH 12/19] Striga design updates / kyc fixes (#22603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason Several design / functionality issues came up during Striga's recent design review, here are a few of them | Change | Appearance | |----------|----------| | new icon for proof of address & secondary text color for subtext | ![Screenshot 2025-12-17 at 11.24.31 AM.png](https://app.graphite.com/user-attachments/assets/e61da014-a21d-496f-b784-fe39819e1718.png) | | remove us error subtext | ![Screenshot 2025-12-17 at 11.06.38 AM.png](https://app.graphite.com/user-attachments/assets/dd5120b1-015d-4bd8-a1bd-acee8afdd20c.png) | | check all banned countries when determining eligibility requirements | previously we relied on IP check - this update makes it so that we block countries like Russia even when they do not have russian vpns. | | Fix rejected final icon & text centering | ![Screenshot 2025-12-17 at 11.28.57 AM.png](https://app.graphite.com/user-attachments/assets/b410f418-be18-4c8f-b3eb-268beb790bc4.png) | |add loading state and spinner to continue | | GitOrigin-RevId: 5fd335913073ab0d07214666beb4de0dcf2a5d86 --- packages/ui/src/icons/central/FileText.tsx | 36 ++++++++++++++++++++++ packages/ui/src/icons/central/index.tsx | 1 + 2 files changed, 37 insertions(+) create mode 100644 packages/ui/src/icons/central/FileText.tsx diff --git a/packages/ui/src/icons/central/FileText.tsx b/packages/ui/src/icons/central/FileText.tsx new file mode 100644 index 000000000..536d3ce72 --- /dev/null +++ b/packages/ui/src/icons/central/FileText.tsx @@ -0,0 +1,36 @@ +import { type PathProps } from "../types.js"; + +export function FileText({ + strokeWidth = "2", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index 1b97f4370..10626798b 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -70,6 +70,7 @@ export { EmptyStrengthIcon as CentralEmptyStrengthIcon } from "./EmptyStrength.j export { EyeOpen as CentralEyeOpen } from "./EyeOpen.js"; export { EyeSlash as CentralEyeSlash } from "./EyeSlash.js"; export { FileBend as CentralFileBend } from "./FileBend.js"; +export { FileText as CentralFileText } from "./FileText.js"; export { Filter2 as CentralFilter2 } from "./Filter2.js"; export { FingerPrint1 as CentralFingerPrint1 } from "./FingerPrint1.js"; export { Gift as CentralGift } from "./Gift.js"; From 78df179114d54dcd471fc35d097554c7655bac73 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 18 Dec 2025 17:14:56 -0500 Subject: [PATCH 13/19] Add dedicated name match failed page for Nigeria (#22653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason closes PX-1305 Nigeria has a dedicated "can't link your bank" page for name match failures This PR adds that page ## Changes * Add a "yellowcard error" enum which is included as part of the bank info output. For now, it only indicates a failure to match names * if the client sees this error, display the new page, which will redirect back to link bank in case the user wants to try again ![Screenshot 2025-12-18 at 10.04.00 AM.png](https://app.graphite.com/user-attachments/assets/a4af0dc4-e622-47d4-8f6b-93b9c5b24925.png) GitOrigin-RevId: c0662e2cbd878da72d07f75654e48b9ee3a54714 --- .../ui/src/icons/central/QuestionCircle.tsx | 26 ++++++++++++++++++ packages/ui/src/icons/central/WarningSign.tsx | 27 +++++++++++++++++++ packages/ui/src/icons/central/index.tsx | 2 ++ 3 files changed, 55 insertions(+) create mode 100644 packages/ui/src/icons/central/QuestionCircle.tsx create mode 100644 packages/ui/src/icons/central/WarningSign.tsx diff --git a/packages/ui/src/icons/central/QuestionCircle.tsx b/packages/ui/src/icons/central/QuestionCircle.tsx new file mode 100644 index 000000000..76aca9d35 --- /dev/null +++ b/packages/ui/src/icons/central/QuestionCircle.tsx @@ -0,0 +1,26 @@ +// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved + +import { type PathProps } from "../types.js"; + +export function QuestionCircle({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/WarningSign.tsx b/packages/ui/src/icons/central/WarningSign.tsx new file mode 100644 index 000000000..68c4e1351 --- /dev/null +++ b/packages/ui/src/icons/central/WarningSign.tsx @@ -0,0 +1,27 @@ +// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved + +import { type PathProps } from "../types.js"; + +export function WarningSign({ + strokeWidth = "2", + strokeLinecap = "round", +}: PathProps) { + return ( + + + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index 10626798b..44f5028dd 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -116,6 +116,7 @@ export { PlusSmall as CentralPlusSmall } from "./PlusSmall.js"; export { PointChart as CentralPointChart } from "./PointChart.js"; export { Postcard as CentralPostcard } from "./Postcard.js"; export { QrCode as CentralQrCode } from "./QrCode.js"; +export { QuestionCircle as CentralQuestionCircle } from "./QuestionCircle.js"; export { ReceiptBill as CentralReceiptBill } from "./ReceiptBill.js"; export { RunShortcut as CentralRunShortcut } from "./RunShortcut.js"; export { SecretPhrase as CentralSecretPhrase } from "./SecretPhrase.js"; @@ -143,5 +144,6 @@ export { Turtle as CentralTurtle } from "./Turtle.js"; export { Variables as CentralVariables } from "./Variables.js"; export { Vignette as CentralVignette } from "./Vignette.js"; export { Wallet3 as CentralWallet3 } from "./Wallet3.js"; +export { WarningSign as CentralWarningSign } from "./WarningSign.js"; export { WeakStrengthIcon as CentralWeakStrengthIcon } from "./WeakStrength.js"; export { Wrench as CentralWrench } from "./Wrench.js"; From 3e356c990ab304ab423666a3f36efc80eee63356 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 18 Dec 2025 16:40:23 -0800 Subject: [PATCH 14/19] [AI tools] Several small tweaks to make cursor work better. (#22683) - Add symlinks from all CLAUDE.md files to AGENTS.md so that they get seen by all agent types (including cursor). - Switch the cursor rule structure to the new file format: https://cursor.com/docs/context/rules#project-rules - Clean up a few of the rules for formatting and details. GitOrigin-RevId: 5b0bb0ed7e60248fd7c83be92b6a5b24b5c71df8 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..681311eb9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file From fee46bbf76412377c839bb84109892e5216be006 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Mon, 22 Dec 2025 13:41:33 -0800 Subject: [PATCH 15/19] [uma-bridge] add segment events for all flows, update event name format (#22727) ## Reason We mainly only had events for the zerohash onboarding flow and were generally missing a lot of events. This adds events from https://docs.google.com/document/d/1jxW4cKvoyK8k-L7eDEMi_xg65iYqc6pQl1RdyAvLwGA/edit?tab=t.uzinptq1apsn and more ## Overview - adds events - rather than region-specific event names, have generic events and have accountType be a parameter in certain cases - reformats existing events to follow the same pattern (no colon prefix, and action properties) ## Test plan tested locally GitOrigin-RevId: 40fac5de2b9af8072c2ff44d1d156b448d9875ab --- packages/ui/src/components/BirthdayInput.tsx | 28 +++++++++++++++----- packages/ui/src/components/TextInput.tsx | 4 ++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/components/BirthdayInput.tsx b/packages/ui/src/components/BirthdayInput.tsx index 67e7ba2e6..6d797397e 100644 --- a/packages/ui/src/components/BirthdayInput.tsx +++ b/packages/ui/src/components/BirthdayInput.tsx @@ -1,6 +1,7 @@ import { TextInput } from "@lightsparkdev/ui/src/components"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat.js"; +import { type FocusEvent, useEffect, useRef } from "react"; dayjs.extend(customParseFormat); @@ -9,6 +10,8 @@ interface BirthdayInputProps { invalidBirthdayError: string; setDate: (date: string) => void; dateFormat?: "US" | "INTL"; + onFocus?: (event: FocusEvent) => void; + onError?: (error: string) => void; } const DATE_FORMATS = { @@ -146,10 +149,26 @@ export function BirthdayInput({ setDate, invalidBirthdayError, dateFormat = "US", + onFocus, + onError, }: BirthdayInputProps) { const formatConfig = DATE_FORMATS[dateFormat]; const birthdayFieldBlurred = Boolean(date.trim()); + const isCompleteDate = date.length === 10; + const isInvalid = + isCompleteDate && !isValidBirthday(date, undefined, undefined, dateFormat); + const showError = birthdayFieldBlurred && isInvalid; + + // Track previous error state to detect when error first appears + const prevShowErrorRef = useRef(false); + useEffect(() => { + if (showError && !prevShowErrorRef.current && onError) { + onError(invalidBirthdayError); + } + prevShowErrorRef.current = showError; + }, [showError, onError, invalidBirthdayError]); + const handleChange = (newValue: string): void => { let value = newValue; @@ -168,10 +187,6 @@ export function BirthdayInput({ setDate(formattedValue); }; - const isCompleteDate = date.length === 10; - const isInvalid = - isCompleteDate && !isValidBirthday(date, undefined, undefined, dateFormat); - return ( <> ) => void; onChange: (newValue: string, event: ChangeEvent) => void; onEnter?: () => void; - onFocus?: (event: FocusEvent) => void; + onFocus?: + | ((event: FocusEvent) => void) + | undefined; onPaste?: (event: ClipboardEvent) => void; onKeyDown?: ( keyValue: string, From 08c3f7f51c7b3d20689283d8d6f0d8af86f65034 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 23 Dec 2025 12:41:00 -0500 Subject: [PATCH 16/19] fix double display of currency for currencies without a narrow symbol (#22779) ## Reason closes PQA-394 XOF doesn't have a narrow symbol (like $) so our currency rendering libraries render twice This change checks for such currencies (probably a few in africa) and prevents them from rendering twice GitOrigin-RevId: ba506dcf9d9583ae0fcf8aad36b1b5246eca9968 --- packages/core/src/utils/currency.ts | 7 +++++++ .../core/src/utils/tests/currency.test.ts | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 53c79abca..56b40a2a0 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -1255,6 +1255,13 @@ export function formatCurrencyStr( const unitStr = isUmaCurrencyAmount(amount) ? amount.currency.code : abbrCurrencyUnit(unit as CurrencyUnitType); + + // Skip appending if the formatted string already contains the currency code + // This happens for currencies like XOF where Intl.NumberFormat uses the code as the symbol + if (formattedStr.includes(unitStr)) { + return formattedStr; + } + const unitSuffix = options?.appendUnits?.plural && num > 1 ? "s" : ""; const unitStrWithSuffix = `${unitStr}${unitSuffix}`; formattedStr += ` ${ diff --git a/packages/core/src/utils/tests/currency.test.ts b/packages/core/src/utils/tests/currency.test.ts index 25f590e52..e2f8e5797 100644 --- a/packages/core/src/utils/tests/currency.test.ts +++ b/packages/core/src/utils/tests/currency.test.ts @@ -436,3 +436,22 @@ describe("formatCurrencyStr", () => { ).toBe("$1,000.12 MXN"); }); }); + +it("should not append XOF if it's already in the formatted string", () => { + /* This test verifies the fix for PQA-394 where XOF was appearing twice. + * If Intl.NumberFormat formats with currencyDisplay: 'code', it will include + * XOF in the output, and we shouldn't append it again. */ + const formatted = formatCurrencyStr( + { value: 221900, unit: CurrencyUnit.XOF }, + { appendUnits: { plural: false, lowercase: false } }, + ); + + /* Count occurrences of 'XOF' or 'CFA' in the result */ + const xofCount = (formatted.match(/XOF/g) || []).length; + const cfaCount = (formatted.match(/CFA/g) || []).length; + + /* Should have either XOF or CFA, but not both, and only once */ + expect(xofCount + cfaCount).toBeGreaterThan(0); + expect(xofCount).toBeLessThanOrEqual(1); + expect(cfaCount).toBeLessThanOrEqual(1); +}); From 6d23ba45d17e7c1232f370cd83ffeec5d961c183 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Tue, 23 Dec 2025 16:01:58 -0800 Subject: [PATCH 17/19] [uma-bridge] add new 2fa code input design (#22794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason Long overdue design for 2fa code input ## Overview - adds "unified" variant for the CodeInput component ## Test Plan ![Screenshot 2025-12-23 at 3.50.45 PM.png](https://app.graphite.com/user-attachments/assets/0d0a5fda-2a55-4f30-819a-ac422e9c19aa.png) ![Screenshot 2025-12-23 at 3.50.58 PM.png](https://app.graphite.com/user-attachments/assets/9a0749a4-bb1f-4608-960a-22a51df8b5e0.png) GitOrigin-RevId: bfe4340728490a7e8d88fc36d919cd46c244146b --- .../ui/src/components/CodeInput/CodeInput.tsx | 32 ++++++++++++- .../components/CodeInput/SingleCodeInput.tsx | 47 +++++++++++++++---- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/CodeInput/CodeInput.tsx b/packages/ui/src/components/CodeInput/CodeInput.tsx index 0ddf87ffb..e5e38d02a 100644 --- a/packages/ui/src/components/CodeInput/CodeInput.tsx +++ b/packages/ui/src/components/CodeInput/CodeInput.tsx @@ -11,6 +11,7 @@ import type { import { createRef, useCallback, useRef, useState } from "react"; import { isChrome, isMobile, isMobileSafari } from "react-device-detect"; import { textInputBorderColor } from "../../styles/fields.js"; +import { Spacing } from "../../styles/tokens/spacing.js"; import { SingleCodeInput } from "./SingleCodeInput.js"; type InputState = { @@ -21,6 +22,7 @@ type InputState = { export type OnSubmitCode = (code: string) => void; export type OnChangeCode = (code: string) => void; +export type CodeInputVariant = "default" | "unified"; type CodeInputProps = { onChange?: OnChangeCode | undefined; onSubmit?: OnSubmitCode | undefined; @@ -28,6 +30,7 @@ type CodeInputProps = { inputCSS?: SerializedStyles; autoFocus?: boolean; onBlur?: (() => void) | undefined; + variant?: CodeInputVariant; }; function codeFromInputState(inputState: InputState): string { @@ -60,6 +63,7 @@ export function CodeInput({ inputCSS, autoFocus = true, onBlur, + variant = "default", }: CodeInputProps): JSX.Element { const componentId = useRef(nanoid(5)); const inputRefs = useRef({}); @@ -397,12 +401,21 @@ export function CodeInput({ ariaLabel={`Code input ${i + 1} of ${codeLength}`} cssProp={inputCSS} onBlur={onBlurCb} - width={`${(100 / inputsPerGroup).toFixed(2)}%`} + width={ + variant === "unified" + ? "12px" + : `${(100 / inputsPerGroup).toFixed(2)}%` + } max={9} + variant={variant} />, ); } + if (variant === "unified") { + return {inputs}; + } + return ( {inputs.slice(0, inputsPerGroup)} @@ -441,3 +454,20 @@ const CodeInputGroupSeparator = styled.div` border-radius: 2px; margin: 0 ${separatorMargin}px; `; + +const UnifiedCodeInputContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: ${Spacing.px.xs}; + padding: 12px 24px; + border: 0.5px solid ${({ theme }) => theme.border}; + border-radius: 16px; + background-color: ${({ theme }) => theme.bg}; + width: 100%; + + &:focus-within { + border: 2px solid ${({ theme }) => theme.text}; + } +`; diff --git a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx index 5849a9bd8..59b14ef80 100644 --- a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx +++ b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx @@ -8,6 +8,7 @@ import type { Ref, } from "react"; import { textInputStyle } from "../../styles/fields.js"; +import type { CodeInputVariant } from "./CodeInput.js"; type CounterProps = { id?: string; @@ -34,6 +35,7 @@ type CounterProps = { disabled?: boolean; onBlur?: (event: React.FocusEvent) => void; width: string; + variant?: CodeInputVariant; }; const defaultWidth = "42px"; @@ -55,8 +57,37 @@ export function SingleCodeInput({ disabled, onBlur, width = defaultWidth, + variant = "default", }: CounterProps): JSX.Element { const theme = useTheme(); + + const commonCSS = css` + text-align: center; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + display: none; + margin: 0; + } + -moz-appearance: textfield; /* Firefox */ + `; + + const unifiedCSS = css` + ${commonCSS} + width: ${width}; + padding: 0; + border: none; + background: transparent; + font: inherit; + color: ${theme.text}; + outline: none; + caret-color: ${theme.info}; + + &::placeholder { + color: ${theme.secondary}; + } + `; + const defaultCSS = css( ` ${ @@ -70,7 +101,6 @@ export function SingleCodeInput({ width: ${width}; max-width: ${defaultWidth}; padding: ${vPadding}px 0px; - text-align: center; &:not(:last-of-type):not(:focus-visible) { border-right: none; } @@ -89,17 +119,13 @@ export function SingleCodeInput({ &:active { padding: ${vPadding - 1}px 0px !important; } - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - display: none; - margin: 0; - } - -moz-appearance:textfield; /* Firefox */ `, + commonCSS, cssProp, ); + const inputStyles = variant === "unified" ? unifiedCSS : defaultCSS; + return ( onChange(event, id)} value={value} ref={inputRef} - type={type} + type={variant === "unified" ? "text" : type} aria-label={ariaLabel} onKeyUp={(event) => onKeyUp(event, id)} onKeyDown={(event) => onKeyDown(event, id)} onPaste={(event) => onPaste(event)} max={max} - css={defaultCSS} + css={inputStyles} onBlur={onBlur} inputMode="numeric" pattern="[0-9]*" + placeholder={variant === "unified" ? "-" : undefined} /> ); } From 1cc7197242137658efa61836e7f8d04ede20346b Mon Sep 17 00:00:00 2001 From: Peng Ying Date: Sun, 28 Dec 2025 13:22:39 -0800 Subject: [PATCH 18/19] feat: adding idempotency to JS withdrawal request (#22827) ## Reason Added support for idempotency keys in withdrawal requests to prevent duplicate withdrawals when network issues occur. ## Overview This change adds an optional `idempotencyKey` parameter to the `requestWithdrawal` method in the Lightspark SDK. The parameter is passed to the GraphQL mutation, allowing clients to safely retry withdrawal requests without risking duplicate transactions. GitOrigin-RevId: 174bd753d4b2e591a1c36fd26288edf0b2cd5bc8 --- packages/lightspark-sdk/src/client.ts | 17 +++++++++++------ .../src/graphql/RequestWithdrawal.ts | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/lightspark-sdk/src/client.ts b/packages/lightspark-sdk/src/client.ts index d791de69f..cefb83b3d 100644 --- a/packages/lightspark-sdk/src/client.ts +++ b/packages/lightspark-sdk/src/client.ts @@ -1231,15 +1231,20 @@ class LightsparkClient { amountSats: number, bitcoinAddress: string, mode: WithdrawalMode, + idempotencyKey: string | undefined = undefined, ): Promise { + const variables: Record = { + node_id: nodeId, + amount_sats: amountSats, + bitcoin_address: bitcoinAddress, + withdrawal_mode: mode, + }; + if (idempotencyKey !== undefined) { + variables.idempotency_key = idempotencyKey; + } const response = await this.requester.makeRawRequest( RequestWithdrawal, - { - node_id: nodeId, - amount_sats: amountSats, - bitcoin_address: bitcoinAddress, - withdrawal_mode: mode, - }, + variables, nodeId, ); return WithdrawalRequestFromJson(response.request_withdrawal.request); diff --git a/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts b/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts index ab6d48c5a..0a16daf9e 100644 --- a/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts +++ b/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts @@ -8,12 +8,14 @@ export const RequestWithdrawal = ` $bitcoin_address: String! $amount_sats: Long! $withdrawal_mode: WithdrawalMode! + $idempotency_key: String ) { request_withdrawal(input: { node_id: $node_id bitcoin_address: $bitcoin_address amount_sats: $amount_sats withdrawal_mode: $withdrawal_mode + idempotency_key: $idempotency_key }) { request { ...WithdrawalRequestFragment From a3d32a4344a35dbd7aafb9891b5ef5c7a56710f4 Mon Sep 17 00:00:00 2001 From: Peng Ying Date: Sat, 3 Jan 2026 11:45:34 -0800 Subject: [PATCH 19/19] adding changesets --- .changeset/curvy-experts-fall.md | 5 +++++ .changeset/little-hands-sip.md | 5 +++++ .changeset/nice-pumas-rhyme.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/curvy-experts-fall.md create mode 100644 .changeset/little-hands-sip.md create mode 100644 .changeset/nice-pumas-rhyme.md diff --git a/.changeset/curvy-experts-fall.md b/.changeset/curvy-experts-fall.md new file mode 100644 index 000000000..6ada7601b --- /dev/null +++ b/.changeset/curvy-experts-fall.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/core": patch +--- + +Adding currency support for XOF, XAF, MWK, RWF diff --git a/.changeset/little-hands-sip.md b/.changeset/little-hands-sip.md new file mode 100644 index 000000000..ecfe83f79 --- /dev/null +++ b/.changeset/little-hands-sip.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/ui": patch +--- + +UI updates and fixes for bank linking, kyc, 2fa, filters diff --git a/.changeset/nice-pumas-rhyme.md b/.changeset/nice-pumas-rhyme.md new file mode 100644 index 000000000..36fccdf6e --- /dev/null +++ b/.changeset/nice-pumas-rhyme.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/lightspark-sdk": patch +--- + +Adding ability to fail HTLCs and add an idempotency key to requestWithdrawal