diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index cd0d68b00a2..95ba393d1d5 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -448,6 +448,16 @@ jobs: cd packages/web cp -r ./public/.well-known build 2>/dev/null || true + # Source maps are uploaded to S3 in release/production deploys (see the + # `Move sourcemaps` step in the release/prod jobs). For preview we don't + # ship them anywhere, but they still need to be removed from the bundle — + # individual chunk maps can exceed the Cloudflare 25 MiB per-asset limit + # and break the deploy. Strip them from both bundles before wrangler. + - name: Strip sourcemaps for preview + run: | + cd packages/web + find build build-ssr -type f -name '*.map' -delete || true + - name: Deploy to Cloudflare (Preview) id: deploy env: diff --git a/package-lock.json b/package-lock.json index c8366acbe83..65fe58ea243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7087,6 +7087,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/@ebay/nice-modal-react": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@ebay/nice-modal-react/-/nice-modal-react-1.2.13.tgz", + "integrity": "sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA==", + "license": "MIT", + "peerDependencies": { + "react": ">16.8.0", + "react-dom": ">16.8.0" + } + }, "node_modules/@ecies/ciphers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz", @@ -134601,6 +134611,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs-pro-react-native": "3.9.0", @@ -144050,6 +144061,7 @@ "@audius/sdk": "*", "@cloudflare/kv-asset-handler": "0.2.0", "@coinflowlabs/react": "5.9.1", + "@ebay/nice-modal-react": "^1.2.13", "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@emotion/server": "11.11.0", diff --git a/packages/common/src/services/index.ts b/packages/common/src/services/index.ts index dbe15a18ad0..bd4e3390c15 100644 --- a/packages/common/src/services/index.ts +++ b/packages/common/src/services/index.ts @@ -12,3 +12,4 @@ export * from './track-download' export * from './oauth' export * from './location' export * from './solana' +export * from './nice-modal-bridge' diff --git a/packages/common/src/services/nice-modal-bridge/index.ts b/packages/common/src/services/nice-modal-bridge/index.ts new file mode 100644 index 00000000000..0d2c491415c --- /dev/null +++ b/packages/common/src/services/nice-modal-bridge/index.ts @@ -0,0 +1,74 @@ +/** + * Platform-agnostic bridge to nice-modal-react. + * + * The web and mobile apps install `@ebay/nice-modal-react` and call + * `setNiceModalAdapter({ show, hide })` at app init. Code in `common` + * (sagas, services) calls `showNiceModal(id, props)` / `hideNiceModal(id)` + * to drive nice-modal-react without taking a direct dependency on it. + * + * If a caller fires `showNiceModal` before the adapter is registered + * (e.g. during early app boot or in a test), the call is logged and + * resolves with `undefined` rather than throwing. + * + * `registerNiceModalId(id)` adds a modal id to the bridge allowlist. The + * `share-modal` saga's bridge saga (`watchOpenViaSetVisibility`) reads + * this allowlist and translates legacy `setVisibility(id, true)` actions + * into `showNiceModal(id)` calls. Wave-D registry-pattern modals call + * `registerNiceModalId('Foo')` next to their `NiceModal.register('Foo', ...)` + * so existing redux trigger sites keep working unchanged. + */ + +export type NiceModalShowFn = ( + id: string, + props?: Record +) => Promise + +export type NiceModalHideFn = (id: string) => Promise + +type NiceModalAdapter = { + show: NiceModalShowFn + hide: NiceModalHideFn +} + +let adapter: NiceModalAdapter | null = null + +export const setNiceModalAdapter = (next: NiceModalAdapter) => { + adapter = next +} + +export const showNiceModal: NiceModalShowFn = (id, props) => { + if (!adapter) { + console.warn( + `[nice-modal-bridge] showNiceModal('${id}') called before adapter init` + ) + return Promise.resolve() + } + return adapter.show(id, props) +} + +export const hideNiceModal: NiceModalHideFn = (id) => { + if (!adapter) { + console.warn( + `[nice-modal-bridge] hideNiceModal('${id}') called before adapter init` + ) + return Promise.resolve() + } + return adapter.hide(id) +} + +/** + * Allowlist of modal ids that should bridge from `setVisibility(id, true)` + * to `showNiceModal(id)`. Modals call `registerNiceModalId('Foo')` at + * module scope, alongside their `NiceModal.register('Foo', Component)`. + * + * Backed by a Set so re-imports are idempotent. + */ +const niceModalIds = new Set() + +export const registerNiceModalId = (id: string) => { + niceModalIds.add(id) +} + +export const isNiceModalId = (id: string): boolean => niceModalIds.has(id) + +export { default as niceModalBridgeSagas } from './sagas' diff --git a/packages/common/src/services/nice-modal-bridge/sagas.ts b/packages/common/src/services/nice-modal-bridge/sagas.ts new file mode 100644 index 00000000000..d85813728f7 --- /dev/null +++ b/packages/common/src/services/nice-modal-bridge/sagas.ts @@ -0,0 +1,79 @@ +import { Action } from '@reduxjs/toolkit' +import { takeEvery as untypedTakeEvery } from 'redux-saga/effects' +import { takeEvery, call } from 'typed-redux-saga' + +import { setVisibility } from '~/store/ui/modals/parentSlice' + +import { hideNiceModal, isNiceModalId, showNiceModal } from './index' + +/** + * Bridge saga: translates legacy redux-driven modal trigger actions into + * `showNiceModal(id)` / `hideNiceModal(id)` for any modal id that has + * registered itself via `registerNiceModalId(...)`. + * + * Two trigger shapes need to bridge: + * + * 1. `parentSlice.setVisibility({ modal, visible })` — used by hand-written + * trigger sites that dispatch directly against the parent registry. + * + * 2. `modals/{reducerPath}/open` — emitted by the per-modal hooks created + * by `createModal()`. e.g. `useLeavingAudiusModal().onOpen({ link })` + * dispatches `modals/LeavingAudiusModal/open`. We watch this generic + * action shape so createModal-driven modals migrate without editing + * every trigger site. + * + * This lets NiceModal-managed modals coexist with the legacy modal registry + * — every existing trigger site keeps working unchanged, and migrating a + * modal to NiceModal becomes: + * 1. Wrap with `NiceModal.create(...)` + * 2. `NiceModal.register('X', Component)` + `registerNiceModalId('X')` + * 3. Side-effect import in `registerNiceModals.ts` + * 4. Remove from web `Modals.tsx` / mobile `Drawers.tsx` (avoid double-mount) + * + * Once every caller has been moved to call `showNiceModal` directly, this + * bridge can be deleted. + */ + +const CREATE_MODAL_ACTION_RE = /^modals\/(.+)\/(open|close|closed)$/ + +function* watchOpenViaSetVisibility() { + yield takeEvery( + setVisibility, + function* (action: ReturnType) { + const { modal, visible } = action.payload + if (!isNiceModalId(modal)) return + if (visible === true) { + yield call(showNiceModal, modal) + } else if (visible === false) { + yield call(hideNiceModal, modal) + } + // 'closing' is a transient state used by the legacy AppDrawer for + // its close animation; ignore it here — NiceModal handles its own + // exit animation. + } + ) +} + +function* watchOpenViaCreateModal() { + // Match every action and filter inside. We use plain redux-saga's + // `takeEvery('*', ...)` here because typed-redux-saga's takeEvery typings + // and runtime path don't reliably accept the wildcard pattern. + yield untypedTakeEvery('*', function* (action: Action) { + if (typeof action?.type !== 'string') return + const match = action.type.match(CREATE_MODAL_ACTION_RE) + if (!match) return + const [, modalId, kind] = match + if (!isNiceModalId(modalId)) return + if (kind === 'open') { + yield call(showNiceModal, modalId) + } else { + // 'close' / 'closed' — NiceModal owns its own exit animation, so + // just hide. modal.remove() is called automatically. + yield call(hideNiceModal, modalId) + } + }) +} + +export default function niceModalBridgeSagas() { + return [watchOpenViaSetVisibility, watchOpenViaCreateModal] +} diff --git a/packages/common/src/store/sagas.ts b/packages/common/src/store/sagas.ts index e7d7cef398d..985b727cf29 100644 --- a/packages/common/src/store/sagas.ts +++ b/packages/common/src/store/sagas.ts @@ -6,6 +6,7 @@ // import recoveryEmailSagas from 'common/store/recovery-email/sagas' // import signOutSagas from 'common/store/sign-out/sagas' +import niceModalBridgeSagas from '~/services/nice-modal-bridge/sagas' import { accountSagas } from '~/store/account' import { buyUSDCSagas } from '~/store/buy-usdc' import { sagas as castSagas } from '~/store/cast/sagas' @@ -52,7 +53,8 @@ export const sagas = (_ctx: CommonStoreContext) => ({ duplidateAddConfirmationModalUI: duplicateAddConfirmationModalUISagas, playback: playbackSagas, playbackPosition: playbackPositionSagas, - withdrawUSDC: withdrawUSDCSagas + withdrawUSDC: withdrawUSDCSagas, + niceModalBridge: niceModalBridgeSagas // signOut: signOutSagas // recoveryEmail: recoveryEmailSagas diff --git a/packages/common/src/store/ui/modals/parentSlice.ts b/packages/common/src/store/ui/modals/parentSlice.ts index 53babad3cac..c9edbd4b658 100644 --- a/packages/common/src/store/ui/modals/parentSlice.ts +++ b/packages/common/src/store/ui/modals/parentSlice.ts @@ -28,7 +28,6 @@ export const initialState: BasicModalsState = { TrendingFilter: { isOpen: false }, TrendingRewardsExplainer: { isOpen: false }, SocialProof: { isOpen: false }, - EditFolder: { isOpen: false }, EditTrack: { isOpen: false }, SignOutConfirmation: { isOpen: false }, Overflow: { isOpen: false }, @@ -41,7 +40,6 @@ export const initialState: BasicModalsState = { StripeOnRamp: { isOpen: false }, InboxSettings: { isOpen: false }, CommentSettings: { isOpen: false }, - LabelAccount: { isOpen: false }, PrivateKeyExporter: { isOpen: false }, LockedContent: { isOpen: false }, PlaybackRate: { isOpen: false }, diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index b51748f77a3..7a5076fb250 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -63,7 +63,6 @@ export type Modals = | 'TrendingFilter' | 'TrendingRewardsExplainer' | 'SocialProof' - | 'EditFolder' | 'EditTrack' | 'SignOutConfirmation' | 'Overflow' @@ -83,7 +82,6 @@ export type Modals = | 'PlaybackRate' | 'ProfileActions' | 'PublishContentModal' - | 'LabelAccount' | 'DuplicateAddConfirmation' | 'PremiumContentPurchaseModal' | 'CreateChatModal' diff --git a/packages/common/src/store/ui/share-modal/sagas.ts b/packages/common/src/store/ui/share-modal/sagas.ts index 44bbb406970..78e433a26a2 100644 --- a/packages/common/src/store/ui/share-modal/sagas.ts +++ b/packages/common/src/store/ui/share-modal/sagas.ts @@ -98,6 +98,10 @@ function* watchHandleRequestOpen() { yield takeEvery(requestOpen, handleRequestOpen) } +// The `setVisibility('Share', true)` → `showNiceModal('Share')` bridge now +// lives in the generalized `services/nice-modal-bridge/sagas.ts`, driven by +// the `registerNiceModalId` allowlist that ShareModal opts into. + export default function sagas() { return [watchHandleRequestOpen] } diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9185f4acad8..fc9f5c20e93 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -50,6 +50,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs-pro-react-native": "3.9.0", diff --git a/packages/mobile/src/app/App.tsx b/packages/mobile/src/app/App.tsx index e3aa48c72a2..1dfadc9ee5c 100644 --- a/packages/mobile/src/app/App.tsx +++ b/packages/mobile/src/app/App.tsx @@ -1,7 +1,9 @@ import { useState } from 'react' import { SyncLocalStorageUserProvider } from '@audius/common/api' +import { setNiceModalAdapter } from '@audius/common/services' import { playbackActions } from '@audius/common/store' +import NiceModal from '@ebay/nice-modal-react' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { PortalProvider, PortalHost } from '@gorhom/portal' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' @@ -39,10 +41,15 @@ import { ConnectivityManager } from './ConnectivityManager' import { Drawers } from './Drawers' import ErrorBoundary from './ErrorBoundary' import { ThemeProvider } from './ThemeProvider' +import './registerNiceModals' import { initSentry, navigationIntegration } from './sentry' initSentry() +// Wire the platform-agnostic bridge so common (sagas/services) can drive +// nice-modal-react without depending on the package directly. +setNiceModalAdapter({ show: NiceModal.show, hide: NiceModal.hide }) + const Airplay = Platform.select({ ios: () => require('../components/audio/Airplay').default, android: () => () => null @@ -128,13 +135,19 @@ const App = () => { > - - - - - - - + {/* NiceModal-managed modals (e.g. + ShareDrawer) call useNavigation(), so + the Provider must mount inside + NavigationContainer. */} + + + + + + + + + diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index e49ca6ee369..4e407ca93ff 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -40,7 +40,6 @@ import { QueueDrawer } from 'app/components/queue-drawer' import { RateCtaDrawer } from 'app/components/rate-cta-drawer' import { ReceiveTokensDrawer } from 'app/components/receive-tokens-drawer' import { SendTokensDrawer } from 'app/components/send-tokens-drawer' -import { ShareDrawer } from 'app/components/share-drawer' import { SignOutConfirmationDrawer } from 'app/components/sign-out-confirmation-drawer' import { StripeOnrampDrawer } from 'app/components/stripe-onramp-drawer' import { TransferAudioMobileDrawer } from 'app/components/transfer-audio-mobile-drawer' @@ -113,7 +112,6 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = { ClaimAllRewards: ClaimAllRewardsDrawer, APIRewardsExplainer: ApiRewardsDrawer, TransferAudioMobileWarning: TransferAudioMobileDrawer, - Share: ShareDrawer, DeactivateAccountConfirmation: DeactivateAccountConfirmationDrawer, TrendingGenreSelection: TrendingFilterDrawer, TrendingFilter: TrendingCombinedFilterDrawer, diff --git a/packages/mobile/src/app/registerNiceModals.ts b/packages/mobile/src/app/registerNiceModals.ts new file mode 100644 index 00000000000..67e22d7b058 --- /dev/null +++ b/packages/mobile/src/app/registerNiceModals.ts @@ -0,0 +1,10 @@ +/** + * Side-effect imports for nice-modal-react registrations. + * + * Each imported module calls `NiceModal.register(id, Component)` at module + * scope. Importing this file from `App.tsx` ensures all registered modals + * are available before anything tries to `showNiceModal(id)`. + * + * Add new NiceModal-managed modals here as they migrate. + */ +import 'app/components/share-drawer/ShareDrawer' diff --git a/packages/mobile/src/components/share-drawer/ShareDrawer.tsx b/packages/mobile/src/components/share-drawer/ShareDrawer.tsx index 00eba92d5f1..2a4826560d1 100644 --- a/packages/mobile/src/components/share-drawer/ShareDrawer.tsx +++ b/packages/mobile/src/components/share-drawer/ShareDrawer.tsx @@ -3,12 +3,14 @@ import React, { useCallback, useEffect, useRef } from 'react' import { useCurrentUserId } from '@audius/common/api' import { useShareAction } from '@audius/common/hooks' import { Name, ShareSource } from '@audius/common/models' +import { registerNiceModalId } from '@audius/common/services' import { collectionsSocialActions, tracksSocialActions, usersSocialActions, shareModalUISelectors } from '@audius/common/store' +import NiceModal, { useModal } from '@ebay/nice-modal-react' import Clipboard from '@react-native-clipboard/clipboard' import { Linking } from 'react-native' import ViewShot from 'react-native-view-shot' @@ -31,9 +33,8 @@ import { make, track } from 'app/services/analytics' import { makeStyles } from 'app/styles' import { useThemeColors } from 'app/utils/theme' -import ActionDrawer from '../action-drawer' +import { ActionDrawerWithoutRedux } from '../action-drawer/ActionDrawerWithoutRedux' import { Text } from '../core' -import { useDrawerState } from '../drawer/AppDrawer' import { ShareToStorySticker } from './ShareToStorySticker' import { messages } from './messages' @@ -71,13 +72,17 @@ const useStyles = makeStyles(({ spacing }) => ({ } })) -export const ShareDrawer = () => { +export const ShareDrawer = NiceModal.create(() => { const styles = useStyles() const viewShotRef = useRef(null) as React.RefObject const navigation = useNavigation() const sendShareAction = useShareAction() - const { onClose } = useDrawerState('Share') + const modal = useModal() + const isOpen = modal.visible + const onClose = useCallback(() => { + modal.hide() + }, [modal]) const { onClose: onCloseNowPlaying } = useDrawer('NowPlaying') const { secondary } = useThemeColors() @@ -277,9 +282,10 @@ export const ShareDrawer = () => { /> ) : null} - { {messages.hiddenPlaylistShareHelperText} ) : null} - + ) -} +}) + +// Register so saga code (and anything else outside React) can open it via +// `showNiceModal('Share')`. The id is also added to the nice-modal bridge +// allowlist so legacy `setVisibility('Share', true)` dispatches translate +// to `showNiceModal('Share')`. +NiceModal.register('Share', ShareDrawer) +registerNiceModalId('Share') diff --git a/packages/mobile/src/store/sagas.ts b/packages/mobile/src/store/sagas.ts index 625caecc5d7..b720321df76 100644 --- a/packages/mobile/src/store/sagas.ts +++ b/packages/mobile/src/store/sagas.ts @@ -1,3 +1,4 @@ +import { niceModalBridgeSagas } from '@audius/common/services' import { buyUSDCSagas, castSagas, @@ -82,6 +83,7 @@ export default function* rootSaga() { ...stripeModalUISagas(), ...modalsSagas(), + ...niceModalBridgeSagas(), // Pages ...trackPageSagas(), diff --git a/packages/web/package.json b/packages/web/package.json index 13434b2fab3..0cfdb9444a3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -75,6 +75,7 @@ "@audius/sdk": "*", "@cloudflare/kv-asset-handler": "0.2.0", "@coinflowlabs/react": "5.9.1", + "@ebay/nice-modal-react": "^1.2.13", "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@emotion/server": "11.11.0", diff --git a/packages/web/src/app/AppProviders.tsx b/packages/web/src/app/AppProviders.tsx index 663fbc63e85..3fdfe68e58d 100644 --- a/packages/web/src/app/AppProviders.tsx +++ b/packages/web/src/app/AppProviders.tsx @@ -1,7 +1,9 @@ import { ReactNode, useState, useMemo } from 'react' import { FrostedSurfaceIntensity, ThemePalette } from '@audius/common/models' +import { setNiceModalAdapter } from '@audius/common/services' import { MediaProvider } from '@audius/harmony/src/contexts' +import NiceModal from '@ebay/nice-modal-react' import { QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { Provider as ReduxProvider } from 'react-redux' @@ -26,8 +28,13 @@ import { } from 'utils/theme/theme' import { wagmiAdapter } from './ReownAppKitModal' +import './registerNiceModals' import { createRoutes } from './routes' +// Wire the platform-agnostic bridge so common (sagas/services) can drive +// nice-modal-react without depending on the package directly. +setNiceModalAdapter({ show: NiceModal.show, hide: NiceModal.hide }) + type AppProvidersProps = { children?: ReactNode } @@ -81,6 +88,9 @@ export const AppProviders = ({ children }: AppProvidersProps) => { + {/* is mounted inside routes.tsx, deeper + in the tree, so NiceModal-managed modals can read app + contexts (AudiusQueryProvider, ToastContext, etc). */} diff --git a/packages/web/src/app/registerNiceModals.ts b/packages/web/src/app/registerNiceModals.ts new file mode 100644 index 00000000000..d1b6b7b99cf --- /dev/null +++ b/packages/web/src/app/registerNiceModals.ts @@ -0,0 +1,53 @@ +/** + * Side-effect imports for nice-modal-react registrations. + * + * Each imported module calls `NiceModal.register(id, Component)` and + * `registerNiceModalId(id)` at module scope. Importing this file from + * `AppProviders` ensures all registered modals are available before + * anything tries to `showNiceModal(id)`. + * + * Add new NiceModal-managed modals here as they migrate. + */ +import 'components/add-cash-modal/AddCashModal' +import 'components/add-to-collection/desktop/AddToCollectionModal' +import 'components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal' +import 'components/artist-pick-modal/ArtistPickModal' +import 'components/buy-sell-modal/BuySellModal' +import 'components/coinflow-onramp-modal/CoinflowOnrampModal' +import 'components/delete-playlist-confirmation-modal/DeletePlaylistConfirmationModal' +import 'components/delete-track-confirmation-modal/DeleteTrackConfirmationModal' +import 'components/download-track-archive-modal/DownloadTrackArchiveModal' +import 'components/duplicate-add-confirmation-modal/DuplicateAddConfirmationModal' +import 'components/early-release-confirmation-modal/EarlyReleaseConfirmationModal' +import 'components/edit-access-confirmation-modal/EditAccessConfirmationModal' +import 'components/finalize-winners-confirmation-modal/FinalizeWinnersConfirmationModal' +import 'components/hide-confirmation-modal/HideContentConfirmationModal' +import 'components/host-remix-contest-modal/HostRemixContestModal' +import 'components/inbox-unavailable-modal/InboxUnavailableModal' +import 'components/leaving-audius-modal/LeavingAudiusModal' +import 'components/locked-content-modal/LockedContentModal' +import 'components/payout-wallet-modal/PayoutWalletModal' +import 'components/premium-content-purchase-modal/PremiumContentPurchaseModal' +import 'components/publish-confirmation-modal/PublishConfirmationModal' +import 'components/receive-tokens-modal/ReceiveTokensModal' +import 'components/replace-track-confirmation-modal/ReplaceTrackConfirmationModal' +import 'components/replace-track-progress-modal/ReplaceTrackProgressModal' +import 'components/rewards/modals/ClaimAllRewardsModal' +import 'components/rewards/modals/TopAPI' +import 'components/send-tokens-modal/SendTokensModal' +import 'components/share-modal/ShareModal' +import 'components/transaction-details-modal/TransactionDetailsModal' +import 'components/upload-confirmation-modal/UploadConfirmationModal' +import 'components/usdc-purchase-details-modal/USDCPurchaseDetailsModal' +import 'components/usdc-transaction-details-modal/USDCTransactionDetailsModal' +import 'components/user-badges/TierExplainerModal' +import 'components/wait-for-download-modal/WaitForDownloadModal' +import 'components/welcome-modal/WelcomeModal' +import 'components/withdraw-usdc-modal/WithdrawUSDCModal' +import 'components/withdraw-usdc-modal/components/CoinflowWithdrawModal' +import 'pages/audio-page/components/modals/AudioBreakdownModal' +import 'pages/audio-page/components/modals/ConnectedWalletsModal' +import 'pages/audio-page/components/modals/TransferAudioMobileDrawer' +import 'pages/chat-page/components/ChatBlastModal' +import 'pages/fan-club-detail-page/components/ClaimVestedCoinsModal' +import 'pages/rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal' diff --git a/packages/web/src/app/routes.tsx b/packages/web/src/app/routes.tsx index 0d557b9b80f..9e643b9f085 100644 --- a/packages/web/src/app/routes.tsx +++ b/packages/web/src/app/routes.tsx @@ -3,6 +3,7 @@ import React, { lazy, Suspense, useEffect } from 'react' import { SyncLocalStorageUserProvider } from '@audius/common/api' import { route } from '@audius/common/utils' import { CoinflowPurchaseProtection } from '@coinflowlabs/react' +import NiceModal from '@ebay/nice-modal-react' import type { RouteObject } from 'react-router' import { Navigate, Outlet, useNavigate } from 'react-router' @@ -80,14 +81,19 @@ const RootLayout = () => { - - - - - + {/* NiceModal-managed modals (e.g. ShareModal) + read AudiusQueryProvider + ToastContext, so the + Provider must mount inside them. */} + + + + + + + diff --git a/packages/web/src/components/add-cash-modal/AddCashModal.tsx b/packages/web/src/components/add-cash-modal/AddCashModal.tsx index 13b862459f3..90a6409d173 100644 --- a/packages/web/src/components/add-cash-modal/AddCashModal.tsx +++ b/packages/web/src/components/add-cash-modal/AddCashModal.tsx @@ -3,12 +3,13 @@ import { useCallback, useEffect, useState } from 'react' import { DEFAULT_PURCHASE_AMOUNT_CENTS } from '@audius/common/hooks' import { walletMessages } from '@audius/common/messages' import { PurchaseMethod, PurchaseVendor } from '@audius/common/models' +import { registerNiceModalId } from '@audius/common/services' import { buyUSDCActions, buyUSDCSelectors, - BuyUSDCStage, - useAddCashModal + BuyUSDCStage } from '@audius/common/store' +import NiceModal, { useModal } from '@ebay/nice-modal-react' import { useDispatch, useSelector } from 'react-redux' import { AddCash } from 'components/add-cash/AddCash' @@ -21,8 +22,10 @@ const { getBuyUSDCFlowStage } = buyUSDCSelectors type Page = 'add-cash' | 'crypto-transfer' -export const AddCashModal = () => { - const { isOpen, onClose } = useAddCashModal() +export const AddCashModal = NiceModal.create(() => { + const modal = useModal() + const isOpen = modal.visible + const onClose = useCallback(() => modal.hide(), [modal]) const dispatch = useDispatch() const buyUSDCStage = useSelector(getBuyUSDCFlowStage) const inProgress = [ @@ -91,4 +94,7 @@ export const AddCashModal = () => { )} ) -} +}) + +NiceModal.register('AddCashModal', AddCashModal) +registerNiceModalId('AddCashModal') diff --git a/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx b/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx index 20934b0c894..71cab821f88 100644 --- a/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx +++ b/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useCurrentUserId, @@ -12,6 +12,7 @@ import { SquareSizes, Collection } from '@audius/common/models' +import { registerNiceModalId } from '@audius/common/services' import { cacheCollectionsActions, addToCollectionUISelectors, @@ -27,12 +28,12 @@ import { Tooltip, Image } from '@audius/harmony' +import NiceModal, { useModal } from '@ebay/nice-modal-react' import cn from 'classnames' import { capitalize } from 'lodash' import InfiniteScroll from 'react-infinite-scroller' import { useDispatch, useSelector } from 'react-redux' -import { useModalState } from 'common/hooks/useModalState' import SearchBar from 'components/search-bar/SearchBar' import { useCollectionCoverArt } from 'hooks/useCollectionCoverArt' @@ -56,10 +57,11 @@ const getMessages = (collectionType: 'album' | 'playlist') => ({ hiddenAdd: `You cannot add hidden tracks to a public ${collectionType}.` }) -const AddToCollectionModal = () => { +const AddToCollectionModal = NiceModal.create(() => { const dispatch = useDispatch() - const [isOpen, setIsOpen] = useModalState('AddToCollection') + const modal = useModal() + const handleClose = useCallback(() => modal.hide(), [modal]) const collectionType = useSelector(getCollectionType) const trackId = useSelector(getTrackId) const trackTitle = useSelector(getTrackTitle) @@ -135,7 +137,7 @@ const AddToCollectionModal = () => { } } - setIsOpen(false) + handleClose() } const handleCreateCollection = () => { @@ -149,16 +151,16 @@ const AddToCollectionModal = () => { 'toast' ) ) - setIsOpen(false) + handleClose() } return ( setIsOpen(false)} + onClose={handleClose} allowScroll={false} bodyClassName={styles.modalBody} headerContainerClassName={styles.modalHeader} @@ -204,7 +206,7 @@ const AddToCollectionModal = () => { ) -} +}) type CollectionItemProps = { collectionType: 'album' | 'playlist' @@ -244,4 +246,7 @@ const CollectionItem = ({ ) } +NiceModal.register('AddToCollection', AddToCollectionModal) +registerNiceModalId('AddToCollection') + export default AddToCollectionModal diff --git a/packages/web/src/components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal.tsx b/packages/web/src/components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal.tsx index c84a4d424bd..fe932682f48 100644 --- a/packages/web/src/components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal.tsx +++ b/packages/web/src/components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { registerNiceModalId } from '@audius/common/services' import { useAlbumTrackRemoveConfirmationModal, cacheCollectionsActions @@ -13,6 +14,7 @@ import { ModalTitle, ModalFooter } from '@audius/harmony' +import NiceModal, { useModal } from '@ebay/nice-modal-react' import { useDispatch } from 'react-redux' const messages = { @@ -24,15 +26,18 @@ const messages = { release: 'Remove Track From Album' } -export const AlbumTrackRemoveConfirmationModal = () => { +export const AlbumTrackRemoveConfirmationModal = NiceModal.create(() => { + const modal = useModal() const { - isOpen, - onClose, data: { trackId, playlistId, timestamp } } = useAlbumTrackRemoveConfirmationModal() const dispatch = useDispatch() + const handleClose = useCallback(() => { + modal.hide() + }, [modal]) + const handleConfirm = useCallback(() => { if (trackId && playlistId && timestamp) { dispatch( @@ -43,11 +48,11 @@ export const AlbumTrackRemoveConfirmationModal = () => { ) ) } - onClose() - }, [dispatch, onClose, playlistId, timestamp, trackId]) + handleClose() + }, [dispatch, handleClose, playlistId, timestamp, trackId]) return ( - + @@ -56,7 +61,7 @@ export const AlbumTrackRemoveConfirmationModal = () => { {messages.description2} - diff --git a/packages/web/src/components/leaving-audius-modal/LeavingAudiusModal.tsx b/packages/web/src/components/leaving-audius-modal/LeavingAudiusModal.tsx index 4a70bfa66fe..2b15cec679e 100644 --- a/packages/web/src/components/leaving-audius-modal/LeavingAudiusModal.tsx +++ b/packages/web/src/components/leaving-audius-modal/LeavingAudiusModal.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { registerNiceModalId } from '@audius/common/services' import { useLeavingAudiusModal } from '@audius/common/store' import { Modal, @@ -13,6 +14,7 @@ import { Text, Hint } from '@audius/harmony' +import NiceModal, { useModal } from '@ebay/nice-modal-react' import styles from './LeavingAudiusModal.module.css' @@ -23,19 +25,28 @@ const messages = { visitSite: 'Visit Site' } -export const LeavingAudiusModal = () => { - const { isOpen, data, onClose, onClosed } = useLeavingAudiusModal() +export const LeavingAudiusModal = NiceModal.create(() => { + const modal = useModal() + // Continue reading the link payload from the redux slice that the + // useLeavingAudiusModal createModal hook stores; visibility is now owned + // by NiceModal via useModal(). + const { data } = useLeavingAudiusModal() const { link } = data + + const handleClose = useCallback(() => { + modal.hide() + }, [modal]) + const handleOpen = useCallback(() => { window.open(link, '_blank', 'noreferrer,noopener') - onClose() - }, [link, onClose]) + handleClose() + }, [link, handleClose]) + return ( @@ -48,7 +59,11 @@ export const LeavingAudiusModal = () => { - + ) } diff --git a/packages/web/src/store/application/ui/editFolderModal/selectors.ts b/packages/web/src/store/application/ui/editFolderModal/selectors.ts deleted file mode 100644 index 32c916f8d09..00000000000 --- a/packages/web/src/store/application/ui/editFolderModal/selectors.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AppState } from 'store/types' - -const getBaseState = (state: AppState) => state.application.ui.editFolderModal - -export const getFolderId = (state: AppState) => { - return getBaseState(state).folderId -} diff --git a/packages/web/src/store/application/ui/editFolderModal/slice.ts b/packages/web/src/store/application/ui/editFolderModal/slice.ts deleted file mode 100644 index 960527c0487..00000000000 --- a/packages/web/src/store/application/ui/editFolderModal/slice.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -export type EditFolderModalState = { - folderId: string | null -} - -const initialState: EditFolderModalState = { - folderId: null -} - -const slice = createSlice({ - name: 'application/ui/editFolderModal', - initialState, - reducers: { - setFolderId: (state, action: PayloadAction) => { - state.folderId = action.payload - } - } -}) - -export const { setFolderId } = slice.actions - -export default slice.reducer diff --git a/packages/web/src/store/reducers.ts b/packages/web/src/store/reducers.ts index 69d554374ae..bb8f53410cc 100644 --- a/packages/web/src/store/reducers.ts +++ b/packages/web/src/store/reducers.ts @@ -14,7 +14,6 @@ import unfollowConfirmation from 'components/unfollow-confirmation-modal/store/r import visualizer from 'pages/visualizer/store/slice' import appCTAModal from 'store/application/ui/app-cta-modal/slice' import cookieBanner from 'store/application/ui/cookieBanner/reducer' -import editFolderModal from 'store/application/ui/editFolderModal/slice' import scrollLock from 'store/application/ui/scrollLock/reducer' import userListModal from 'store/application/ui/userListModal/slice' import dragndrop from 'store/dragndrop/slice' @@ -47,7 +46,6 @@ const createRootReducer = () => { ui: combineReducers({ appCTAModal, cookieBanner, - editFolderModal, embedModal, firstUploadModal, scrollLock, diff --git a/packages/web/src/store/sagas.ts b/packages/web/src/store/sagas.ts index fe16f7f9912..3d9eeb85fc5 100644 --- a/packages/web/src/store/sagas.ts +++ b/packages/web/src/store/sagas.ts @@ -1,3 +1,4 @@ +import { niceModalBridgeSagas } from '@audius/common/services' import { buyUSDCSagas, castSagas, @@ -80,6 +81,7 @@ export default function* rootSaga() { trackPageSagas(), modalsSagas(), + niceModalBridgeSagas(), // Cache collectionsSagas(), diff --git a/packages/web/src/store/types.ts b/packages/web/src/store/types.ts index df0e67641c1..1404665f751 100644 --- a/packages/web/src/store/types.ts +++ b/packages/web/src/store/types.ts @@ -26,7 +26,6 @@ import { ErrorState } from 'store/errors/reducers' import { BackendState } from '../common/store/backend/types' import { CookieBannerState } from './application/ui/cookieBanner/types' -import { EditFolderModalState } from './application/ui/editFolderModal/slice' import { ScrollLockState } from './application/ui/scrollLock/types' import { UserListModalState } from './application/ui/userListModal/types' import { DragnDropState } from './dragndrop/slice' @@ -52,7 +51,6 @@ export type AppState = CommonState & { appCTAModal: ReturnType averageColor: ReturnType cookieBanner: CookieBannerState - editFolderModal: EditFolderModalState embedModal: EmbedModalState firstUploadModal: FirstUploadModalState scrollLock: ScrollLockState