Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/common/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './track-download'
export * from './oauth'
export * from './location'
export * from './solana'
export * from './nice-modal-bridge'
74 changes: 74 additions & 0 deletions packages/common/src/services/nice-modal-bridge/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
) => Promise<unknown>

export type NiceModalHideFn = (id: string) => Promise<unknown>

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<string>()

export const registerNiceModalId = (id: string) => {
niceModalIds.add(id)
}

export const isNiceModalId = (id: string): boolean => niceModalIds.has(id)

export { default as niceModalBridgeSagas } from './sagas'
79 changes: 79 additions & 0 deletions packages/common/src/services/nice-modal-bridge/sagas.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setVisibility>) {
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]
}
4 changes: 3 additions & 1 deletion packages/common/src/store/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions packages/common/src/store/ui/modals/parentSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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 },
Expand Down
2 changes: 0 additions & 2 deletions packages/common/src/store/ui/modals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export type Modals =
| 'TrendingFilter'
| 'TrendingRewardsExplainer'
| 'SocialProof'
| 'EditFolder'
| 'EditTrack'
| 'SignOutConfirmation'
| 'Overflow'
Expand All @@ -83,7 +82,6 @@ export type Modals =
| 'PlaybackRate'
| 'ProfileActions'
| 'PublishContentModal'
| 'LabelAccount'
| 'DuplicateAddConfirmation'
| 'PremiumContentPurchaseModal'
| 'CreateChatModal'
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/store/ui/share-modal/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
1 change: 1 addition & 0 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 20 additions & 7 deletions packages/mobile/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -128,13 +135,19 @@ const App = () => {
>
<BottomSheetModalProvider>
<CommentDrawerProvider>
<Toasts />
<Airplay />
<RootScreen />
<Drawers />
<NotificationReminder />
<RateCtaReminder />
<PortalHost name='ChatReactionsPortal' />
{/* NiceModal-managed modals (e.g.
ShareDrawer) call useNavigation(), so
the Provider must mount inside
NavigationContainer. */}
<NiceModal.Provider>
<Toasts />
<Airplay />
<RootScreen />
<Drawers />
<NotificationReminder />
<RateCtaReminder />
<PortalHost name='ChatReactionsPortal' />
</NiceModal.Provider>
</CommentDrawerProvider>
</BottomSheetModalProvider>
<PortalHost name='DrawerPortal' />
Expand Down
2 changes: 0 additions & 2 deletions packages/mobile/src/app/Drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -113,7 +112,6 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = {
ClaimAllRewards: ClaimAllRewardsDrawer,
APIRewardsExplainer: ApiRewardsDrawer,
TransferAudioMobileWarning: TransferAudioMobileDrawer,
Share: ShareDrawer,
DeactivateAccountConfirmation: DeactivateAccountConfirmationDrawer,
TrendingGenreSelection: TrendingFilterDrawer,
TrendingFilter: TrendingCombinedFilterDrawer,
Expand Down
10 changes: 10 additions & 0 deletions packages/mobile/src/app/registerNiceModals.ts
Original file line number Diff line number Diff line change
@@ -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'
Loading
Loading