diff --git a/packages/common/src/store/buy-usdc/sagas.ts b/packages/common/src/store/buy-usdc/sagas.ts index 1102129f91d..82edda2ea8f 100644 --- a/packages/common/src/store/buy-usdc/sagas.ts +++ b/packages/common/src/store/buy-usdc/sagas.ts @@ -17,10 +17,10 @@ import { initializeStripeModal } from 'store/ui/stripe-modal/slice' import { buyUSDCFlowFailed, buyUSDCFlowSucceeded, - onRampCanceled, - onRampOpened, - onPurchaseStarted, - onRampSucceeded + onrampCanceled, + onrampOpened, + purchaseStarted, + onrampSucceeded } from './slice' import { USDCOnRampProvider } from './types' import { getUSDCUserBank } from './utils' @@ -69,12 +69,12 @@ function* purchaseStep({ ) const initialBalance = initialAccountInfo.amount - yield* put(onPurchaseStarted()) + yield* put(purchaseStarted()) // Wait for on ramp finish const result = yield* race({ - success: take(onRampSucceeded), - canceled: take(onRampCanceled) + success: take(onrampSucceeded), + canceled: take(onrampCanceled) }) // If the user didn't complete the on ramp flow, return early @@ -119,7 +119,7 @@ function* doBuyUSDC({ provider, purchaseInfo: { desiredAmount } } -}: ReturnType) { +}: ReturnType) { const reportToSentry = yield* getContext('reportToSentry') const { track, make } = yield* getContext('analytics') @@ -136,8 +136,8 @@ function* doBuyUSDC({ amount: (desiredAmount / 100).toString(), destinationCurrency: 'usdc', destinationWallet: userBank.toString(), - onRampCanceled, - onRampSucceeded + onrampCanceled, + onrampSucceeded }) ) @@ -201,7 +201,7 @@ function* doBuyUSDC({ } function* watchOnRampOpened() { - yield takeLatest(onRampOpened, doBuyUSDC) + yield takeLatest(onrampOpened, doBuyUSDC) } export default function sagas() { diff --git a/packages/common/src/store/buy-usdc/slice.ts b/packages/common/src/store/buy-usdc/slice.ts index f4e4452d52d..f6fbc408266 100644 --- a/packages/common/src/store/buy-usdc/slice.ts +++ b/packages/common/src/store/buy-usdc/slice.ts @@ -31,7 +31,7 @@ const slice = createSlice({ name: 'buy-usdc', initialState, reducers: { - onRampOpened: ( + onrampOpened: ( state, action: PayloadAction<{ purchaseInfo: PurchaseInfo @@ -44,19 +44,18 @@ const slice = createSlice({ state.provider = action.payload.provider state.onSuccess = action.payload.onSuccess }, - onPurchaseStarted: (state) => { + purchaseStarted: (state) => { state.stage = BuyUSDCStage.PURCHASING }, - onRampCanceled: (state) => { + onrampCanceled: (state) => { if (state.stage === BuyUSDCStage.PURCHASING) { state.stage = BuyUSDCStage.CANCELED } }, - onRampSucceeded: (state) => { + onrampSucceeded: (state) => { state.stage = BuyUSDCStage.CONFIRMING_PURCHASE }, buyUSDCFlowFailed: (state) => { - // TODO: Probably want to pass error in action payload state.error = new Error('USDC purchase failed') }, buyUSDCFlowSucceeded: (state) => { @@ -74,10 +73,10 @@ const slice = createSlice({ export const { buyUSDCFlowFailed, buyUSDCFlowSucceeded, - onRampOpened, - onPurchaseStarted, - onRampSucceeded, - onRampCanceled, + onrampOpened, + purchaseStarted, + onrampSucceeded, + onrampCanceled, stripeSessionStatusChanged } = slice.actions diff --git a/packages/common/src/store/purchase-content/index.ts b/packages/common/src/store/purchase-content/index.ts index dea8a0df475..89c36dee8ef 100644 --- a/packages/common/src/store/purchase-content/index.ts +++ b/packages/common/src/store/purchase-content/index.ts @@ -5,3 +5,4 @@ export { export * as purchaseContentSelectors from './selectors' export { default as purchaseContentSagas } from './sagas' export * from './types' +export * from './utils' diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index a0ca9e161da..f73d266a085 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -14,8 +14,8 @@ import { accountSelectors } from 'store/account' import { buyUSDCFlowFailed, buyUSDCFlowSucceeded, - onRampOpened, - onRampCanceled + onrampOpened, + onrampCanceled } from 'store/buy-usdc/slice' import { USDCOnRampProvider } from 'store/buy-usdc/types' import { getUSDCUserBank } from 'store/buy-usdc/utils' @@ -29,10 +29,11 @@ import { pollPremiumTrack } from '../premium-content/sagas' import { updatePremiumTrackStatus } from '../premium-content/slice' import { - onBuyUSDC, - onPurchaseConfirmed, - onPurchaseSucceeded, - onUSDCBalanceSufficient, + buyUSDC, + purchaseCanceled, + purchaseConfirmed, + purchaseSucceeded, + usdcBalanceSufficient, purchaseContentFlowFailed, startPurchaseContentFlow } from './slice' @@ -159,9 +160,9 @@ function* doStartPurchaseContentFlow({ // buy USDC if necessary if (initialBalance.lt(new BN(price).mul(BN_USDC_CENT_WEI))) { - yield* put(onBuyUSDC()) + yield* put(buyUSDC()) yield* put( - onRampOpened({ + onrampOpened({ provider: USDCOnRampProvider.STRIPE, purchaseInfo: { desiredAmount: price @@ -171,17 +172,22 @@ function* doStartPurchaseContentFlow({ const result = yield* race({ success: take(buyUSDCFlowSucceeded), - canceled: take(onRampCanceled), + canceled: take(onrampCanceled), failed: take(buyUSDCFlowFailed) }) - if (result.canceled || result.failed) { - // Return early for failure or cancellation + // Return early for failure or cancellation + if (result.canceled) { + yield* put(purchaseCanceled()) + return + } + if (result.failed) { + yield* put(purchaseContentFlowFailed()) return } } - yield* put(onUSDCBalanceSufficient()) + yield* put(usdcBalanceSufficient()) const { blocknumber, splits } = yield* getPurchaseConfig({ contentId, @@ -195,13 +201,13 @@ function* doStartPurchaseContentFlow({ splits, type: 'track' }) - yield* put(onPurchaseSucceeded()) + yield* put(purchaseSucceeded()) // confirm purchase yield* pollForPurchaseConfirmation({ contentId, contentType }) // finish - yield* put(onPurchaseConfirmed()) + yield* put(purchaseConfirmed()) yield* put( setVisibility({ diff --git a/packages/common/src/store/purchase-content/slice.ts b/packages/common/src/store/purchase-content/slice.ts index 35874b58bcd..6e927debbb0 100644 --- a/packages/common/src/store/purchase-content/slice.ts +++ b/packages/common/src/store/purchase-content/slice.ts @@ -42,37 +42,35 @@ const slice = createSlice({ state.contentType = action.payload.contentType || ContentType.TRACK state.onSuccess = action.payload.onSuccess }, - onBuyUSDC: (state) => { + buyUSDC: (state) => { state.stage = PurchaseContentStage.BUY_USDC }, - onUSDCBalanceSufficient: (state) => { + usdcBalanceSufficient: (state) => { state.stage = PurchaseContentStage.PURCHASING }, - onPurchaseCanceled: (state) => { - state.error = new Error('Content purchase canceled') + purchaseCanceled: (state) => { state.stage = PurchaseContentStage.CANCELED }, - onPurchaseSucceeded: (state) => { + purchaseSucceeded: (state) => { state.stage = PurchaseContentStage.CONFIRMING_PURCHASE }, - onPurchaseConfirmed: (state) => { + purchaseConfirmed: (state) => { state.stage = PurchaseContentStage.FINISH }, - purchaseContentFlowFailed: (state) => { - // TODO: Probably want to pass error in action payload state.error = new Error('Content purchase failed') - } + }, + cleanup: () => initialState } }) export const { startPurchaseContentFlow, - onBuyUSDC, - onUSDCBalanceSufficient, - onPurchaseSucceeded, - onPurchaseConfirmed, - onPurchaseCanceled, + buyUSDC, + usdcBalanceSufficient, + purchaseSucceeded, + purchaseConfirmed, + purchaseCanceled, purchaseContentFlowFailed } = slice.actions diff --git a/packages/common/src/store/purchase-content/utils.ts b/packages/common/src/store/purchase-content/utils.ts new file mode 100644 index 00000000000..7697dd5df4c --- /dev/null +++ b/packages/common/src/store/purchase-content/utils.ts @@ -0,0 +1,9 @@ +import { PurchaseContentStage } from './types' + +export const isContentPurchaseInProgress = (stage: PurchaseContentStage) => { + return [ + PurchaseContentStage.BUY_USDC, + PurchaseContentStage.PURCHASING, + PurchaseContentStage.CONFIRMING_PURCHASE + ].includes(stage) +} diff --git a/packages/common/src/store/ui/buy-audio/slice.ts b/packages/common/src/store/ui/buy-audio/slice.ts index 00c96844511..c7fa2fb4b47 100644 --- a/packages/common/src/store/ui/buy-audio/slice.ts +++ b/packages/common/src/store/ui/buy-audio/slice.ts @@ -129,15 +129,15 @@ const slice = createSlice({ state.provider = action.payload.provider state.onSuccess = action.payload.onSuccess }, - onRampOpened: (state, _action: PayloadAction) => { + onrampOpened: (state, _action: PayloadAction) => { state.stage = BuyAudioStage.PURCHASING }, - onRampCanceled: (state) => { + onrampCanceled: (state) => { if (state.stage === BuyAudioStage.PURCHASING) { state.error = true } }, - onRampSucceeded: (state) => { + onrampSucceeded: (state) => { state.stage = BuyAudioStage.CONFIRMING_PURCHASE }, swapStarted: (state) => { @@ -175,9 +175,9 @@ export const { cacheTransactionFees, clearFeesCache, startBuyAudioFlow, - onRampOpened, - onRampSucceeded, - onRampCanceled, + onrampOpened, + onrampSucceeded, + onrampCanceled, swapStarted, swapCompleted, transferStarted, diff --git a/packages/common/src/store/ui/stripe-modal/sagas.ts b/packages/common/src/store/ui/stripe-modal/sagas.ts index d75870c073d..36d186ac6e4 100644 --- a/packages/common/src/store/ui/stripe-modal/sagas.ts +++ b/packages/common/src/store/ui/stripe-modal/sagas.ts @@ -30,20 +30,20 @@ function* handleStripeSessionChanged({ payload: { status } }: ReturnType) { if (status === 'fulfillment_complete') { - const { onRampSucceeded } = yield* select(getStripeModalState) - if (onRampSucceeded) { - yield* put(onRampSucceeded) + const { onrampSucceeded } = yield* select(getStripeModalState) + if (onrampSucceeded) { + yield* put(onrampSucceeded) } yield* put(setVisibility({ modal: 'StripeOnRamp', visible: false })) } } function* handleCancelStripeOnramp() { - const { onRampCanceled } = yield* select(getStripeModalState) + const { onrampCanceled } = yield* select(getStripeModalState) yield* put(setVisibility({ modal: 'StripeOnRamp', visible: false })) - if (onRampCanceled) { - yield* put(onRampCanceled) + if (onrampCanceled) { + yield* put(onrampCanceled) } } diff --git a/packages/common/src/store/ui/stripe-modal/slice.ts b/packages/common/src/store/ui/stripe-modal/slice.ts index 14bdf550483..5cefbb1663b 100644 --- a/packages/common/src/store/ui/stripe-modal/slice.ts +++ b/packages/common/src/store/ui/stripe-modal/slice.ts @@ -10,8 +10,8 @@ type InitializeStripeModalPayload = { amount: string destinationCurrency: StripeDestinationCurrencyType destinationWallet: string - onRampSucceeded: Action - onRampCanceled: Action + onrampSucceeded: Action + onrampCanceled: Action } const initialState: StripeModalState = {} @@ -25,8 +25,8 @@ const slice = createSlice({ action: PayloadAction ) => { state.stripeSessionStatus = 'initialized' - state.onRampSucceeded = action.payload.onRampSucceeded - state.onRampCanceled = action.payload.onRampCanceled + state.onrampSucceeded = action.payload.onrampSucceeded + state.onrampCanceled = action.payload.onrampCanceled }, stripeSessionCreated: ( state, diff --git a/packages/common/src/store/ui/stripe-modal/types.ts b/packages/common/src/store/ui/stripe-modal/types.ts index 51c7393e36f..bc93a58ca53 100644 --- a/packages/common/src/store/ui/stripe-modal/types.ts +++ b/packages/common/src/store/ui/stripe-modal/types.ts @@ -10,8 +10,8 @@ export type StripeSessionStatus = export type StripeDestinationCurrencyType = 'sol' | 'usdc' export type StripeModalState = { - onRampSucceeded?: Action - onRampCanceled?: Action + onrampSucceeded?: Action + onrampCanceled?: Action stripeSessionStatus?: StripeSessionStatus stripeClientSecret?: string } diff --git a/packages/web/src/components/buy-audio-modal/components/CoinbaseBuyAudioButton.tsx b/packages/web/src/components/buy-audio-modal/components/CoinbaseBuyAudioButton.tsx index 2aa06fd6621..b8d136f57e4 100644 --- a/packages/web/src/components/buy-audio-modal/components/CoinbaseBuyAudioButton.tsx +++ b/packages/web/src/components/buy-audio-modal/components/CoinbaseBuyAudioButton.tsx @@ -17,9 +17,9 @@ import { getRootSolanaAccount } from 'services/audius-backend/BuyAudio' import styles from './CoinbaseBuyAudioButton.module.css' const { - onRampOpened, - onRampCanceled, - onRampSucceeded, + onrampOpened, + onrampCanceled, + onrampSucceeded, calculateAudioPurchaseInfo } = buyAudioActions const { getAudioPurchaseInfo, getAudioPurchaseInfoStatus } = buyAudioSelectors @@ -43,10 +43,10 @@ export const CoinbaseBuyAudioButton = () => { const isDisabled = purchaseInfoStatus === Status.LOADING || belowSolThreshold const handleExit = useCallback(() => { - dispatch(onRampCanceled()) + dispatch(onrampCanceled()) }, [dispatch]) const handleSuccess = useCallback(() => { - dispatch(onRampSucceeded()) + dispatch(onrampSucceeded()) }, [dispatch]) const handleClick = useCallback(() => { @@ -60,7 +60,7 @@ export const CoinbaseBuyAudioButton = () => { onSuccess: handleSuccess, onExit: handleExit }) - dispatch(onRampOpened(purchaseInfo)) + dispatch(onrampOpened(purchaseInfo)) coinbasePay.open() } else if (purchaseInfoStatus === Status.IDLE) { // Generally only possible if `amount` is still undefined, diff --git a/packages/web/src/components/buy-audio-modal/components/StripeBuyAudioButton.tsx b/packages/web/src/components/buy-audio-modal/components/StripeBuyAudioButton.tsx index 76cad02dd05..59e924a8262 100644 --- a/packages/web/src/components/buy-audio-modal/components/StripeBuyAudioButton.tsx +++ b/packages/web/src/components/buy-audio-modal/components/StripeBuyAudioButton.tsx @@ -15,7 +15,7 @@ import { getRootSolanaAccount } from 'services/audius-backend/BuyAudio' import styles from './StripeBuyAudioButton.module.css' const { getAudioPurchaseInfo } = buyAudioSelectors -const { onRampOpened, onRampSucceeded, onRampCanceled } = buyAudioActions +const { onrampOpened, onrampSucceeded, onrampCanceled } = buyAudioActions const { initializeStripeModal } = stripeModalUIActions const messages = { @@ -37,7 +37,7 @@ export const StripeBuyAudioButton = () => { if (!amount || !purchaseInfo || purchaseInfo?.isError === true) { return } - dispatch(onRampOpened(purchaseInfo)) + dispatch(onrampOpened(purchaseInfo)) try { const destinationWallet: string = ( await getRootSolanaAccount() @@ -45,14 +45,14 @@ export const StripeBuyAudioButton = () => { dispatch( initializeStripeModal({ amount, - onRampSucceeded, - onRampCanceled, + onrampSucceeded, + onrampCanceled, destinationCurrency: 'sol', destinationWallet }) ) } catch (e) { - dispatch(onRampCanceled()) + dispatch(onrampCanceled()) console.error(e) } }, [dispatch, amount, purchaseInfo]) diff --git a/packages/web/src/components/data-entry/HelperText.tsx b/packages/web/src/components/data-entry/HelperText.tsx index 7416d453065..9d9c16014d9 100644 --- a/packages/web/src/components/data-entry/HelperText.tsx +++ b/packages/web/src/components/data-entry/HelperText.tsx @@ -16,7 +16,6 @@ export const HelperText = (props: HelperTextProps) => { {children} diff --git a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx index 98213a04fb5..7babb05c218 100644 --- a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx +++ b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx @@ -1,8 +1,12 @@ import { useCallback } from 'react' -import { premiumContentSelectors, useGetTrackById } from '@audius/common' +import { + premiumContentSelectors, + purchaseContentActions, + useGetTrackById +} from '@audius/common' import { IconCart, Modal, ModalContentPages, ModalHeader } from '@audius/stems' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import { Icon } from 'components/Icon' @@ -26,6 +30,7 @@ enum PurchaseSteps { export const PremiumContentPurchaseModal = () => { const [isOpen, setIsOpen] = useModalState('PremiumContentPurchase') const trackId = useSelector(getPurchaseContentId) + const dispatch = useDispatch() const { data: track } = useGetTrackById( { id: trackId! }, { disabled: !trackId } @@ -33,7 +38,8 @@ export const PremiumContentPurchaseModal = () => { const handleClose = useCallback(() => { setIsOpen(false) - }, [setIsOpen]) + dispatch(purchaseContentActions.cleanup()) + }, [setIsOpen, dispatch]) const currentStep = !track ? PurchaseSteps.LOADING : PurchaseSteps.DETAILS diff --git a/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.module.css b/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.module.css index c7a866109e8..be413dac395 100644 --- a/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.module.css +++ b/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.module.css @@ -18,3 +18,9 @@ .purchaseButtonSpinner g path { stroke: currentColor; } + +.errorContainer { + display: flex; + gap: var(--unit-2); + align-items: center; +} diff --git a/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.tsx b/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.tsx index 9b2b07d4c7e..84707d70d68 100644 --- a/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.tsx +++ b/packages/web/src/components/premium-content-purchase-modal/components/PurchaseDetailsPage.tsx @@ -3,29 +3,42 @@ import { useCallback } from 'react' import { ContentType, formatPrice, + isContentPurchaseInProgress, isPremiumContentUSDCPurchaseGated, purchaseContentActions, purchaseContentSelectors, - PurchaseContentStage, Track, UserTrackMetadata } from '@audius/common' -import { HarmonyButton } from '@audius/stems' +import { HarmonyButton, IconError } from '@audius/stems' import { useDispatch, useSelector } from 'react-redux' +import { Icon } from 'components/Icon' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import { LockedTrackDetailsTile } from 'components/track/LockedTrackDetailsTile' +import { Text } from 'components/typography' import { PayToUnlockInfo } from './PayToUnlockInfo' import styles from './PurchaseDetailsPage.module.css' import { PurchaseSummaryTable } from './PurchaseSummaryTable' const { startPurchaseContentFlow } = purchaseContentActions -const { getPurchaseContentFlowStage } = purchaseContentSelectors +const { getPurchaseContentFlowStage, getPurchaseContentError } = + purchaseContentSelectors const messages = { buy: (price: string) => `Buy $${price}`, - purchasing: 'Purchasing' + purchasing: 'Purchasing', + error: 'Your purchase was unsuccessful.' +} + +const ContentPurchaseError = () => { + return ( + + + {messages.error} + + ) } export const PurchaseDetailsPage = ({ @@ -35,11 +48,8 @@ export const PurchaseDetailsPage = ({ }) => { const dispatch = useDispatch() const stage = useSelector(getPurchaseContentFlowStage) - const isUnlocking = [ - PurchaseContentStage.BUY_USDC, - PurchaseContentStage.PURCHASING, - PurchaseContentStage.CONFIRMING_PURCHASE - ].includes(stage) + const error = useSelector(getPurchaseContentError) + const isUnlocking = !error && isContentPurchaseInProgress(stage) const onClickBuy = useCallback(() => { if (isUnlocking) return @@ -91,6 +101,7 @@ export const PurchaseDetailsPage = ({ text={textContent} fullWidth /> + {error ? : null} ) } diff --git a/packages/web/src/pages/upload-page/fields/MultiTrackSidebar.tsx b/packages/web/src/pages/upload-page/fields/MultiTrackSidebar.tsx index 3c7ed0e51b2..f6231b77b79 100644 --- a/packages/web/src/pages/upload-page/fields/MultiTrackSidebar.tsx +++ b/packages/web/src/pages/upload-page/fields/MultiTrackSidebar.tsx @@ -57,7 +57,6 @@ export const MultiTrackSidebar = () => { size='xSmall' fill='accentRed' /> - {/* @ts-expect-error */} {messages.fixErrors} diff --git a/packages/web/src/store/application/ui/buy-audio/sagas.ts b/packages/web/src/store/application/ui/buy-audio/sagas.ts index 0524a5bbb3a..ad4b8fd832d 100644 --- a/packages/web/src/store/application/ui/buy-audio/sagas.ts +++ b/packages/web/src/store/application/ui/buy-audio/sagas.ts @@ -76,9 +76,9 @@ const { cacheAssociatedTokenAccount, cacheTransactionFees, startBuyAudioFlow, - onRampOpened, - onRampSucceeded, - onRampCanceled, + onrampOpened, + onrampSucceeded, + onrampCanceled, swapCompleted, swapStarted, transferStarted, @@ -590,8 +590,8 @@ function* purchaseStep({ // Wait for on ramp finish const result = yield* race({ - success: take(onRampSucceeded), - canceled: take(onRampCanceled) + success: take(onrampSucceeded), + canceled: take(onrampCanceled) }) // If the user didn't complete the on ramp flow, return early @@ -820,7 +820,7 @@ function* transferStep({ */ function* doBuyAudio({ payload: { desiredAudioAmount, estimatedSOL, estimatedUSD } -}: ReturnType) { +}: ReturnType) { const provider = yield* select(getBuyAudioProvider) let userRootWallet = '' try { @@ -1213,7 +1213,7 @@ function* watchCalculateAudioPurchaseInfo() { } function* watchOnRampOpened() { - yield takeLatest(onRampOpened, doBuyAudio) + yield takeLatest(onrampOpened, doBuyAudio) } function* watchStartBuyAudioFlow() { diff --git a/packages/web/src/utils/theme/dark.ts b/packages/web/src/utils/theme/dark.ts index 7143028fe8e..f38aa012b88 100644 --- a/packages/web/src/utils/theme/dark.ts +++ b/packages/web/src/utils/theme/dark.ts @@ -33,6 +33,7 @@ const theme = { '--white': '#32334D', '--darkmode-static-white': 'var(--static-white)', + '--accent-red': '#F9344C', '--accent-red-dark-1': '#C43047', '--special-light-green': '#13c65a', diff --git a/packages/web/src/utils/theme/matrix.ts b/packages/web/src/utils/theme/matrix.ts index eb57bb00605..159bdc3b45f 100644 --- a/packages/web/src/utils/theme/matrix.ts +++ b/packages/web/src/utils/theme/matrix.ts @@ -33,6 +33,8 @@ const theme = { '--white': '#1F211F', '--darkmode-static-white': 'var(--white)', + '--accent-red': '#F9344C', + '--page-header-gradient-color-1': '#4FF069', '--page-header-gradient-color-2': '#09BD51', '--page-header-gradient':