From 5fcfa7d5fbabc0b5de8fbb1e535b0c64d6501d44 Mon Sep 17 00:00:00 2001 From: Mounir Dhahri Date: Fri, 8 Nov 2024 17:04:07 +0100 Subject: [PATCH] feat: refactor navigation infra (#10977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add feature flag * feat: prepare main stack * feat: add tabs stack * feat: hook up the different screens * feat: update other navigate methods to support new navigation infra * chore: add hides back button to tabs * feat: update tabs styling * chore: remove console.log * chore: update live sale logic * chore: remove unused prop * feat: remove tab screens from shared routes * chore: remove comment * chore: update screen wrapper props * feat: add tab screens to the individual tabs * chore: add todo comment * fix: imports on Tabs.tsx * fix: ts * fix: borken types for tabs * feat: support switchTab * fix: add missing isNavigationReady to deep linking 💣🚨 * chore: hide relay logs when metaflag is disabled * chore: better types (thanks Carlos) * feat/fix: register native module * chore: improve presentation of dev settings * feat: default navigation header and other UI fixes * chore(side-quest): improve OTP UX * feat: add new patterns example on dev menu * chore: better directories * fix: more UX fixes * chore: better DX * feat: add staging indicator * fix: more UI fixes for headers * chore: better bottom tabs hiding logic * fix: more UI issues * fix: consignment inquiry UI issues * chore: decouple fetch profile * fix: more broken paddings * fix: patch broken react-native-screens * fix: more screen header issues * fix: more navigation issues * fix: UI issues on the Inbox * chore: extract spinner * feat: better bottom tabs * chore: bring back tab button count * fix: broken tests * fix: more broken UI * fix: more tests * fix: skip migrations test TODO: To be fixed later * fix: more fixes to the dev menu * chore: align staging indicator * fix: broken tests * chore: minor UI fixes * chore: refactor - make routes a variable instead of a function * fix: old navigation headers logic * chore: remove hack * chore: self review [round 1] * fix: more broken tests * chore: better nav stack logic * fix: remove patch * fix: city guide missing back button * fix: remove post-install * chore: address review comments * fix: make inbox full bleed * feat: support replace * feat: support only show in tab name * fix: minor tweaks * chore: bring back state change instrumentation * chore: bring back tap on search to focus * fix: align simple page with header with current designs * fix: align text title on android * Revert "fix: align simple page with header with current designs" This reverts commit 4cb2960e41470044cb8eb30374dc1ffc424ef92e. * fix: align (again) simple page with header with current designs * fix: annoying spinner on add artwork to my collection * feat: introduce new modal stack * fix: auction bidding modal * fix: make sure privacy and terms are displayed in modals * fix: typescript * fix: broken test * fix: address review comments * fix: skip not needed broken test * fix: bring back old presentation of screens in app registry --- ios/Podfile.lock | 4 +- package.json | 2 +- src/app/App.tsx | 26 +- src/app/AppRegistry.tsx | 227 +++++++++++---- src/app/Components/ArtsyWebView.tsx | 16 +- src/app/Components/Containers/Inbox.tsx | 43 ++- .../FancyModal/FancyModalContext.tsx | 2 +- src/app/Components/PageWithSimpleHeader.tsx | 26 +- src/app/NativeModules/ArtsyNativeModule.tsx | 2 +- .../AuthenticatedRoutes/HomeTab.tsx | 15 + .../AuthenticatedRoutes/InboxTab.tsx | 20 ++ .../AuthenticatedRoutes/NativeScreens.tsx | 45 +++ .../AuthenticatedRoutes/ProfileTab.tsx | 16 ++ .../AuthenticatedRoutes/SearchTab.tsx | 16 ++ .../AuthenticatedRoutes/SellTab.tsx | 16 ++ .../AuthenticatedRoutes/SharedRoutes.tsx | 46 +++ .../AuthenticatedRoutes/StackNavigator.tsx | 121 ++++++++ .../Navigation/AuthenticatedRoutes/Tabs.tsx | 80 ++++++ src/app/Navigation/Navigation.tsx | 118 ++++++++ src/app/Navigation/UnauthenticatedRoutes.tsx | 22 ++ src/app/Navigation/Utils/LargeHeaderView.tsx | 12 + src/app/Navigation/Utils/isHeaderShown.tsx | 5 + src/app/Navigation/Utils/isModalScreen.tsx | 5 + .../_TO_BE_DELETED_Screens/LoginScreen.tsx | 23 ++ .../_TO_BE_DELETED_Screens/SignUpScreen.tsx | 22 ++ src/app/Scenes/About/About.tsx | 13 +- .../Components/ArticleWebViewScreen.tsx | 7 +- .../Components/ArtworkStickyBottomContent.tsx | 4 +- .../InquirySuccessNotification.tsx | 4 +- .../ArtworkAttributionClassFAQ.tsx | 41 +-- .../Scenes/ArtworkMedium/ArtworkMedium.tsx | 41 +-- .../AuctionResult/AuctionResult.tests.tsx | 16 +- .../Scenes/AuctionResult/AuctionResult.tsx | 38 ++- .../Scenes/BottomTabs/BottomTabs.tests.tsx | 4 +- .../BottomTabs/BottomTabsButton.tests.tsx | 6 +- .../Scenes/BottomTabs/BottomTabsButton.tsx | 81 +++++- .../Scenes/City/Components/Event/index.tsx | 9 +- .../__tests__/useCompleteProfile.tests.tsx | 9 +- .../hooks/useCompleteProfile.ts | 4 +- src/app/Scenes/HomeView/HomeView.tsx | 4 +- .../Components/Conversations/Composer.tsx | 2 + .../Components/Conversations/Messages.tsx | 2 +- .../OfferSubmittedModal.tests.tsx | 5 +- .../Conversations/OfferSubmittedModal.tsx | 6 +- .../Conversations/OpenInquiryModalButton.tsx | 8 +- .../Inbox/Screens/Conversation.tests.tsx | 9 +- src/app/Scenes/Inbox/Screens/Conversation.tsx | 159 +++++------ .../Inbox/Screens/ConversationDetails.tsx | 75 ++--- src/app/Scenes/Map/MapRenderer.tsx | 2 +- src/app/Scenes/MyAccount/MyAccount.tests.tsx | 6 +- src/app/Scenes/MyAccount/MyAccount.tsx | 31 +- .../MyAccount/MyAccountEditEmail.tests.tsx | 65 +---- .../Scenes/MyAccount/MyAccountEditEmail.tsx | 127 ++++++--- .../MyAccount/MyAccountEditPassword.tests.tsx | 17 +- .../MyAccount/MyAccountEditPassword.tsx | 124 ++++---- .../Scenes/MyAccount/MyAccountEditPhone.tsx | 91 ++++-- .../MyAccountEditPriceRange.tests.tsx | 11 +- .../MyAccount/MyAccountEditPriceRange.tsx | 119 +++++--- src/app/Scenes/MyBids/Components/SaleCard.tsx | 14 +- .../Screens/Artwork/MyCollectionArtwork.tsx | 3 +- ...ollectionArtworkFormDeleteArtworkModal.tsx | 5 +- .../MyCollectionAddCollectedArtists.tsx | 13 +- src/app/Scenes/MyProfile/MyProfile.tsx | 6 +- .../Scenes/MyProfile/MyProfileEditForm.tsx | 57 ++-- .../MyProfile/MyProfileHeader.tests.tsx | 4 +- src/app/Scenes/MyProfile/MyProfileHeader.tsx | 59 ++-- src/app/Scenes/MyProfile/MyProfilePayment.tsx | 129 +++++---- .../MyProfilePaymentNewCreditCard.tsx | 269 ++++++++++-------- .../MyProfile/MyProfilePushNotifications.tsx | 76 +++-- .../MyProfile/MyProfileSettings.tests.tsx | 7 + .../Scenes/MyProfile/MyProfileSettings.tsx | 39 ++- .../Onboarding/Auth2/scenes/LoginOTPStep.tsx | 1 + .../OrderDetails/Components/OrderDetails.tsx | 203 ++++++------- .../OrderDetails/OrderDetails.tests.tsx | 111 +++----- src/app/Scenes/OrderHistory/OrderHistory.tsx | 163 +++++------ .../Components/PartnerShowRailItem.tsx | 7 +- .../Partner/Components/PartnerShows.tsx | 8 +- .../Partner/Components/PartnerShowsRail.tsx | 37 ++- .../Scenes/PrivacyRequest/PrivacyRequest.tsx | 73 +++-- src/app/Scenes/Sale/Sale.tests.tsx | 16 +- src/app/Scenes/Sale/Sale.tsx | 8 +- src/app/Scenes/SaleInfo/SaleInfo.tsx | 14 +- .../SavedSearchAlertForm.tests.tsx | 14 +- .../SavedSearchAlert/SavedSearchAlertForm.tsx | 5 +- src/app/Scenes/Search/Search.tests.tsx | 2 +- src/app/Scenes/Search/Search.tsx | 15 +- .../ConsignmentInquiryForm.tsx | 4 +- .../ConsignmentInquiryScreen.tsx | 5 +- .../SellWithArtsy/SellWithArtsyHome.tsx | 7 +- .../UploadPhotos/utils/index.tsx | 5 +- src/app/Scenes/Show/Show.tsx | 11 +- src/app/routes.ts | 5 - src/app/store/GlobalStore.tsx | 4 + src/app/store/__tests__/AuthModel.tests.ts | 2 - src/app/store/__tests__/migration.tests.ts | 19 -- src/app/store/config/features.ts | 5 + .../DevMenu/Components/NavButtons.tsx | 2 +- src/app/system/devTools/DevMenu/DevMenu.tsx | 93 +++--- src/app/system/navigation/navigate.tests.tsx | 1 + src/app/system/navigation/navigate.ts | 80 +++++- src/app/system/navigation/routes.tests.ts | 44 ++- src/app/system/relay/defaultEnvironment.ts | 5 +- src/app/utils/bottomTabsHelper.tsx | 24 +- src/app/utils/hooks/useBackHandler.tests.ts | 31 +- src/app/utils/hooks/useBackHandler.ts | 7 + src/app/utils/hooks/useSelectedTab.ts | 9 + src/app/utils/useDeepLinks.ts | 12 +- src/app/utils/useTabBarBadge.ts | 27 ++ src/setupJest.tsx | 2 + yarn.lock | 39 +-- 110 files changed, 2451 insertions(+), 1420 deletions(-) create mode 100644 src/app/Navigation/AuthenticatedRoutes/HomeTab.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/InboxTab.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/NativeScreens.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/ProfileTab.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/SearchTab.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/SellTab.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/SharedRoutes.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/StackNavigator.tsx create mode 100644 src/app/Navigation/AuthenticatedRoutes/Tabs.tsx create mode 100644 src/app/Navigation/Navigation.tsx create mode 100644 src/app/Navigation/UnauthenticatedRoutes.tsx create mode 100644 src/app/Navigation/Utils/LargeHeaderView.tsx create mode 100644 src/app/Navigation/Utils/isHeaderShown.tsx create mode 100644 src/app/Navigation/Utils/isModalScreen.tsx create mode 100644 src/app/Navigation/_TO_BE_DELETED_Screens/LoginScreen.tsx create mode 100644 src/app/Navigation/_TO_BE_DELETED_Screens/SignUpScreen.tsx create mode 100644 src/app/utils/useTabBarBadge.ts diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 61667546acf..a5bf154dab9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1452,7 +1452,7 @@ PODS: - RCT-Folly (= 2022.05.16.00) - React-Core - ReactCommon/turbomodule/core - - RNScreens (3.34.0): + - RNScreens (3.35.0): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -2187,7 +2187,7 @@ SPEC CHECKSUMS: RNPermissions: 23abdfa77d3cd3700b181a2d47f29939c6878ce6 RNReactNativeHapticFeedback: b83bfb4b537bdd78eb4f6ffe63c6884f7b049ead RNReanimated: 8a4d86eb951a4a99d8e86266dc71d7735c0c30a9 - RNScreens: 29418ceffb585b8f0ebd363de304288c3dce8323 + RNScreens: 69850d4519d95b9359cec15a67bb5f9d0bd75262 RNSentry: 3feba366b62cbf306d9f8be629beb516ed811f65 RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a diff --git a/package.json b/package.json index b8051ffe851..085f82736dd 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "react-native-reanimated-zoom": "0.3.3", "react-native-render-html": "6.3.4", "react-native-safe-area-context": "3.4.0", - "react-native-screens": "3.34.0", + "react-native-screens": "3.35.0", "react-native-shake": "5.5.2", "react-native-share": "10.0.2", "react-native-svg": "14.1.0", diff --git a/src/app/App.tsx b/src/app/App.tsx index 5c1c89522c7..10e56314b43 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,6 @@ import { GoogleSignin } from "@react-native-google-signin/google-signin" import * as Sentry from "@sentry/react-native" +import { Navigation } from "app/Navigation/Navigation" import { GlobalStore, unsafe__getEnvironment, unsafe_getDevToggle } from "app/store/GlobalStore" import { codePushOptions } from "app/system/codepush" import { AsyncStorageDevtools } from "app/system/devTools/AsyncStorageDevTools" @@ -10,6 +11,7 @@ import { setupSentry } from "app/system/errorReporting/setupSentry" import { ModalStack } from "app/system/navigation/ModalStack" import { usePurgeCacheOnAppUpdate } from "app/system/relay/usePurgeCacheOnAppUpdate" import { useDevToggle } from "app/utils/hooks/useDevToggle" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { addTrackingProvider } from "app/utils/track" import { SEGMENT_TRACKING_PROVIDER, @@ -93,6 +95,7 @@ const Main = () => { ) const fpsCounter = useDevToggle("DTFPSCounter") + const useNewNavigation = useFeatureFlag("AREnableNewNavigation") useStripeConfig() useSiftConfig() @@ -127,6 +130,9 @@ const Main = () => { return } + if (useNewNavigation) { + return + } if (!isLoggedIn || onboardingState === "incomplete") { return } @@ -139,17 +145,19 @@ const Main = () => { ) } -const InnerApp = () => ( - - +const InnerApp = () => { + return ( + + - -
- + +
+ - - -) + + + ) +} const SentryApp = !__DEV__ ? Sentry.wrap(InnerApp) : InnerApp export const App = codePush(codePushOptions)(SentryApp) diff --git a/src/app/AppRegistry.tsx b/src/app/AppRegistry.tsx index 203afc1d453..3dc11691d69 100644 --- a/src/app/AppRegistry.tsx +++ b/src/app/AppRegistry.tsx @@ -1,3 +1,4 @@ +import { Flex } from "@artsy/palette-mobile" import { NativeStackNavigationOptions } from "@react-navigation/native-stack" import { BidFlow } from "app/Components/Containers/BidFlow" import { InboxQueryRenderer, InboxScreenQuery } from "app/Components/Containers/Inbox" @@ -39,6 +40,8 @@ import { SearchScreen, SearchScreenQuery } from "app/Scenes/Search/Search" import { SubmitArtworkForm } from "app/Scenes/SellWithArtsy/ArtworkForm/SubmitArtworkForm" import { SubmitArtworkFormEditContainer } from "app/Scenes/SellWithArtsy/ArtworkForm/SubmitArtworkFormEdit" import { SimilarToRecentlyViewedScreen } from "app/Scenes/SimilarToRecentlyViewed/SimilarToRecentlyViewed" +import { BackButton } from "app/system/navigation/BackButton" +import { goBack } from "app/system/navigation/navigate" import { ArtsyKeyboardAvoidingViewContext } from "app/utils/ArtsyKeyboardAvoidingView" import { SafeAreaInsets, useScreenDimensions } from "app/utils/hooks" import { useSelectedTab } from "app/utils/hooks/useSelectedTab" @@ -143,7 +146,7 @@ import { ViewingRoomsListScreen, viewingRoomsListScreenQuery, } from "./Scenes/ViewingRoom/ViewingRoomsList" -import { GlobalStore } from "./store/GlobalStore" +import { GlobalStore, unsafe_getFeatureFlag } from "./store/GlobalStore" import { propsStore } from "./store/PropsStore" import { DevMenu } from "./system/devTools/DevMenu/DevMenu" import { Schema, screenTrack } from "./utils/track" @@ -298,7 +301,7 @@ export interface ViewOptions { screenOptions?: NativeStackNavigationOptions } -type ModuleDescriptor = { +export type ModuleDescriptor = { type: "react" Component: React.ComponentType Queries?: GraphQLTaggedNode[] @@ -339,7 +342,11 @@ export const modules = defineModules({ hidesBackButton: true, hidesBottomTabs: true, }), - About: reactModule(About), + About: reactModule(About, { + screenOptions: { + headerTitle: "About", + }, + }), AddMyCollectionArtist: reactModule(AddMyCollectionArtist, { hidesBackButton: true, }), @@ -392,18 +399,24 @@ export const modules = defineModules({ ), ArtworkMedium: reactModule(ArtworkMediumQueryRenderer, { fullBleed: true, - alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", + alwaysPresentModally: !unsafe_getFeatureFlag("AREnableNewNavigation"), + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, }), ArtworkAttributionClassFAQ: reactModule(ArtworkAttributionClassFAQQueryRenderer, { fullBleed: true, - alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", + alwaysPresentModally: !unsafe_getFeatureFlag("AREnableNewNavigation"), + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, }), ArtworkCertificateAuthenticity: reactModule(CertificateOfAuthenticity, { fullBleed: true, - alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", + alwaysPresentModally: !unsafe_getFeatureFlag("AREnableNewNavigation"), + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, }), ArtworkList: reactModule(ArtworkListScreen, { hidesBackButton: true }), ArtworkRecommendations: reactModule(ArtworkRecommendationsScreen), @@ -417,7 +430,9 @@ export const modules = defineModules({ [SalesScreenQuery] ), AuctionInfo: reactModule(SaleInfoQueryRenderer), - AuctionResult: reactModule(AuctionResultQueryRenderer, { hidesBackButton: true }), + AuctionResult: reactModule(AuctionResultQueryRenderer, { + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + }), AuctionResultsForArtistsYouFollow: reactModule( AuctionResultsForArtistsYouFollowQueryRenderer, {}, @@ -426,8 +441,8 @@ export const modules = defineModules({ AuctionResultsForArtistsYouCollect: reactModule(AuctionResultsForArtistsYouCollect), AuctionRegistration: reactModule(RegistrationFlow, { alwaysPresentModally: true, + fullBleed: Platform.OS === "ios" && !unsafe_getFeatureFlag("AREnableNewNavigation"), hasOwnModalCloseButton: true, - fullBleed: Platform.OS === "ios", screenOptions: { // Don't allow the screen to be swiped away by mistake gestureEnabled: false, @@ -436,12 +451,14 @@ export const modules = defineModules({ AuctionBidArtwork: reactModule(BidFlow, { alwaysPresentModally: true, hasOwnModalCloseButton: true, - fullBleed: true, + fullBleed: !unsafe_getFeatureFlag("AREnableNewNavigation"), }), AuctionBuyersPremium: reactModule(AuctionBuyersPremiumQueryRenderer, { fullBleed: true, - alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", + alwaysPresentModally: !unsafe_getFeatureFlag("AREnableNewNavigation"), + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, }), BottomTabs: reactModule(BottomTabs, { fullBleed: true }), BrowseSimilarWorks: reactModule(BrowseSimilarWorksQueryRenderer, { @@ -449,9 +466,10 @@ export const modules = defineModules({ hidesBottomTabs: true, }), CareerHighlightsBigCardsSwiper: reactModule(CareerHighlightsBigCardsSwiper, { - alwaysPresentModally: true, + alwaysPresentModally: !unsafe_getFeatureFlag("AREnableNewNavigation"), + fullBleed: !unsafe_getFeatureFlag("AREnableNewNavigation"), hidesBackButton: true, - fullBleed: true, + hidesBottomTabs: unsafe_getFeatureFlag("AREnableNewNavigation"), }), City: reactModule(CityView, { fullBleed: true, ignoreTabs: true }), CityFairList: reactModule(CityFairListQueryRenderer, { fullBleed: true }), @@ -464,15 +482,35 @@ export const modules = defineModules({ hidesBackButton: true, }), ConsignmentInquiry: reactModule(ConsignmentInquiryScreen, { - hidesBottomTabs: true, + hidesBottomTabs: !unsafe_getFeatureFlag("AREnableNewNavigation"), screenOptions: { gestureEnabled: false, }, }), - Conversation: reactModule(Conversation, { onlyShowInTabName: "inbox" }), - ConversationDetails: reactModule(ConversationDetailsQueryRenderer), + Conversation: reactModule(Conversation, { + onlyShowInTabName: "inbox", + hidesBackButton: true, + }), + ConversationDetails: reactModule(ConversationDetailsQueryRenderer, { + screenOptions: { + headerTitle: "Details", + }, + }), DarkModeSettings: reactModule(DarkModeSettings), - DevMenu: reactModule(DevMenu, { hidesBottomTabs: true, hidesBackButton: true }), + DevMenu: reactModule(DevMenu, { + // No need to hide bottom tabs if it's a modal because they will be hidden by default + hidesBottomTabs: !unsafe_getFeatureFlag("AREnableNewNavigation"), + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + alwaysPresentModally: !!unsafe_getFeatureFlag("AREnableNewNavigation"), + fullBleed: !!unsafe_getFeatureFlag("AREnableNewNavigation"), + screenOptions: { + headerTitle: "Dev Settings", + headerLargeTitle: true, + headerLeft: () => { + return + }, + }, + }), EditSavedSearchAlert: reactModule(EditSavedSearchAlertQueryRenderer, { hidesBackButton: true, hidesBottomTabs: true, @@ -500,6 +538,7 @@ export const modules = defineModules({ HomeContainer, { isRootViewForTabName: "home", + hidesBackButton: true, fullBleed: true, }, [homeViewScreenQuery] @@ -509,26 +548,76 @@ export const modules = defineModules({ hidesBackButton: true, fullBleed: true, }), - Inbox: reactModule(InboxQueryRenderer, { isRootViewForTabName: "inbox" }, [InboxScreenQuery]), + Inbox: reactModule( + InboxQueryRenderer, + { + isRootViewForTabName: "inbox", + hidesBackButton: true, + fullBleed: true, + }, + [InboxScreenQuery] + ), Inquiry: reactModule(Inquiry, { alwaysPresentModally: true, hasOwnModalCloseButton: true }), LiveAuction: reactModule(LiveAuctionView, { alwaysPresentModally: true, hasOwnModalCloseButton: true, modalPresentationStyle: "fullScreen", }), - LocalDiscovery: reactModule(CityGuideView, { fullBleed: true }), + LocalDiscovery: reactModule(CityGuideView, { + fullBleed: true, + screenOptions: unsafe_getFeatureFlag("AREnableNewNavigation") + ? { + headerTransparent: true, + headerLeft: () => { + return ( + { + goBack() + }} + /> + ) + }, + } + : undefined, + }), MakeOfferModal: reactModule(MakeOfferModalQueryRenderer, { hasOwnModalCloseButton: true, }), MedianSalePriceAtAuction: reactModule(MedianSalePriceAtAuction), Map: reactModule(MapContainer, { fullBleed: true, ignoreTabs: true }), - MyAccount: reactModule(MyAccountQueryRenderer), - MyAccountEditEmail: reactModule(MyAccountEditEmailQueryRenderer, { hidesBackButton: true }), + MyAccount: reactModule(MyAccountQueryRenderer, { + screenOptions: { + headerTitle: "Account Settings", + }, + }), + MyAccountEditEmail: reactModule(MyAccountEditEmailQueryRenderer, { + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + screenOptions: { + headerTitle: "Email", + }, + }), MyAccountEditPriceRange: reactModule(MyAccountEditPriceRangeQueryRenderer, { - hidesBackButton: true, + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + screenOptions: { + headerTitle: "Price Range", + }, + }), + MyAccountEditPassword: reactModule(MyAccountEditPassword, { + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + screenOptions: { + headerTitle: "Password", + }, + }), + MyAccountEditPhone: reactModule(MyAccountEditPhoneQueryRenderer, { + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + screenOptions: { + headerTitle: "Phone Number", + }, }), - MyAccountEditPassword: reactModule(MyAccountEditPassword, { hidesBackButton: true }), - MyAccountEditPhone: reactModule(MyAccountEditPhoneQueryRenderer, { hidesBackButton: true }), MyAccountDeleteAccount: reactModule(MyAccountDeleteAccountQueryRenderer), MyBids: reactModule(MyBidsQueryRenderer), MyCollection: reactModule(MyCollectionQueryRenderer), @@ -539,7 +628,7 @@ export const modules = defineModules({ ), MyCollectionArtworkAdd: reactModule(MyCollectionArtworkAdd, { hidesBackButton: true, - hidesBottomTabs: true, + hidesBottomTabs: !unsafe_getFeatureFlag("AREnableNewNavigation"), alwaysPresentModally: true, modalPresentationStyle: "fullScreen", screenOptions: { @@ -548,7 +637,7 @@ export const modules = defineModules({ }), MyCollectionArtworkEdit: reactModule(MyCollectionArtworkEditQueryRenderer, { hidesBackButton: true, - hidesBottomTabs: true, + hidesBottomTabs: !unsafe_getFeatureFlag("AREnableNewNavigation"), alwaysPresentModally: true, modalPresentationStyle: "fullScreen", screenOptions: { @@ -556,10 +645,12 @@ export const modules = defineModules({ }, }), MyCollectionAddCollectedArtists: reactModule(MyCollectionAddCollectedArtistsScreen, { + hidesBackButton: !unsafe_getFeatureFlag("AREnableNewNavigation"), + hidesBottomTabs: true, screenOptions: { + headerTitle: "Add Artists You Collect", gestureEnabled: false, }, - hidesBottomTabs: true, }), MyCollectionSellingWithartsyFAQ: reactModule(MyCollectionSellingWithArtsyFAQ), MyCollectionCollectedArtistsPrivacy: reactModule( @@ -578,6 +669,7 @@ export const modules = defineModules({ { isRootViewForTabName: "profile", fullBleed: true, + hidesBackButton: true, }, [MyCollectionScreenQuery] ), @@ -586,9 +678,21 @@ export const modules = defineModules({ hidesBackButton: true, hidesBottomTabs: true, }), - MyProfileEditForm: reactModule(MyProfileEditFormScreen), - MyProfilePayment: reactModule(MyProfilePaymentQueryRenderer), - MyProfileSettings: reactModule(MyProfileSettings), + MyProfileEditForm: reactModule(MyProfileEditFormScreen, { + screenOptions: { + headerTitle: "Edit Profile", + }, + }), + MyProfilePayment: reactModule(MyProfilePaymentQueryRenderer, { + screenOptions: { + headerTitle: "Payment", + }, + }), + MyProfileSettings: reactModule(MyProfileSettings, { + screenOptions: { + headerTitle: "Account", + }, + }), MySellingProfile: reactModule(View), NewWorksForYou: reactModule(NewWorksForYouQueryRenderer, { hidesBottomTabs: true, @@ -596,9 +700,15 @@ export const modules = defineModules({ fullBleed: true, }), MyProfilePaymentNewCreditCard: reactModule(MyProfilePaymentNewCreditCard, { - hidesBackButton: true, + screenOptions: { + headerTitle: "Add new card", + }, + }), + MyProfilePushNotifications: reactModule(MyProfilePushNotificationsQueryRenderer, { + screenOptions: { + headerTitle: "Push Notifications", + }, }), - MyProfilePushNotifications: reactModule(MyProfilePushNotificationsQueryRenderer), NewWorksFromGalleriesYouFollow: reactModule(NewWorksFromGalleriesYouFollowScreen, { hidesBackButton: true, fullBleed: true, @@ -611,8 +721,16 @@ export const modules = defineModules({ }, [NewsScreenQuery] ), - OrderHistory: reactModule(OrderHistoryQueryRender), - OrderDetails: reactModule(OrderDetailsQueryRender), + OrderHistory: reactModule(OrderHistoryQueryRender, { + screenOptions: { + headerTitle: "Order History", + }, + }), + OrderDetails: reactModule(OrderDetailsQueryRender, { + screenOptions: { + headerTitle: "Order Details", + }, + }), Partner: reactModule(PartnerQueryRenderer, { fullBleed: true, hidesBackButton: true, @@ -623,22 +741,27 @@ export const modules = defineModules({ hidesBackButton: true, }), PriceDatabase: reactModule(PriceDatabase, { hidesBackButton: true }), - PrivacyRequest: reactModule(PrivacyRequest), + PrivacyRequest: reactModule(PrivacyRequest, { + screenOptions: { + headerTitle: "Personal Data Request", + }, + }), PurchaseModal: reactModule(PurchaseModalQueryRenderer, { hasOwnModalCloseButton: true, }), ModalWebView: reactModule(ArtsyWebViewPage, { - fullBleed: false, hasOwnModalCloseButton: true, hidesBackButton: true, alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, screenOptions: { gestureEnabled: false, }, }), ReactWebView: reactModule(ArtsyWebViewPage, { - fullBleed: true, + fullBleed: !unsafe_getFeatureFlag("AREnableNewNavigation"), hasOwnModalCloseButton: true, hidesBackButton: true, }), @@ -660,9 +783,11 @@ export const modules = defineModules({ hidesBackButton: true, fullBleed: true, }), - Sell: reactModule(SellWithArtsy, { isRootViewForTabName: "sell", fullBleed: true }, [ - SellWithArtsyHomeScreenQuery, - ]), + Sell: reactModule( + SellWithArtsy, + { isRootViewForTabName: "sell", fullBleed: true, hidesBackButton: true }, + [SellWithArtsyHomeScreenQuery] + ), SellNotRootTabView: reactModule(SellWithArtsy), SavedArtworks: reactModule(SavedArtworks, { fullBleed: true, @@ -672,7 +797,11 @@ export const modules = defineModules({ fullBleed: true, hidesBackButton: true, }), - Search: reactModule(SearchScreen, { isRootViewForTabName: "search" }, [SearchScreenQuery]), + Search: reactModule( + SearchScreen, + { isRootViewForTabName: "search", hidesBackButton: true, fullBleed: true }, + [SearchScreenQuery] + ), Show: reactModule(ShowQueryRenderer, { fullBleed: true }), ShowMoreInfo: reactModule(ShowMoreInfoQueryRenderer), SimilarToRecentlyViewed: reactModule(SimilarToRecentlyViewedScreen, { @@ -682,8 +811,9 @@ export const modules = defineModules({ SubmitArtwork: reactModule(SubmitArtworkForm, { hidesBackButton: true, alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", - hidesBottomTabs: true, + modalPresentationStyle: !unsafe_getFeatureFlag("AREnableNewNavigation") + ? "fullScreen" + : undefined, screenOptions: { gestureEnabled: false, }, @@ -691,7 +821,6 @@ export const modules = defineModules({ SubmitArtworkEdit: reactModule(SubmitArtworkFormEditContainer, { hidesBackButton: true, alwaysPresentModally: true, - modalPresentationStyle: "fullScreen", hidesBottomTabs: true, screenOptions: { gestureEnabled: false, @@ -710,7 +839,7 @@ export const modules = defineModules({ // Register react modules with the app registry for (const moduleName of Object.keys(modules)) { const descriptor = modules[moduleName as AppModule] - if (Platform.OS === "ios") { + if (Platform.OS === "ios" && !unsafe_getFeatureFlag("AREnableNewNavigation")) { // TODO: this should not be needed. right? register(moduleName, descriptor.Component, { fullBleed: descriptor.options.fullBleed, diff --git a/src/app/Components/ArtsyWebView.tsx b/src/app/Components/ArtsyWebView.tsx index 9700f48a536..ecc1e15325a 100644 --- a/src/app/Components/ArtsyWebView.tsx +++ b/src/app/Components/ArtsyWebView.tsx @@ -1,7 +1,7 @@ import { OwnerType } from "@artsy/cohesion" import { Flex, Text } from "@artsy/palette-mobile" -import { addBreadcrumb } from "@sentry/react-native" import * as Sentry from "@sentry/react-native" +import { addBreadcrumb } from "@sentry/react-native" import { BottomTabRoutes } from "app/Scenes/BottomTabs/bottomTabsConfig" import { matchRoute } from "app/routes" import { GlobalStore, getCurrentEmissionState } from "app/store/GlobalStore" @@ -10,6 +10,7 @@ import { ArtsyKeyboardAvoidingView } from "app/utils/ArtsyKeyboardAvoidingView" import { useBackHandler } from "app/utils/hooks/useBackHandler" import { useDevToggle } from "app/utils/hooks/useDevToggle" import { useEnvironment } from "app/utils/hooks/useEnvironment" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { Schema } from "app/utils/track" import { useWebViewCallback } from "app/utils/useWebViewEvent" import { debounce } from "lodash" @@ -69,6 +70,7 @@ export const ArtsyWebViewPage = ({ backAction?: () => void } & ArtsyWebViewConfig) => { const saInsets = useSafeAreaInsets() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const [canGoBack, setCanGoBack] = useState(false) const webURL = useEnvironment().webURL @@ -131,7 +133,11 @@ export const ArtsyWebViewPage = ({ const leftButton = useRightCloseButton && !canGoBack ? undefined : handleBackButtonPress return ( - + { @@ -180,6 +186,8 @@ export const ArtsyWebView = forwardRef< }, ref ) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const innerRef = useRef(null) useImperativeHandle(ref, () => innerRef.current as WebViewWithShareTitleUrl) const userAgent = getCurrentEmissionState().userAgent @@ -259,7 +267,7 @@ export const ArtsyWebView = forwardRef< } } - const WebViewWrapper = isPresentedModally ? SafeAreaView : View + const WebViewWrapper = isPresentedModally && !enableNewNavigation ? SafeAreaView : View return ( diff --git a/src/app/Components/Containers/Inbox.tsx b/src/app/Components/Containers/Inbox.tsx index a3fb8072741..70c9b37a14b 100644 --- a/src/app/Components/Containers/Inbox.tsx +++ b/src/app/Components/Containers/Inbox.tsx @@ -1,6 +1,15 @@ import { ActionType } from "@artsy/cohesion" -import { Spacer, Flex, Separator, Tabs, Skeleton, SkeletonText } from "@artsy/palette-mobile" +import { + Spacer, + Flex, + Separator, + Tabs, + Skeleton, + SkeletonText, + Screen, +} from "@artsy/palette-mobile" import { TabsContainer } from "@artsy/palette-mobile/dist/elements/Tabs/TabsContainer" +import { StackScreenProps } from "@react-navigation/stack" import { InboxQuery } from "__generated__/InboxQuery.graphql" import { Inbox_me$data } from "__generated__/Inbox_me.graphql" import { ConversationsContainer } from "app/Scenes/Inbox/Components/Conversations/Conversations" @@ -137,20 +146,26 @@ export const InboxScreenQuery = graphql` } ` -export const InboxQueryRenderer: React.FC<{ isVisible: boolean }> = (props) => { +interface InboxQueryRendererProps extends StackScreenProps { + isVisible?: boolean +} + +export const InboxQueryRenderer: React.FC = (props) => { return ( - - environment={getRelayEnvironment()} - query={InboxScreenQuery} - variables={{}} - render={(...args) => - renderWithPlaceholder({ - Container: InboxContainer, - initialProps: props, - renderPlaceholder: () => , - })(...args) - } - /> + + + environment={getRelayEnvironment()} + query={InboxScreenQuery} + variables={{}} + render={(...args) => + renderWithPlaceholder({ + Container: InboxContainer, + initialProps: props, + renderPlaceholder: () => , + })(...args) + } + /> + ) } diff --git a/src/app/Components/FancyModal/FancyModalContext.tsx b/src/app/Components/FancyModal/FancyModalContext.tsx index d8ff2e94826..5009be41c02 100644 --- a/src/app/Components/FancyModal/FancyModalContext.tsx +++ b/src/app/Components/FancyModal/FancyModalContext.tsx @@ -1,8 +1,8 @@ import { ExecutionQueue } from "app/utils/ExecutionQueue" +import { useScreenDimensions } from "app/utils/hooks" import { compact, flatten } from "lodash" import React, { RefObject, useEffect, useRef, useState } from "react" import { Animated, View } from "react-native" -import { useScreenDimensions } from "app/utils/hooks" import { AnimationCreator, ease, diff --git a/src/app/Components/PageWithSimpleHeader.tsx b/src/app/Components/PageWithSimpleHeader.tsx index ff38f89584f..6433b75c196 100644 --- a/src/app/Components/PageWithSimpleHeader.tsx +++ b/src/app/Components/PageWithSimpleHeader.tsx @@ -1,4 +1,4 @@ -import { Flex, Box, Text, Separator, TextProps } from "@artsy/palette-mobile" +import { Flex, Box, Text, Separator, TextProps, NAVBAR_HEIGHT } from "@artsy/palette-mobile" import { View } from "react-native" export const PageWithSimpleHeader: React.FC<{ @@ -10,17 +10,29 @@ export const PageWithSimpleHeader: React.FC<{ }> = ({ title, titleWeight, left, right, children, noSeparator }) => { return ( - - + + {left} - {/* TODO: figure out how to make this stretch dynamically */} - - + + {title} - + {right} diff --git a/src/app/NativeModules/ArtsyNativeModule.tsx b/src/app/NativeModules/ArtsyNativeModule.tsx index e6814f1c085..1fc42f90df0 100644 --- a/src/app/NativeModules/ArtsyNativeModule.tsx +++ b/src/app/NativeModules/ArtsyNativeModule.tsx @@ -13,7 +13,7 @@ export const DEFAULT_NAVIGATION_BAR_COLOR = "#FFFFFF" export const ArtsyNativeModule = { launchCount: Platform.OS === "ios" - ? LegacyNativeModules.ARNotificationsManager.nativeState.launchCount + ? LegacyNativeModules.ARNotificationsManager.nativeState.launchCount || 0 : (NativeModules.ArtsyNativeModule.getConstants().launchCount as number), setAppStyling: Platform.OS === "ios" diff --git a/src/app/Navigation/AuthenticatedRoutes/HomeTab.tsx b/src/app/Navigation/AuthenticatedRoutes/HomeTab.tsx new file mode 100644 index 00000000000..18f7fc9eb62 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/HomeTab.tsx @@ -0,0 +1,15 @@ +import { modules } from "app/AppRegistry" +import { registerSharedRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" + +export const HomeTab: React.FC = () => { + return ( + + {registerScreen({ + name: "Home", + module: modules["Home"], + })} + {registerSharedRoutes()} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/InboxTab.tsx b/src/app/Navigation/AuthenticatedRoutes/InboxTab.tsx new file mode 100644 index 00000000000..67dcc1b1b79 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/InboxTab.tsx @@ -0,0 +1,20 @@ +import { modules } from "app/AppRegistry" +import { registerSharedRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" + +export const InboxTab: React.FC = () => { + return ( + + {registerScreen({ + name: "Inbox", + module: modules["Inbox"], + })} + {registerScreen({ + name: "Conversation", + module: modules["Conversation"], + })} + + {registerSharedRoutes()} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/NativeScreens.tsx b/src/app/Navigation/AuthenticatedRoutes/NativeScreens.tsx new file mode 100644 index 00000000000..5ed760cdf06 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/NativeScreens.tsx @@ -0,0 +1,45 @@ +import { CityGuideView } from "app/NativeModules/CityGuideView" +import { LiveAuctionView } from "app/NativeModules/LiveAuctionView" +import { Providers } from "app/Providers" +import { CityView } from "app/Scenes/City/City" +import { CityPicker } from "app/Scenes/City/CityPicker" +import { MapContainer } from "app/Scenes/Map/MapContainer" +import { AppRegistry, Platform } from "react-native" + +function register(moduleName: string, Component: React.ComponentType) { + const WrappedComponent = (props: any) => ( + + + + ) + AppRegistry.registerComponent(moduleName, () => WrappedComponent) +} + +const modules: { moduleName: string; module: React.FC }[] = [ + { + moduleName: "LocalDiscovery", + module: CityGuideView, + }, + { + moduleName: "LiveAuction", + module: LiveAuctionView, + }, + { + moduleName: "CityPicker", + module: CityPicker, + }, + { + moduleName: "Map", + module: MapContainer, + }, + { + moduleName: "City", + module: CityView as any, + }, +] + +if (Platform.OS === "ios") { + modules.map(({ moduleName, module }) => { + register(moduleName, module) + }) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/ProfileTab.tsx b/src/app/Navigation/AuthenticatedRoutes/ProfileTab.tsx new file mode 100644 index 00000000000..afc9ea2bb6d --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/ProfileTab.tsx @@ -0,0 +1,16 @@ +import { modules } from "app/AppRegistry" +import { registerSharedRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" + +export const ProfileTab: React.FC = () => { + return ( + + {registerScreen({ + name: "MyProfile", + module: modules["MyProfile"], + })} + + {registerSharedRoutes()} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/SearchTab.tsx b/src/app/Navigation/AuthenticatedRoutes/SearchTab.tsx new file mode 100644 index 00000000000..932ea5d8dd0 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/SearchTab.tsx @@ -0,0 +1,16 @@ +import { modules } from "app/AppRegistry" +import { registerSharedRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" + +export const SearchTab: React.FC = () => { + return ( + + {registerScreen({ + name: "Search", + module: modules["Search"], + })} + + {registerSharedRoutes()} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/SellTab.tsx b/src/app/Navigation/AuthenticatedRoutes/SellTab.tsx new file mode 100644 index 00000000000..99149b7a902 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/SellTab.tsx @@ -0,0 +1,16 @@ +import { modules } from "app/AppRegistry" +import { registerSharedRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" + +export const SellTab: React.FC = () => { + return ( + + {registerScreen({ + name: "Sell", + module: modules["Sell"], + })} + + {registerSharedRoutes()} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/SharedRoutes.tsx b/src/app/Navigation/AuthenticatedRoutes/SharedRoutes.tsx new file mode 100644 index 00000000000..ac44faf7545 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/SharedRoutes.tsx @@ -0,0 +1,46 @@ +import { AppModule, modules as allModules } from "app/AppRegistry" +import { registerScreen, StackNavigator } from "app/Navigation/AuthenticatedRoutes/StackNavigator" +import { AuthenticatedRoutesStack } from "app/Navigation/AuthenticatedRoutes/Tabs" +import { isModalScreen } from "app/Navigation/Utils/isModalScreen" + +const modules = Object.fromEntries( + Object.entries(allModules).filter(([_, module]) => { + return ( + module.type === "react" && + module.Component && + // The module should not be a root view for a tab + !module.options.isRootViewForTabName && + // The module is not an restricted to a specific tab + !module.options.onlyShowInTabName + ) + }) +) + +const modalModules = Object.entries(modules).filter(([_, module]) => isModalScreen(module)) +const nonModalModules = Object.entries(modules).filter(([_, module]) => !isModalScreen(module)) + +export const registerSharedRoutes = () => { + return ( + + {nonModalModules.map(([moduleName, module]) => { + return registerScreen({ + name: moduleName as AppModule, + module: module, + }) + })} + + ) +} + +export const registerSharedModalRoutes = () => { + return ( + + {modalModules.map(([moduleName, module]) => { + return registerScreen({ + name: moduleName as AppModule, + module: module, + }) + })} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/StackNavigator.tsx b/src/app/Navigation/AuthenticatedRoutes/StackNavigator.tsx new file mode 100644 index 00000000000..3a7b968e688 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/StackNavigator.tsx @@ -0,0 +1,121 @@ +import { + ArrowLeftIcon, + CloseIcon, + DEFAULT_HIT_SLOP, + Flex, + THEMES, + Touchable, +} from "@artsy/palette-mobile" +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { ModuleDescriptor } from "app/AppRegistry" +import { AuthenticatedRoutesParams } from "app/Navigation/AuthenticatedRoutes/Tabs" +import { isHeaderShown } from "app/Navigation/Utils/isHeaderShown" +import { isModalScreen } from "app/Navigation/Utils/isModalScreen" +import { ICON_HEIGHT } from "app/Scenes/BottomTabs/BottomTabsIcon" +import { goBack } from "app/system/navigation/navigate" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +export const StackNavigator = createNativeStackNavigator() + +type StackNavigatorScreenProps = { + name: keyof AuthenticatedRoutesParams + module: ModuleDescriptor +} & Omit, "component" | "getComponent"> + +export const registerScreen: React.FC = ({ name, module, ...props }) => { + return ( + { + if (!canGoBack) { + return null + } + + return ( + { + goBack() + }} + underlayColor="transparent" + hitSlop={DEFAULT_HIT_SLOP} + > + {isModalScreen(module) ? ( + + ) : ( + + )} + + ) + }, + headerTitle: "", + headerTitleAlign: "center", + ...module.options.screenOptions, + headerTitleStyle: { + fontFamily: THEMES.v3.fonts.sans.regular, + ...THEMES.v3.textTreatments["sm-display"], + ...((module.options.screenOptions?.headerTitleStyle as {} | undefined) ?? {}), + }, + }} + children={(screenProps) => { + const params = screenProps.route.params || {} + const isFullBleed = + module.options.fullBleed ?? + // when no header is visible, we want to make sure we are bound by the insets + isHeaderShown(module) + + const hidesBottomTabs = module.options.hidesBottomTabs || isModalScreen(module) + + return ( + + + + ) + }} + /> + ) +} + +export interface ScreenWrapperProps { + fullBleed?: boolean + readonly hidesBottomTabs?: boolean +} + +export const ScreenWrapper: React.FC = ({ + fullBleed = false, + hidesBottomTabs = false, + children, +}) => { + const safeAreaInsets = useSafeAreaInsets() + // We don't have the bottom tabs context on modal screens + // eslint-disable-next-line react-hooks/rules-of-hooks + const tabBarHeight = hidesBottomTabs ? 0 : useBottomTabBarHeight() + + const padding = fullBleed + ? { + paddingBottom: hidesBottomTabs ? 0 : tabBarHeight, + } + : { + // Bottom inset + bottom tabs height - bottom tabs border + paddingBottom: hidesBottomTabs ? 0 : safeAreaInsets.bottom + ICON_HEIGHT - 2, + paddingTop: safeAreaInsets.top, + paddingRight: safeAreaInsets.right, + paddingLeft: safeAreaInsets.left, + } + + return ( + + {children} + + ) +} diff --git a/src/app/Navigation/AuthenticatedRoutes/Tabs.tsx b/src/app/Navigation/AuthenticatedRoutes/Tabs.tsx new file mode 100644 index 00000000000..6060246c3e9 --- /dev/null +++ b/src/app/Navigation/AuthenticatedRoutes/Tabs.tsx @@ -0,0 +1,80 @@ +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs" +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { AppModule, modules } from "app/AppRegistry" +import { HomeTab } from "app/Navigation/AuthenticatedRoutes/HomeTab" +import { InboxTab } from "app/Navigation/AuthenticatedRoutes/InboxTab" +import { ProfileTab } from "app/Navigation/AuthenticatedRoutes/ProfileTab" +import { SearchTab } from "app/Navigation/AuthenticatedRoutes/SearchTab" +import { SellTab } from "app/Navigation/AuthenticatedRoutes/SellTab" +import { registerSharedModalRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes" +import { BottomTabsButton } from "app/Scenes/BottomTabs/BottomTabsButton" +import { internal_navigationRef } from "app/system/navigation/navigate" +import { Platform } from "react-native" + +if (Platform.OS === "ios") { + require("app/Navigation/AuthenticatedRoutes/NativeScreens") +} + +export type AuthenticatedRoutesParams = { + Home: undefined + Search: undefined + Profile: undefined + Inbox: undefined + Sell: undefined +} & { [key in AppModule]: undefined } + +type TabRoutesParams = { + home: undefined + search: undefined + inbox: undefined + sell: undefined + profile: undefined +} + +const Tab = createBottomTabNavigator() + +const AppTabs: React.FC = () => { + return ( + { + const currentRoute = internal_navigationRef.current?.getCurrentRoute()?.name + return { + headerShown: false, + tabBarStyle: { + animate: true, + position: "absolute", + display: + currentRoute && modules[currentRoute as AppModule]?.options.hidesBottomTabs + ? "none" + : "flex", + }, + tabBarHideOnKeyboard: true, + tabBarButton: (props) => { + return + }, + } + }} + > + + + + + + + ) +} + +export const AuthenticatedRoutesStack = createNativeStackNavigator() + +export const AuthenticatedRoutes: React.FC = () => { + return ( + + + {registerSharedModalRoutes()} + + ) +} diff --git a/src/app/Navigation/Navigation.tsx b/src/app/Navigation/Navigation.tsx new file mode 100644 index 00000000000..8e3ac31d1ea --- /dev/null +++ b/src/app/Navigation/Navigation.tsx @@ -0,0 +1,118 @@ +import { Flex, Text } from "@artsy/palette-mobile" +import { DefaultTheme, NavigationContainer } from "@react-navigation/native" +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { addBreadcrumb } from "@sentry/react-native" +import { LoadingSpinner } from "app/Components/Modals/LoadingModal" +import { + AuthenticatedRoutes, + AuthenticatedRoutesParams, +} from "app/Navigation/AuthenticatedRoutes/Tabs" +import { + UnauthenticatedRoutes, + UnauthenticatedRoutesParams, +} from "app/Navigation/UnauthenticatedRoutes" +import { GlobalStore } from "app/store/GlobalStore" +import { routingInstrumentation } from "app/system/errorReporting/setupSentry" +import { internal_navigationRef } from "app/system/navigation/navigate" + +import { useReloadedDevNavigationState } from "app/system/navigation/useReloadedDevNavigationState" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { logNavigation } from "app/utils/loggers" +import { Platform } from "react-native" +import SiftReactNative from "sift-react-native" + +export type NavigationRoutesParams = UnauthenticatedRoutesParams & AuthenticatedRoutesParams + +export const MainStackNavigator = createNativeStackNavigator() + +const MODAL_NAVIGATION_STACK_STATE_KEY = "MODAL_NAVIGATION_STACK_STATE_KEY" + +export const Navigation = () => { + const { isReady, initialState, saveSession } = useReloadedDevNavigationState( + MODAL_NAVIGATION_STACK_STATE_KEY + ) + + const isLoggedIn = GlobalStore.useAppState((state) => state.auth.userID) + const { setSessionState: setNavigationReady } = GlobalStore.actions + + // Code for Sift tracking; needs to be manually fired on Android + // See https://github.com/SiftScience/sift-react-native/pull/23#issuecomment-1630984250 + const enableAdditionalSiftAndroidTracking = useFeatureFlag( + "AREnableAdditionalSiftAndroidTracking" + ) + const trackSiftAndroid = Platform.OS === "android" && enableAdditionalSiftAndroidTracking + + if (!isReady) { + return + } + + return ( + { + routingInstrumentation.registerNavigationContainer(internal_navigationRef) + + setNavigationReady({ isNavigationReady: true }) + + if (trackSiftAndroid) { + const initialRouteName = internal_navigationRef?.current?.getCurrentRoute()?.name + SiftReactNative.setPageName(`screen_${initialRouteName}`) + SiftReactNative.upload() + } + }} + onStateChange={(state) => { + saveSession(state) + + const currentRoute = internal_navigationRef?.current?.getCurrentRoute() + const params = currentRoute?.params + + if (__DEV__ && logNavigation) { + console.log( + `navigated to ${currentRoute?.name} ${ + currentRoute?.params ? JSON.stringify(currentRoute.params) : "" + } ` + ) + } + + addBreadcrumb({ + message: `navigated to ${currentRoute?.name}`, + category: "navigation", + data: { ...params }, + level: "info", + }) + + if (trackSiftAndroid) { + SiftReactNative.setPageName(`screen_${currentRoute?.name}`) + SiftReactNative.upload() + } + }} + > + {!isLoggedIn && } + {!!isLoggedIn && } + + ) +} + +const theme = { + ...DefaultTheme, + colors: { + ...DefaultTheme.colors, + background: "#FFF", + }, +} + +const NavigationLoadingIndicator = () => { + return ( + + {!!__DEV__ && ( + + + This spinner is only visible in DEV mode.{"\n"} + + + )} + + ) +} diff --git a/src/app/Navigation/UnauthenticatedRoutes.tsx b/src/app/Navigation/UnauthenticatedRoutes.tsx new file mode 100644 index 00000000000..bdd5d4d2147 --- /dev/null +++ b/src/app/Navigation/UnauthenticatedRoutes.tsx @@ -0,0 +1,22 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { MainStackNavigator } from "app/Navigation/Navigation" +import { LoginScreen } from "app/Navigation/_TO_BE_DELETED_Screens/LoginScreen" +import { SignUpScreen } from "app/Navigation/_TO_BE_DELETED_Screens/SignUpScreen" + +export type UnauthenticatedRoutesParams = { + Login: undefined + SignUp: undefined +} + +const UnauthenticatedStack = createNativeStackNavigator() + +export const UnauthenticatedRoutes = () => { + return ( + + + + + + + ) +} diff --git a/src/app/Navigation/Utils/LargeHeaderView.tsx b/src/app/Navigation/Utils/LargeHeaderView.tsx new file mode 100644 index 00000000000..4757e10336e --- /dev/null +++ b/src/app/Navigation/Utils/LargeHeaderView.tsx @@ -0,0 +1,12 @@ +import { Flex, NAVBAR_HEIGHT } from "@artsy/palette-mobile" +import { Platform } from "react-native" + +const LARGE_HEADER_HEIGHT = 100 + +export const LargeHeaderView: React.FC = () => { + if (Platform.OS !== "ios") { + return null + } + + return +} diff --git a/src/app/Navigation/Utils/isHeaderShown.tsx b/src/app/Navigation/Utils/isHeaderShown.tsx new file mode 100644 index 00000000000..b8c6c1697b4 --- /dev/null +++ b/src/app/Navigation/Utils/isHeaderShown.tsx @@ -0,0 +1,5 @@ +import { ModuleDescriptor } from "app/AppRegistry" + +export const isHeaderShown = (module: ModuleDescriptor) => { + return !module.options.hidesBackButton && !module.options.hasOwnModalCloseButton +} diff --git a/src/app/Navigation/Utils/isModalScreen.tsx b/src/app/Navigation/Utils/isModalScreen.tsx new file mode 100644 index 00000000000..981249ea341 --- /dev/null +++ b/src/app/Navigation/Utils/isModalScreen.tsx @@ -0,0 +1,5 @@ +import { ModuleDescriptor } from "app/AppRegistry" + +export const isModalScreen = (module: ModuleDescriptor) => { + return !!module.options.alwaysPresentModally +} diff --git a/src/app/Navigation/_TO_BE_DELETED_Screens/LoginScreen.tsx b/src/app/Navigation/_TO_BE_DELETED_Screens/LoginScreen.tsx new file mode 100644 index 00000000000..b162a11e401 --- /dev/null +++ b/src/app/Navigation/_TO_BE_DELETED_Screens/LoginScreen.tsx @@ -0,0 +1,23 @@ +import { Button, Flex, Spacer, Text } from "@artsy/palette-mobile" +import { NavigationProp, useNavigation } from "@react-navigation/native" +import { UnauthenticatedRoutesParams } from "app/Navigation/UnauthenticatedRoutes" + +export const LoginScreen: React.FC = () => { + const navigation = useNavigation>() + + return ( + + Login + + + + + + ) +} diff --git a/src/app/Navigation/_TO_BE_DELETED_Screens/SignUpScreen.tsx b/src/app/Navigation/_TO_BE_DELETED_Screens/SignUpScreen.tsx new file mode 100644 index 00000000000..2862aa6d4fd --- /dev/null +++ b/src/app/Navigation/_TO_BE_DELETED_Screens/SignUpScreen.tsx @@ -0,0 +1,22 @@ +import { Button, Flex, Spacer, Text } from "@artsy/palette-mobile" +import { NavigationProp, useNavigation } from "@react-navigation/native" +import { UnauthenticatedRoutesParams } from "app/Navigation/UnauthenticatedRoutes" + +export const SignUpScreen: React.FC = () => { + const navigation = useNavigation>() + return ( + + Sign Up + + + + + + ) +} diff --git a/src/app/Scenes/About/About.tsx b/src/app/Scenes/About/About.tsx index c32377603e3..af9f4258905 100644 --- a/src/app/Scenes/About/About.tsx +++ b/src/app/Scenes/About/About.tsx @@ -5,12 +5,13 @@ import { useToast } from "app/Components/Toast/toastHook" import { GlobalStore } from "app/store/GlobalStore" import { navigate } from "app/system/navigation/navigate" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" -import React, { useEffect, useState } from "react" +import React, { Fragment, useEffect, useState } from "react" import { ScrollView } from "react-native" import DeviceInfo from "react-native-device-info" import useDebounce from "react-use/lib/useDebounce" export const About: React.FC = () => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const { color } = useTheme() const appVersion = DeviceInfo.getVersion() const toast = useToast() @@ -44,8 +45,14 @@ export const About: React.FC = () => { [tapCount] ) + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - + { } /> - + ) } diff --git a/src/app/Scenes/Article/Components/ArticleWebViewScreen.tsx b/src/app/Scenes/Article/Components/ArticleWebViewScreen.tsx index b774cdd7385..548f5136740 100644 --- a/src/app/Scenes/Article/Components/ArticleWebViewScreen.tsx +++ b/src/app/Scenes/Article/Components/ArticleWebViewScreen.tsx @@ -19,12 +19,7 @@ export const ArticleWebViewScreen: React.FC = ({ arti - {/* - NOTE: we don't need safeAreaEdges but passing undefined or empty array didn't work, - so we're passing "left" that doesn't actually add anything to the webview to avoid - having double paddings from Screen and ArtsyWebView - */} - + ) diff --git a/src/app/Scenes/Artwork/Components/ArtworkStickyBottomContent.tsx b/src/app/Scenes/Artwork/Components/ArtworkStickyBottomContent.tsx index fa566d5fc8e..a6cc18d0956 100644 --- a/src/app/Scenes/Artwork/Components/ArtworkStickyBottomContent.tsx +++ b/src/app/Scenes/Artwork/Components/ArtworkStickyBottomContent.tsx @@ -11,7 +11,7 @@ import { ArtworkStore } from "app/Scenes/Artwork/ArtworkStore" import { useScreenDimensions } from "app/utils/hooks" import { DateTime } from "luxon" import { useEffect } from "react" -import { useFragment, graphql } from "react-relay" +import { graphql, useFragment } from "react-relay" import { ArtworkCommercialButtons } from "./ArtworkCommercialButtons" import { ArtworkPrice } from "./ArtworkPrice" @@ -29,10 +29,10 @@ export const ArtworkStickyBottomContent: React.FC { - const { safeAreaInsets } = useScreenDimensions() const artworkData = useFragment(artworkFragment, artwork) const partnerOfferData = useFragment(partnerOfferFragment, partnerOffer) const auctionState = ArtworkStore.useStoreState((state) => state.auctionState) + const { safeAreaInsets } = useScreenDimensions() const { bottom: bottomSafeAreaInset } = useScreenDimensions().safeAreaInsets diff --git a/src/app/Scenes/Artwork/Components/CommercialButtons/InquirySuccessNotification.tsx b/src/app/Scenes/Artwork/Components/CommercialButtons/InquirySuccessNotification.tsx index 063fba3267b..bf4b82cf176 100644 --- a/src/app/Scenes/Artwork/Components/CommercialButtons/InquirySuccessNotification.tsx +++ b/src/app/Scenes/Artwork/Components/CommercialButtons/InquirySuccessNotification.tsx @@ -1,6 +1,6 @@ import { Flex, Text } from "@artsy/palette-mobile" import { themeGet } from "@styled-system/theme-get" -import { navigate } from "app/system/navigation/navigate" +import { switchTab } from "app/system/navigation/navigate" import { useArtworkInquiryContext } from "app/utils/ArtworkInquiry/ArtworkInquiryStore" import { useScreenDimensions } from "app/utils/hooks" import React, { useEffect } from "react" @@ -28,7 +28,7 @@ export const InquirySuccessNotification: React.FC = () => { const navigateToConversation = () => { dispatch({ type: "setSuccessNotificationVisible", payload: false }) - navigate("inbox") + switchTab("inbox") } const handleRequestClose = () => { diff --git a/src/app/Scenes/ArtworkAttributionClassFAQ/ArtworkAttributionClassFAQ.tsx b/src/app/Scenes/ArtworkAttributionClassFAQ/ArtworkAttributionClassFAQ.tsx index 90495c96ab7..0c822dc7b9b 100644 --- a/src/app/Scenes/ArtworkAttributionClassFAQ/ArtworkAttributionClassFAQ.tsx +++ b/src/app/Scenes/ArtworkAttributionClassFAQ/ArtworkAttributionClassFAQ.tsx @@ -1,12 +1,12 @@ import { - Spacer, Box, - Text, - Separator, - Join, Button, - Screen, CloseIcon, + Join, + Screen, + Separator, + Spacer, + Text, Touchable, useSpace, } from "@artsy/palette-mobile" @@ -15,6 +15,7 @@ import { ArtworkAttributionClassFAQ_artworkAttributionClasses$data } from "__gen import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { useAndroidGoBack } from "app/utils/hooks/useBackHandler" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import renderWithLoadProgress from "app/utils/renderWithLoadProgress" import React from "react" import { ScrollView } from "react-native" @@ -25,23 +26,27 @@ interface Props { } export const ArtworkAttributionClassFAQ: React.FC = ({ artworkAttributionClasses }) => { - useAndroidGoBack() const space = useSpace() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + useAndroidGoBack() return ( - goBack()} - hitSlop={{ top: space(2), left: space(2), bottom: space(2), right: space(2) }} - > - - - } - /> + {!enableNewNavigation && ( + goBack()} + hitSlop={{ top: space(2), left: space(2), bottom: space(2), right: space(2) }} + > + + + } + /> + )} + diff --git a/src/app/Scenes/ArtworkMedium/ArtworkMedium.tsx b/src/app/Scenes/ArtworkMedium/ArtworkMedium.tsx index 9e5cb152301..2595fb4ac34 100644 --- a/src/app/Scenes/ArtworkMedium/ArtworkMedium.tsx +++ b/src/app/Scenes/ArtworkMedium/ArtworkMedium.tsx @@ -1,20 +1,21 @@ import { - Spacer, Box, - Text, - Separator, - Join, Button, + CloseIcon, + Join, Screen, + Separator, + Spacer, + Text, Touchable, useSpace, - CloseIcon, } from "@artsy/palette-mobile" import { ArtworkMediumQuery } from "__generated__/ArtworkMediumQuery.graphql" import { ArtworkMedium_artwork$data } from "__generated__/ArtworkMedium_artwork.graphql" import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { useAndroidGoBack } from "app/utils/hooks/useBackHandler" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import renderWithLoadProgress from "app/utils/renderWithLoadProgress" import { ScrollView } from "react-native" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" @@ -24,23 +25,27 @@ interface Props { } export const ArtworkMedium: React.FC = ({ artwork }) => { - useAndroidGoBack() const space = useSpace() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + + useAndroidGoBack() return ( - goBack()} - hitSlop={{ top: space(2), left: space(2), bottom: space(2), right: space(2) }} - > - - - } - /> + {!enableNewNavigation && ( + goBack()} + hitSlop={{ top: space(2), left: space(2), bottom: space(2), right: space(2) }} + > + + + } + /> + )} diff --git a/src/app/Scenes/AuctionResult/AuctionResult.tests.tsx b/src/app/Scenes/AuctionResult/AuctionResult.tests.tsx index 0776f578da6..659612058e5 100644 --- a/src/app/Scenes/AuctionResult/AuctionResult.tests.tsx +++ b/src/app/Scenes/AuctionResult/AuctionResult.tests.tsx @@ -1,3 +1,4 @@ +import { screen } from "@testing-library/react-native" import { AuctionResult_artist$data } from "__generated__/AuctionResult_artist.graphql" import { AuctionResult_auctionResult$data } from "__generated__/AuctionResult_auctionResult.graphql" import { AuctionResultQueryRenderer } from "app/Scenes/AuctionResult/AuctionResult" @@ -23,7 +24,7 @@ describe("Activity", () => { }) it("renders items", async () => { - const { getAllByText } = renderWithHookWrappersTL( + renderWithHookWrappersTL( { await flushPromiseQueue() - expect(getAllByText("Banksy")).toBeTruthy() - expect(getAllByText("Pulp Fiction")).toBeTruthy() - expect(getAllByText("Bonhams")).toBeTruthy() - expect(getAllByText("Pre-sale Estimate")).toBeTruthy() - expect(getAllByText("£70,000–£100,000")).toBeTruthy() - expect(getAllByText("London, New Bond Street")).toBeTruthy() - expect(getAllByText("Lot number")).toBeTruthy() + expect(screen.getAllByText("Pulp Fiction")).toBeTruthy() + expect(screen.getAllByText("Bonhams")).toBeTruthy() + expect(screen.getAllByText("Pre-sale Estimate")).toBeTruthy() + expect(screen.getAllByText("£70,000–£100,000")).toBeTruthy() + expect(screen.getAllByText("London, New Bond Street")).toBeTruthy() + expect(screen.getAllByText("Lot number")).toBeTruthy() }) }) diff --git a/src/app/Scenes/AuctionResult/AuctionResult.tsx b/src/app/Scenes/AuctionResult/AuctionResult.tsx index 58f136493c2..72b6ab681d1 100644 --- a/src/app/Scenes/AuctionResult/AuctionResult.tsx +++ b/src/app/Scenes/AuctionResult/AuctionResult.tsx @@ -11,6 +11,7 @@ import { Separator, BackButton, } from "@artsy/palette-mobile" +import { NavigationProp, useNavigation } from "@react-navigation/native" import { addBreadcrumb } from "@sentry/react-native" import { AuctionResultQuery } from "__generated__/AuctionResultQuery.graphql" import { AuctionResult_artist$key } from "__generated__/AuctionResult_artist.graphql" @@ -20,16 +21,18 @@ import { } from "__generated__/AuctionResult_auctionResult.graphql" import { ratioColor } from "app/Components/AuctionResult/AuctionResultMidEstimate" import { InfoButton } from "app/Components/Buttons/InfoButton" +import { AuthenticatedRoutesParams } from "app/Navigation/AuthenticatedRoutes/Tabs" import { goBack, navigate } from "app/system/navigation/navigate" import { QAInfoPanel } from "app/utils/QAInfo" import { useScreenDimensions } from "app/utils/hooks" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox } from "app/utils/placeholders" import { getImageSquareDimensions } from "app/utils/resizeImage" import { ProvideScreenTrackingWithCohesionSchema } from "app/utils/track" import { screen } from "app/utils/track/helpers" import { capitalize } from "lodash" import moment from "moment" -import React, { Suspense, useEffect, useState } from "react" +import React, { Suspense, useEffect, useLayoutEffect, useState } from "react" import { Image, ScrollView, TextInput, TouchableWithoutFeedback } from "react-native" import FastImage from "react-native-fast-image" import { graphql, useFragment, useLazyLoadQuery } from "react-relay" @@ -47,12 +50,20 @@ interface Props { export const AuctionResult: React.FC = (props) => { const artist = useFragment(artistFragment, props.artist) const auctionResult = useFragment(auctionResultFragment, props.auctionResult) + const navigation = useNavigation>() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const { theme } = useTheme() const space = useSpace() const tracking = useTracking() + useLayoutEffect(() => { + navigation.setOptions({ + headerTitle: `${auctionResult.title}, ${auctionResult.dateText}`, + }) + }, [navigation]) + const details = [] const makeRow = ( label: string, @@ -224,18 +235,21 @@ export const AuctionResult: React.FC = (props) => { return ( - - - + {!enableNewNavigation && ( + + + + + + {auctionResult.title} + + {!!auctionResult.dateText && , {auctionResult.dateText}} - - {auctionResult.title} - - {!!auctionResult.dateText && , {auctionResult.dateText}} - + )} + }> {!!auctionResult?.images?.larger && ( diff --git a/src/app/Scenes/BottomTabs/BottomTabs.tests.tsx b/src/app/Scenes/BottomTabs/BottomTabs.tests.tsx index ec112bbc052..76f7cec79b1 100644 --- a/src/app/Scenes/BottomTabs/BottomTabs.tests.tsx +++ b/src/app/Scenes/BottomTabs/BottomTabs.tests.tsx @@ -62,7 +62,7 @@ describe(BottomTabs, () => { await flushPromiseQueue() - expect(screen.queryByLabelText("home visual clue")).toBeTruthy() + expect(screen.getByLabelText("home visual clue")).toBeTruthy() }) it("should NOT be displayed if there are NO unseen notifications", async () => { @@ -179,8 +179,6 @@ describe(BottomTabs, () => { // Check badge counters const currentInboxCounter = await findBadgeCounterForTab("inbox") expect(currentInboxCounter).toHaveTextContent("3") - - jest.useRealTimers() }) }) diff --git a/src/app/Scenes/BottomTabs/BottomTabsButton.tests.tsx b/src/app/Scenes/BottomTabs/BottomTabsButton.tests.tsx index 8603b2d7c55..ad61e500ce3 100644 --- a/src/app/Scenes/BottomTabs/BottomTabsButton.tests.tsx +++ b/src/app/Scenes/BottomTabs/BottomTabsButton.tests.tsx @@ -1,3 +1,4 @@ +import { Touchable } from "@artsy/palette-mobile" import { __globalStoreTestUtils__, GlobalStoreProvider } from "app/store/GlobalStore" import { ModalStack } from "app/system/navigation/ModalStack" import { switchTab } from "app/system/navigation/navigate" @@ -5,7 +6,6 @@ import { extractText } from "app/utils/tests/extractText" import { flushPromiseQueue } from "app/utils/tests/flushPromiseQueue" import { mockTrackEvent } from "app/utils/tests/globallyMockedStuff" import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { TouchableWithoutFeedback } from "react-native" import { BottomTabsButton } from "./BottomTabsButton" const TestWrapper: React.FC> = (props) => { @@ -24,7 +24,7 @@ describe(BottomTabsButton, () => { expect(__globalStoreTestUtils__?.getCurrentState().bottomTabs.sessionState.selectedTab).toBe( "home" ) - tree.root.findByType(TouchableWithoutFeedback).props.onPress() + tree.root.findByType(Touchable).props.onPress() await flushPromiseQueue() expect(switchTab).toHaveBeenCalledWith("search") }) @@ -32,7 +32,7 @@ describe(BottomTabsButton, () => { it(`dispatches an analytics action on press`, async () => { const tree = renderWithWrappersLEGACY() expect(mockTrackEvent).not.toHaveBeenCalled() - tree.root.findByType(TouchableWithoutFeedback).props.onPress() + tree.root.findByType(Touchable).props.onPress() await flushPromiseQueue() expect(switchTab).toHaveBeenCalledWith("sell") expect(mockTrackEvent).toHaveBeenCalledWith({ diff --git a/src/app/Scenes/BottomTabs/BottomTabsButton.tsx b/src/app/Scenes/BottomTabs/BottomTabsButton.tsx index ac68f4a6bc6..382af818307 100644 --- a/src/app/Scenes/BottomTabs/BottomTabsButton.tsx +++ b/src/app/Scenes/BottomTabs/BottomTabsButton.tsx @@ -1,13 +1,17 @@ import { tappedTabBar } from "@artsy/cohesion" -import { Flex, PopIn, Text, VisualClueDot, useColor } from "@artsy/palette-mobile" +import { Flex, PopIn, Text, Touchable, VisualClueDot, useColor } from "@artsy/palette-mobile" import { ProgressiveOnboardingFindSavedArtwork } from "app/Components/ProgressiveOnboarding/ProgressiveOnboardingFindSavedArtwork" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import { unsafe__getSelectedTab } from "app/store/GlobalStore" import { VisualClueName } from "app/store/config/visualClues" import { switchTab } from "app/system/navigation/navigate" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { useIsStaging } from "app/utils/hooks/useIsStaging" import { useSelectedTab } from "app/utils/hooks/useSelectedTab" import { useVisualClue } from "app/utils/hooks/useVisualClue" -import { LayoutAnimation, TouchableWithoutFeedback, View } from "react-native" +import { useTabBarBadge } from "app/utils/useTabBarBadge" +import { useMemo } from "react" +import { GestureResponderEvent, LayoutAnimation, View } from "react-native" import { useTracking } from "react-tracking" import styled from "styled-components/native" import { BottomTabOption, BottomTabType } from "./BottomTabType" @@ -19,24 +23,72 @@ export interface BottomTabsButtonProps { badgeCount?: number visualClue?: VisualClueName forceDisplayVisualClue?: boolean + onPress?: (e: GestureResponderEvent) => void } export const BOTTOM_TABS_TEXT_HEIGHT = 15 +// TODO: Improve this component once we remove enableNewNavigation feature flag +// There are too many rerenders happening in this component export const BottomTabsButton: React.FC = ({ tab, - badgeCount = 0, + badgeCount: badgeCountProp = 0, visualClue, - forceDisplayVisualClue, + forceDisplayVisualClue: forceDisplayVisualClueProp, + ...buttonProps }) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const selectedTab = useSelectedTab() + const color = useColor() + const isActive = selectedTab === tab + const { unreadConversationsCount, hasUnseenNotifications } = useTabBarBadge() + + const forceDisplayVisualClue = useMemo(() => { + if (!enableNewNavigation) { + return forceDisplayVisualClueProp + } + + if (tab === "home") { + return hasUnseenNotifications + } + + return false + }, [hasUnseenNotifications, forceDisplayVisualClueProp]) + + const badgeCount = useMemo(() => { + if (!enableNewNavigation) { + return badgeCountProp + } + + if (tab === "inbox") { + return unreadConversationsCount ?? 0 + } + + return 0 + }, [unreadConversationsCount, badgeCountProp]) + const { showVisualClue } = useVisualClue() const tracking = useTracking() + const isStaging = useIsStaging() + + const onPress = (e: GestureResponderEvent) => { + if (enableNewNavigation) { + buttonProps.onPress?.(e) + tracking.trackEvent( + tappedTabBar({ + tab: bottomTabsConfig[tab].analyticsDescription, + badge: badgeCount > 0, + contextScreenOwnerType: BottomTabOption[selectedTab], + }) + ) + + return + } - const onPress = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) if (tab === unsafe__getSelectedTab()) { LegacyNativeModules.ARScreenPresenterModule.popToRootOrScrollToTop(tab) @@ -53,17 +105,26 @@ export const BottomTabsButton: React.FC = ({ } return ( - - - + + @@ -127,7 +188,7 @@ export const BottomTabsButton: React.FC = ({ )} - + ) } diff --git a/src/app/Scenes/City/Components/Event/index.tsx b/src/app/Scenes/City/Components/Event/index.tsx index 3a6fdd90870..cc7fca2676f 100644 --- a/src/app/Scenes/City/Components/Event/index.tsx +++ b/src/app/Scenes/City/Components/Event/index.tsx @@ -1,10 +1,9 @@ -import { Flex, Box, ClassTheme, Text, Button } from "@artsy/palette-mobile" +import { Box, Button, ClassTheme, Flex, Image, Text } from "@artsy/palette-mobile" import { EventMutation } from "__generated__/EventMutation.graphql" -import OpaqueImageView from "app/Components/OpaqueImageView/OpaqueImageView" import { exhibitionDates } from "app/Scenes/Map/exhibitionPeriodParser" import { Show } from "app/Scenes/Map/types" import { navigate } from "app/system/navigation/navigate" -import { Schema, Track, track as _track } from "app/utils/track" +import { track as _track, Schema, Track } from "app/utils/track" import React from "react" import { TouchableWithoutFeedback } from "react-native" import { commitMutation, graphql, RelayProp } from "react-relay" @@ -133,8 +132,8 @@ export class Event extends React.Component { this.handleTap()}> {!!url && ( - - + + )} diff --git a/src/app/Scenes/CompleteMyProfile/hooks/__tests__/useCompleteProfile.tests.tsx b/src/app/Scenes/CompleteMyProfile/hooks/__tests__/useCompleteProfile.tests.tsx index ccee8db7172..76a15eb9c30 100644 --- a/src/app/Scenes/CompleteMyProfile/hooks/__tests__/useCompleteProfile.tests.tsx +++ b/src/app/Scenes/CompleteMyProfile/hooks/__tests__/useCompleteProfile.tests.tsx @@ -1,9 +1,9 @@ import { useNavigation, useRoute } from "@react-navigation/native" -import { renderHook, act } from "@testing-library/react-hooks" +import { act, renderHook } from "@testing-library/react-hooks" import { useToast } from "app/Components/Toast/toastHook" import { CompleteMyProfileStore } from "app/Scenes/CompleteMyProfile/CompleteMyProfileProvider" import { useCompleteProfile } from "app/Scenes/CompleteMyProfile/hooks/useCompleteProfile" -import { navigate as artsyNavigate } from "app/system/navigation/navigate" +import { goBack as ArtsyGoBack } from "app/system/navigation/navigate" import { useUpdateMyProfile } from "app/utils/mutations/useUpdateMyProfile" jest.mock("@react-navigation/native", () => ({ @@ -17,6 +17,7 @@ jest.mock("app/utils/mutations/useUpdateMyProfile", () => ({ jest.mock("app/system/navigation/navigate", () => ({ navigate: jest.fn(), + goBack: jest.fn(), })) jest.mock("app/Components/Toast/toastHook", () => ({ @@ -94,7 +95,7 @@ describe("useCompleteProfile", () => { expect(mockGoBack).toHaveBeenCalled() }) - it('should navigate to "/my-profile" if cannot go back', () => { + it('should navigate back to "/my-profile" if cannot go back', () => { useNavigationMock.mockReturnValue({ navigate: mockNavigate, goBack: mockGoBack, @@ -110,7 +111,7 @@ describe("useCompleteProfile", () => { result.current.goBack() }) - expect(artsyNavigate).toHaveBeenCalledWith("/my-profile") + expect(ArtsyGoBack).toHaveBeenCalled() }) it("should save and exit correctly", () => { diff --git a/src/app/Scenes/CompleteMyProfile/hooks/useCompleteProfile.ts b/src/app/Scenes/CompleteMyProfile/hooks/useCompleteProfile.ts index c16768080ba..412dec00879 100644 --- a/src/app/Scenes/CompleteMyProfile/hooks/useCompleteProfile.ts +++ b/src/app/Scenes/CompleteMyProfile/hooks/useCompleteProfile.ts @@ -11,7 +11,7 @@ import { CompleteMyProfileStore, } from "app/Scenes/CompleteMyProfile/CompleteMyProfileProvider" import { getNextRoute } from "app/Scenes/CompleteMyProfile/hooks/useCompleteMyProfileSteps" -import { navigate as artsyNavigate } from "app/system/navigation/navigate" +import { navigate as artsyNavigate, goBack as systemGoBack } from "app/system/navigation/navigate" import { useUpdateMyProfile } from "app/utils/mutations/useUpdateMyProfile" import { useMemo } from "react" @@ -53,7 +53,7 @@ export const useCompleteProfile = () => { if (canGoBack()) { _goBack() } else { - artsyNavigate("/my-profile") + systemGoBack() } } diff --git a/src/app/Scenes/HomeView/HomeView.tsx b/src/app/Scenes/HomeView/HomeView.tsx index 329b1f02608..1e13e78a4f7 100644 --- a/src/app/Scenes/HomeView/HomeView.tsx +++ b/src/app/Scenes/HomeView/HomeView.tsx @@ -22,7 +22,7 @@ import { ProvidePlaceholderContext } from "app/utils/placeholders" import { usePrefetch } from "app/utils/queryPrefetching" import { requestPushNotificationsPermission } from "app/utils/requestPushNotificationsPermission" import { useMaybePromptForReview } from "app/utils/useMaybePromptForReview" -import { Suspense, useCallback, useEffect, useState } from "react" +import { RefObject, Suspense, useCallback, useEffect, useState } from "react" import { FlatList, RefreshControl } from "react-native" import { fetchQuery, graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay" @@ -133,7 +133,7 @@ export const HomeView: React.FC = () => { } data={sections} keyExtractor={(item) => item.internalID} renderItem={({ item, index }) => { diff --git a/src/app/Scenes/Inbox/Components/Conversations/Composer.tsx b/src/app/Scenes/Inbox/Components/Conversations/Composer.tsx index 3b67e2747cd..12c826048df 100644 --- a/src/app/Scenes/Inbox/Components/Conversations/Composer.tsx +++ b/src/app/Scenes/Inbox/Components/Conversations/Composer.tsx @@ -19,6 +19,8 @@ const Container = styled.View` align-items: flex-start; border-top-width: 1px; border-top-color: ${themeGet("colors.black10")}; + border-bottom-color: ${themeGet("colors.black10")}; + border-bottom-width: 1px; padding: 10px; background-color: ${(p: ContainerProps) => (p.active ? "white" : themeGet("colors.black5"))}; ` diff --git a/src/app/Scenes/Inbox/Components/Conversations/Messages.tsx b/src/app/Scenes/Inbox/Components/Conversations/Messages.tsx index 41ab9ed49a2..be12d2a22be 100644 --- a/src/app/Scenes/Inbox/Components/Conversations/Messages.tsx +++ b/src/app/Scenes/Inbox/Components/Conversations/Messages.tsx @@ -166,7 +166,7 @@ export const Messages: React.FC = forwardRef((props, ref) => { inverted ref={flatList} keyExtractor={(group) => { - return group[0].id + return group[0].__id }} onEndReached={loadMore} onEndReachedThreshold={0.2} diff --git a/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tests.tsx b/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tests.tsx index 4c93633af17..426ccb8b375 100644 --- a/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tests.tsx +++ b/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tests.tsx @@ -1,11 +1,12 @@ import { act, fireEvent } from "@testing-library/react-native" -import { goBack, navigate } from "app/system/navigation/navigate" +import { goBack, switchTab } from "app/system/navigation/navigate" import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" import { OfferSubmittedModal } from "./OfferSubmittedModal" jest.mock("app/system/navigation/navigate", () => ({ navigate: jest.fn(), goBack: jest.fn(), + switchTab: jest.fn(), })) let callback: undefined | (([...args]: any) => void) @@ -37,6 +38,6 @@ describe("OfferSubmittedModal", () => { act(() => callback?.({ orderCode: "1234", message: "Test message" })) fireEvent.press(getAllByText("Go to inbox")[0]) - expect(navigate).toHaveBeenCalledWith("inbox") + expect(switchTab).toHaveBeenCalledWith("inbox") }) }) diff --git a/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tsx b/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tsx index f2e03449613..2ec9624b912 100644 --- a/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tsx +++ b/src/app/Scenes/Inbox/Components/Conversations/OfferSubmittedModal.tsx @@ -1,8 +1,8 @@ -import { Box, Text, Button } from "@artsy/palette-mobile" +import { Box, Button, Text } from "@artsy/palette-mobile" import { NavigationContainer } from "@react-navigation/native" import { FancyModal } from "app/Components/FancyModal/FancyModal" import { FancyModalHeader } from "app/Components/FancyModal/FancyModalHeader" -import { goBack, navigate } from "app/system/navigation/navigate" +import { goBack, switchTab } from "app/system/navigation/navigate" import { useSetWebViewCallback } from "app/utils/useWebViewEvent" import React, { useState } from "react" @@ -20,7 +20,7 @@ export const OfferSubmittedModal: React.FC = (props) => { ) const onGoToInbox = () => { - navigate("inbox") + switchTab("inbox") setVisible(false) } diff --git a/src/app/Scenes/Inbox/Components/Conversations/OpenInquiryModalButton.tsx b/src/app/Scenes/Inbox/Components/Conversations/OpenInquiryModalButton.tsx index 3596678c4ba..aec2fccde00 100644 --- a/src/app/Scenes/Inbox/Components/Conversations/OpenInquiryModalButton.tsx +++ b/src/app/Scenes/Inbox/Components/Conversations/OpenInquiryModalButton.tsx @@ -31,7 +31,13 @@ export const OpenInquiryModalButton: React.FC = ({ - + Always complete purchases with our secure checkout in order to be covered by{" "} { }) // @ts-ignore conversation.root.findAllByType(Touchable)[0].props.onPress() - expect(mockNavigator.push).toHaveBeenCalledWith({ - component: ConversationDetailsQueryRenderer, - passProps: { conversationID: "123" }, - title: "", - }) + expect(navigate).toHaveBeenCalledWith("/conversation/123/details") }) it("handles a dismissed modal with modalDismiss event", () => { diff --git a/src/app/Scenes/Inbox/Screens/Conversation.tsx b/src/app/Scenes/Inbox/Screens/Conversation.tsx index 320f221c059..711e39db0fb 100644 --- a/src/app/Scenes/Inbox/Screens/Conversation.tsx +++ b/src/app/Scenes/Inbox/Screens/Conversation.tsx @@ -1,46 +1,29 @@ -import { InfoCircleIcon, Flex, Text, Touchable } from "@artsy/palette-mobile" +import { BackButton, InfoCircleIcon, Touchable } from "@artsy/palette-mobile" import NetInfo from "@react-native-community/netinfo" import { ConversationQuery } from "__generated__/ConversationQuery.graphql" import { Conversation_me$data } from "__generated__/Conversation_me.graphql" import ConnectivityBanner from "app/Components/ConnectivityBanner" import { LoadFailureView } from "app/Components/LoadFailureView" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { ComposerFragmentContainer } from "app/Scenes/Inbox/Components/Conversations/Composer" import Messages from "app/Scenes/Inbox/Components/Conversations/Messages" import { sendConversationMessage } from "app/Scenes/Inbox/Components/Conversations/SendConversationMessage" import { updateConversation } from "app/Scenes/Inbox/Components/Conversations/UpdateConversation" import { ShadowSeparator } from "app/Scenes/Inbox/Components/ShadowSeparator" import { GlobalStore } from "app/store/GlobalStore" -import { navigationEvents } from "app/system/navigation/navigate" +import { goBack, navigate, navigationEvents } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" import renderWithLoadProgress from "app/utils/renderWithLoadProgress" -import { Schema, Track, track as _track } from "app/utils/track" +import { track as _track, Schema, Track } from "app/utils/track" import React from "react" -import { View } from "react-native" import { createRefetchContainer, graphql, QueryRenderer, RelayRefetchProp } from "react-relay" import styled from "styled-components/native" -import { ConversationDetailsQueryRenderer } from "./ConversationDetails" const Container = styled.View` flex: 1; flex-direction: column; ` -const Header = styled.View` - align-self: stretch; - margin-top: 22px; - flex-direction: column; - margin-bottom: 18px; -` - -// This makes it really easy to style the HeaderTextContainer with space-between -const PlaceholderView = View - -const HeaderTextContainer = styled(Flex)` - flex-direction: row; - justify-content: center; - flex-grow: 1; - justify-content: space-between; -` interface Props { me: Conversation_me$data @@ -168,78 +151,70 @@ export class Conversation extends React.Component { const partnerName = conversation.to.name return ( - (this.composer = composer)} - // @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏 - value={this.state.failedMessageText} - onSubmit={(text) => { - this.setState({ sendingMessage: true, failedMessageText: null }) - sendConversationMessage( - this.props.relay.environment, - // @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏 - conversation, - text, - (_response) => { - this.messageSuccessfullySent(text) - }, - (error) => { - this.messageFailedToSend(error, text) - } - ) - this.messages.scrollToLastMessage() - }} - > - -
- - - - - {partnerName} - - { - this.props.navigator.push({ - component: ConversationDetailsQueryRenderer, - title: "", - passProps: { - conversationID: this.props.me?.conversation?.internalID, - }, - }) - }} - hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }} - > - - - - -
- - {!this.state.isConnected && } - (this.messages = messages)} - conversation={conversation as any} - onDataFetching={(loading) => { - this.setState({ fetchingData: loading }) - }} - onRefresh={() => { - this.props.relay.refetch( - { conversationID: conversation?.internalID }, - null, - (error) => { - if (error) { - console.error("Conversation.tsx", error.message) - } - }, - { force: true } - ) + } + noSeparator + right={ + { + navigate(`/conversation/${this.props.me?.conversation?.internalID}/details`) }} - /> -
-
+ hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }} + > + + + } + > + (this.composer = composer)} + // @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏 + value={this.state.failedMessageText} + onSubmit={(text) => { + this.setState({ sendingMessage: true, failedMessageText: null }) + sendConversationMessage( + this.props.relay.environment, + // @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏 + conversation, + text, + (_response) => { + this.messageSuccessfullySent(text) + }, + (error) => { + this.messageFailedToSend(error, text) + } + ) + this.messages.scrollToLastMessage() + }} + > + + + {!this.state.isConnected && } + (this.messages = messages)} + conversation={conversation as any} + onDataFetching={(loading) => { + this.setState({ fetchingData: loading }) + }} + onRefresh={() => { + this.props.relay.refetch( + { conversationID: conversation?.internalID }, + null, + (error) => { + if (error) { + console.error("Conversation.tsx", error.message) + } + }, + { force: true } + ) + }} + /> + + + ) } } diff --git a/src/app/Scenes/Inbox/Screens/ConversationDetails.tsx b/src/app/Scenes/Inbox/Screens/ConversationDetails.tsx index a350270d5c6..433d8fd72f5 100644 --- a/src/app/Scenes/Inbox/Screens/ConversationDetails.tsx +++ b/src/app/Scenes/Inbox/Screens/ConversationDetails.tsx @@ -11,7 +11,9 @@ import { ShippingFragmentContainer } from "app/Scenes/Inbox/Components/Conversat import { Support } from "app/Scenes/Inbox/Components/Conversations/Support" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import renderWithLoadProgress from "app/utils/renderWithLoadProgress" +import { Fragment } from "react" import { ScrollView } from "react-native" import { createFragmentContainer, graphql, QueryRenderer, RelayProp } from "react-relay" @@ -21,37 +23,34 @@ interface Props { } export const ConversationDetails: React.FC = ({ me }) => { const conversation = me.conversation - const partnerName = conversation?.to.name const item = conversation?.items?.[0]?.item const order = extractNodes(conversation?.orderConnection) const orderItem = order[0] return ( - - - - {!!orderItem && } + + + {!!orderItem && } - {!!item && } + {!!item && } - {!!item && !!orderItem && ( - - )} + {!!item && !!orderItem && ( + + )} - {!!orderItem && ( - <> - - - - )} + {!!orderItem && ( + <> + + + + )} - {!!conversation && } + {!!conversation && } - - - - + +
+
) } @@ -90,20 +89,30 @@ export const ConversationDetailsFragmentContainer = createFragmentContainer(Conv export const ConversationDetailsQueryRenderer: React.FC<{ conversationID: string }> = ({ conversationID }) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - environment={getRelayEnvironment()} - query={graphql` - query ConversationDetailsQuery($conversationID: String!) { - me { - ...ConversationDetails_me + + + environment={getRelayEnvironment()} + query={graphql` + query ConversationDetailsQuery($conversationID: String!) { + me { + ...ConversationDetails_me + } } - } - `} - variables={{ - conversationID, - }} - render={renderWithLoadProgress(ConversationDetailsFragmentContainer)} - /> + `} + variables={{ + conversationID, + }} + render={renderWithLoadProgress(ConversationDetailsFragmentContainer)} + /> + ) } diff --git a/src/app/Scenes/Map/MapRenderer.tsx b/src/app/Scenes/Map/MapRenderer.tsx index 71ac22cc4ff..0f70eefe6c1 100644 --- a/src/app/Scenes/Map/MapRenderer.tsx +++ b/src/app/Scenes/Map/MapRenderer.tsx @@ -50,7 +50,7 @@ export const MapRenderer: React.FC<{ error, retry: () => { isRetrying = true - retry!() + retry?.() }, isRetrying, }} diff --git a/src/app/Scenes/MyAccount/MyAccount.tests.tsx b/src/app/Scenes/MyAccount/MyAccount.tests.tsx index 5e8a735af13..9982d42f160 100644 --- a/src/app/Scenes/MyAccount/MyAccount.tests.tsx +++ b/src/app/Scenes/MyAccount/MyAccount.tests.tsx @@ -1,6 +1,7 @@ import { Text } from "@artsy/palette-mobile" import { MyAccountTestsQuery } from "__generated__/MyAccountTestsQuery.graphql" import { MenuItem } from "app/Components/MenuItem" +import { __globalStoreTestUtils__ } from "app/store/GlobalStore" import { extractText } from "app/utils/tests/extractText" import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" import { Platform } from "react-native" @@ -58,6 +59,9 @@ describe(MyAccountQueryRenderer, () => { ) beforeEach(() => { mockEnvironment = createMockEnvironment() + __globalStoreTestUtils__?.injectFeatureFlags({ + AREnableNewNavigation: true, + }) }) it("truncated long emails", () => { @@ -76,7 +80,7 @@ describe(MyAccountQueryRenderer, () => { return result }) - expect(tree.findAllByType(Text)[2].props.children).toBe( + expect(tree.findAllByType(Text)[1].props.children).toBe( "myverylongemailmyverylongemailmyverylongemail@averylongdomainaverylongdomainaverylongdomain.com" ) }) diff --git a/src/app/Scenes/MyAccount/MyAccount.tsx b/src/app/Scenes/MyAccount/MyAccount.tsx index c5025e0a763..1b495a98d9c 100644 --- a/src/app/Scenes/MyAccount/MyAccount.tsx +++ b/src/app/Scenes/MyAccount/MyAccount.tsx @@ -1,4 +1,4 @@ -import { Spacer, Flex, Box, Text, Button } from "@artsy/palette-mobile" +import { Box, Button, Flex, Spacer, Text } from "@artsy/palette-mobile" import { MyAccountQuery } from "__generated__/MyAccountQuery.graphql" import { MyAccount_me$data } from "__generated__/MyAccount_me.graphql" import { MenuItem } from "app/Components/MenuItem" @@ -9,15 +9,18 @@ import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { useAppleLink } from "app/utils/LinkedAccounts/apple" import { useFacebookLink } from "app/utils/LinkedAccounts/facebook" import { useGoogleLink } from "app/utils/LinkedAccounts/google" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderText } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { times } from "lodash" +import { Fragment } from "react" import { ActivityIndicator, Image, Platform, ScrollView } from "react-native" import { createFragmentContainer, graphql, QueryRenderer, RelayProp } from "react-relay" import { PRICE_BUCKETS } from "./MyAccountEditPriceRange" const MyAccount: React.FC<{ me: MyAccount_me$data; relay: RelayProp }> = ({ me, relay }) => { const hasOnlyOneAuth = me.authentications.length + (me.hasPassword ? 1 : 0) < 2 + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const onlyExistingAuthFor = (provider: "FACEBOOK" | "GOOGLE" | "APPLE") => { return ( @@ -69,8 +72,14 @@ const MyAccount: React.FC<{ me: MyAccount_me$data; relay: RelayProp }> = ({ me, ? PRICE_BUCKETS.find((i) => me.priceRange === i.value)?.label ?? "Select a price range" : "Select a price range" + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - + = ({ me, Delete My Account - + ) } const MyAccountPlaceholder: React.FC = () => { return ( - - - {times(5).map((index: number) => ( - - - - ))} - - + + {times(5).map((index: number) => ( + + + + ))} + ) } diff --git a/src/app/Scenes/MyAccount/MyAccountEditEmail.tests.tsx b/src/app/Scenes/MyAccount/MyAccountEditEmail.tests.tsx index dd4cd1c8aac..0c47b3303e6 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditEmail.tests.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditEmail.tests.tsx @@ -1,7 +1,6 @@ -import { fireEvent } from "@testing-library/react-native" +import { screen } from "@testing-library/react-native" import { MyAccountEditEmailTestsQuery } from "__generated__/MyAccountEditEmailTestsQuery.graphql" import { flushPromiseQueue } from "app/utils/tests/flushPromiseQueue" -import { resolveMostRecentRelayOperation } from "app/utils/tests/resolveMostRecentRelayOperation" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { graphql } from "react-relay" import { MyAccountEditEmailContainer, MyAccountEditEmailQueryRenderer } from "./MyAccountEditEmail" @@ -40,8 +39,8 @@ describe(MyAccountEditEmailQueryRenderer, () => { `, }) - it("shows confirm email toast when email is changed", async () => { - const { getByText, getByLabelText, env } = renderWithRelay({ + it("show email input", async () => { + renderWithRelay({ Me: () => ({ email: "old-email@test.com", }), @@ -49,62 +48,6 @@ describe(MyAccountEditEmailQueryRenderer, () => { await flushPromiseQueue() - expect(getByText("Email")).toBeTruthy() - - const input = getByLabelText("email-input") - expect(input.props.value).toEqual("old-email@test.com") - - fireEvent.changeText(input, "new-email@test.com") - expect(input.props.value).toEqual("new-email@test.com") - - const saveButton = getByLabelText("save-button") - fireEvent.press(saveButton) - - resolveMostRecentRelayOperation(env, { - Me: () => ({ - email: "new-email@test.com", - }), - }) - - await flushPromiseQueue() - - expect(mockShow).toHaveBeenCalledWith( - "Please confirm your new email for this update to take effect", - "middle", - { - duration: "long", - } - ) - }) - - it("does not show confirm email toast when email did not change", async () => { - const { getByText, getByLabelText, env } = renderWithRelay({ - Me: () => ({ - email: "old-email@test.com", - }), - }) - - await flushPromiseQueue() - - expect(getByText("Email")).toBeTruthy() - - const input = getByLabelText("email-input") - expect(input.props.value).toEqual("old-email@test.com") - - fireEvent.changeText(input, "old-email@test.com") - expect(input.props.value).toEqual("old-email@test.com") - - const saveButton = getByLabelText("save-button") - fireEvent.press(saveButton) - - resolveMostRecentRelayOperation(env, { - Me: () => ({ - email: "old-email@test.com", - }), - }) - - await flushPromiseQueue() - - expect(mockShow).not.toHaveBeenCalled() + expect(screen.getByLabelText("email-input")).toBeTruthy() }) }) diff --git a/src/app/Scenes/MyAccount/MyAccountEditEmail.tsx b/src/app/Scenes/MyAccount/MyAccountEditEmail.tsx index f08cf49d165..d60fc4627fa 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditEmail.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditEmail.tsx @@ -1,17 +1,18 @@ -import { Input } from "@artsy/palette-mobile" +import { Flex, Input, Text, Touchable } from "@artsy/palette-mobile" +import { useNavigation } from "@react-navigation/native" import { MyAccountEditEmailQuery } from "__generated__/MyAccountEditEmailQuery.graphql" import { MyAccountEditEmail_me$data } from "__generated__/MyAccountEditEmail_me.graphql" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { useToast } from "app/Components/Toast/toastHook" +import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" -import React, { useEffect, useRef, useState } from "react" +import React, { Fragment, useEffect, useRef, useState } from "react" import { createFragmentContainer, graphql, QueryRenderer, RelayProp } from "react-relay" import { string } from "yup" -import { - MyAccountFieldEditScreen, - MyAccountFieldEditScreenPlaceholder, -} from "./Components/MyAccountFieldEditScreen" +import { MyAccountFieldEditScreen } from "./Components/MyAccountFieldEditScreen" import { updateMyUserProfile } from "./updateMyUserProfile" const MyAccountEditEmail: React.FC<{ me: MyAccountEditEmail_me$data; relay: RelayProp }> = ({ @@ -19,7 +20,10 @@ const MyAccountEditEmail: React.FC<{ me: MyAccountEditEmail_me$data; relay: Rela relay, }) => { const [email, setEmail] = useState(me.email ?? "") + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const [receivedError, setReceivedError] = useState(undefined) + const navigation = useNavigation() useEffect(() => { setReceivedError(undefined) @@ -27,56 +31,89 @@ const MyAccountEditEmail: React.FC<{ me: MyAccountEditEmail_me$data; relay: Rela const isEmailValid = Boolean(email && string().email().isValidSync(email)) + useEffect(() => { + navigation.setOptions({ + headerRight: () => { + return ( + + + Save + + + ) + }, + }) + }, [navigation, email]) + + const handleSave = async () => { + try { + await updateMyUserProfile({ email }, relay.environment) + + if (email !== me.email) { + toast.show("Please confirm your new email for this update to take effect", "middle", { + duration: "long", + }) + } + + goBack() + } catch (e: any) { + setReceivedError(e) + } + } + const editScreenRef = useRef(null) const toast = useToast() - return ( - { - try { - await updateMyUserProfile({ email }, relay.environment) - - if (email !== me.email) { - toast.show("Please confirm your new email for this update to take effect", "middle", { - duration: "long", - }) - } + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) - dismiss() - } catch (e: any) { - setReceivedError(e) - } - }} - > - { - if (isEmailValid) { - editScreenRef.current?.save() - } - }} - error={receivedError} - /> - + return ( + + + { + if (isEmailValid) { + editScreenRef.current?.save() + } + }} + error={receivedError} + /> + + ) } const MyAccountEditEmailPlaceholder: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - + - + ) } diff --git a/src/app/Scenes/MyAccount/MyAccountEditPassword.tests.tsx b/src/app/Scenes/MyAccount/MyAccountEditPassword.tests.tsx index 65489cc56a1..eea1404fe7a 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditPassword.tests.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditPassword.tests.tsx @@ -1,11 +1,18 @@ -import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { MyAccountFieldEditScreen } from "./Components/MyAccountFieldEditScreen" +import { screen } from "@testing-library/react-native" +import { __globalStoreTestUtils__ } from "app/store/GlobalStore" +import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" import { MyAccountEditPassword } from "./MyAccountEditPassword" describe(MyAccountEditPassword, () => { - it("has the right title", () => { - const tree = renderWithWrappersLEGACY() + jest.clearAllMocks() + __globalStoreTestUtils__?.injectFeatureFlags({ + AREnableNewNavigation: true, + }) + + it("has the right titles", () => { + renderWithWrappers() - expect(tree.root.findByType(MyAccountFieldEditScreen).props.title).toEqual("Password") + expect(screen.getByText("Current password")).toBeTruthy() + expect(screen.getByText("New password")).toBeTruthy() }) }) diff --git a/src/app/Scenes/MyAccount/MyAccountEditPassword.tsx b/src/app/Scenes/MyAccount/MyAccountEditPassword.tsx index e59f37918b8..1f1c1a83437 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditPassword.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditPassword.tsx @@ -1,20 +1,21 @@ -import { Flex, Input, Separator } from "@artsy/palette-mobile" +import { Flex, Input, Separator, Text, Touchable } from "@artsy/palette-mobile" +import { useNavigation } from "@react-navigation/native" import { Stack } from "app/Components/Stack" +import { MyAccountFieldEditScreen } from "app/Scenes/MyAccount/Components/MyAccountFieldEditScreen" import { getCurrentEmissionState, GlobalStore, unsafe__getEnvironment } from "app/store/GlobalStore" -import React, { useEffect, useState } from "react" -import { Alert, Text } from "react-native" -import { - MyAccountFieldEditScreen, - MyAccountFieldEditScreenProps, -} from "./Components/MyAccountFieldEditScreen" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import React, { Fragment, useEffect, useState } from "react" +import { Alert } from "react-native" export const MyAccountEditPassword: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [passwordConfirmation, setPasswordConfirmation] = useState("") const [receivedErrorCurrent, setReceivedErrorCurrent] = useState(undefined) const [receivedErrorNew, setReceivedErrorNew] = useState(undefined) const [receivedErrorConfirm, setReceivedErrorConfirm] = useState(undefined) + const navigation = useNavigation() // resetting the errors when user types useEffect(() => { @@ -27,7 +28,7 @@ export const MyAccountEditPassword: React.FC<{}> = ({}) => { setReceivedErrorConfirm(undefined) }, [passwordConfirmation]) - const onSave: MyAccountFieldEditScreenProps["onSave"] = async () => { + const handleSave = async () => { const { authenticationToken, userAgent } = getCurrentEmissionState() const { gravityURL } = unsafe__getEnvironment() if (newPassword !== passwordConfirmation) { @@ -86,47 +87,74 @@ export const MyAccountEditPassword: React.FC<{}> = ({}) => { } } + useEffect(() => { + const isValid = Boolean(currentPassword && newPassword && passwordConfirmation) + + navigation.setOptions({ + headerRight: () => { + return ( + + + Save + + + ) + }, + }) + }, [navigation, currentPassword, newPassword, passwordConfirmation]) + + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + return ( - - - + + + + + + + + + Password must include at least one uppercase letter, one lowercase letter, and one + number. + + + + - - - - Password must include at least one uppercase letter, one lowercase letter, and one number. - - - - - + ) } diff --git a/src/app/Scenes/MyAccount/MyAccountEditPhone.tsx b/src/app/Scenes/MyAccount/MyAccountEditPhone.tsx index f4a78973bd2..921c24a5ef0 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditPhone.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditPhone.tsx @@ -1,22 +1,43 @@ +import { Flex, Text, Touchable } from "@artsy/palette-mobile" +import { useNavigation } from "@react-navigation/native" import { MyAccountEditPhoneQuery } from "__generated__/MyAccountEditPhoneQuery.graphql" import { MyAccountEditPhone_me$data } from "__generated__/MyAccountEditPhone_me.graphql" import { PhoneInput } from "app/Components/Input/PhoneInput/PhoneInput" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" +import { MyAccountFieldEditScreen } from "app/Scenes/MyAccount/Components/MyAccountFieldEditScreen" +import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" -import React, { useEffect, useState } from "react" +import React, { Fragment, useEffect, useState } from "react" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" -import { - MyAccountFieldEditScreen, - MyAccountFieldEditScreenPlaceholder, -} from "./Components/MyAccountFieldEditScreen" import { updateMyUserProfile } from "./updateMyUserProfile" const MyAccountEditPhone: React.FC<{ me: MyAccountEditPhone_me$data }> = ({ me }) => { + const navigation = useNavigation() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const [phone, setPhone] = useState(me.phone ?? "") const [receivedError, setReceivedError] = useState(undefined) const [isValidNumber, setIsValidNumber] = useState(false) + useEffect(() => { + const isValid = canSave() + + navigation.setOptions({ + headerRight: () => { + return ( + + + Save + + + ) + }, + }) + }, [navigation, phone, isValidNumber]) + const canSave = () => { if (!isValidNumber && !!phone.trim()) { return false @@ -29,36 +50,50 @@ const MyAccountEditPhone: React.FC<{ me: MyAccountEditPhone_me$data }> = ({ me } setReceivedError(undefined) }, [phone]) + const handleSave = async () => { + try { + await updateMyUserProfile({ phone }) + goBack() + } catch (e: any) { + setReceivedError(e) + } + } + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + return ( - { - try { - await updateMyUserProfile({ phone }) - dismiss() - } catch (e: any) { - setReceivedError(e) - } - }} - > - - + + + + + ) } const MyAccountEditPhonePlaceholder: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - + - + ) } diff --git a/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tests.tsx b/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tests.tsx index 86a82796a0b..d810fcacda6 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tests.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tests.tsx @@ -1,4 +1,6 @@ +import { screen } from "@testing-library/react-native" import { MyAccountEditPriceRangeTestsQuery } from "__generated__/MyAccountEditPriceRangeTestsQuery.graphql" +import { __globalStoreTestUtils__ } from "app/store/GlobalStore" import { flushPromiseQueue } from "app/utils/tests/flushPromiseQueue" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { graphql } from "react-relay" @@ -10,6 +12,9 @@ import { describe(MyAccountEditPriceRangeQueryRenderer, () => { beforeEach(() => { jest.clearAllMocks() + __globalStoreTestUtils__?.injectFeatureFlags({ + AREnableNewNavigation: true, + }) }) const { renderWithRelay } = setupTestWrapper({ @@ -29,7 +34,7 @@ describe(MyAccountEditPriceRangeQueryRenderer, () => { }) it("submits the changes", async () => { - const { getAllByText, getByText } = renderWithRelay({ + renderWithRelay({ Me: () => ({ priceRange: "-1:2500", priceRangeMax: 2500, @@ -39,8 +44,8 @@ describe(MyAccountEditPriceRangeQueryRenderer, () => { await flushPromiseQueue() - expect(getAllByText("Price Range")[0]).toBeTruthy() + expect(screen.getAllByText("Price Range")[0]).toBeTruthy() - expect(getByText("Under $2,500")).toBeTruthy() + expect(screen.getByText("Under $2,500")).toBeTruthy() }) }) diff --git a/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tsx b/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tsx index a6b49162070..ac911fd44d3 100644 --- a/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tsx +++ b/src/app/Scenes/MyAccount/MyAccountEditPriceRange.tsx @@ -1,20 +1,25 @@ +import { Flex, Text, Touchable } from "@artsy/palette-mobile" +import { useNavigation } from "@react-navigation/native" import { MyAccountEditPriceRangeQuery } from "__generated__/MyAccountEditPriceRangeQuery.graphql" import { MyAccountEditPriceRange_me$data } from "__generated__/MyAccountEditPriceRange_me.graphql" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { Select, SelectOption } from "app/Components/Select" +import { MyAccountFieldEditScreen } from "app/Scenes/MyAccount/Components/MyAccountFieldEditScreen" +import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" -import React, { useEffect, useState } from "react" +import React, { Fragment, useEffect, useState } from "react" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" -import { - MyAccountFieldEditScreen, - MyAccountFieldEditScreenPlaceholder, -} from "./Components/MyAccountFieldEditScreen" import { updateMyUserProfile } from "./updateMyUserProfile" const MyAccountEditPriceRange: React.FC<{ me: MyAccountEditPriceRange_me$data }> = ({ me }) => { + const navigation = useNavigation() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const [receivedError, setReceivedError] = useState(undefined) const [priceRange, setPriceRange] = useState(me.priceRange ?? "") const [priceRangeMax, setPriceRangeMax] = useState(me.priceRangeMax) @@ -24,45 +29,89 @@ const MyAccountEditPriceRange: React.FC<{ setReceivedError(undefined) }, [priceRange]) + useEffect(() => { + const isValid = !!priceRange && priceRange !== me.priceRange + + navigation.setOptions({ + headerRight: () => { + return ( + + + Save + + + ) + }, + }) + }, [navigation, priceRange, me.priceRange]) + + const handleSave = async () => { + try { + await updateMyUserProfile({ priceRangeMin, priceRangeMax }) + goBack() + } catch (e: any) { + setReceivedError(e) + } + } + + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + { + try { + await updateMyUserProfile({ priceRangeMin, priceRangeMax }) + dismiss() + } catch (e: any) { + setReceivedError(e) + } + }} + > + {children} + + ) + return ( - { - try { - await updateMyUserProfile({ priceRangeMin, priceRangeMax }) - dismiss() - } catch (e: any) { - setReceivedError(e) - } - }} - > - { + setPriceRange(value) - // We don't actually accept a priceRange, - // so have to split it into min/max - const [priceRangeMinFin, priceRangeMaxFin] = value.split(":").map((n) => parseInt(n, 10)) + // We don't actually accept a priceRange, + // so have to split it into min/max + const [priceRangeMinFin, priceRangeMaxFin] = value + .split(":") + .map((n) => parseInt(n, 10)) - setPriceRangeMin(priceRangeMinFin) - setPriceRangeMax(priceRangeMaxFin) - }} - hasError={!!receivedError} - /> - + setPriceRangeMin(priceRangeMinFin) + setPriceRangeMax(priceRangeMaxFin) + }} + hasError={!!receivedError} + /> +
+ ) } const MyAccountEditPriceRangePlaceholder: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - + - + ) } diff --git a/src/app/Scenes/MyBids/Components/SaleCard.tsx b/src/app/Scenes/MyBids/Components/SaleCard.tsx index 1f23e8de699..61040b501a9 100644 --- a/src/app/Scenes/MyBids/Components/SaleCard.tsx +++ b/src/app/Scenes/MyBids/Components/SaleCard.tsx @@ -4,16 +4,17 @@ import { ClockFill, ExclamationMarkCircleFill, Flex, - Text, + Image, Separator, + Text, Touchable, } from "@artsy/palette-mobile" import { SaleCard_me$data } from "__generated__/SaleCard_me.graphql" import { SaleCard_sale$data } from "__generated__/SaleCard_sale.graphql" -import OpaqueImageView from "app/Components/OpaqueImageView/OpaqueImageView" import { CompleteRegistrationCTAWrapper } from "app/Scenes/MyBids/Components/CompleteRegistrationCTAWrapper" import { SaleInfo } from "app/Scenes/MyBids/Components/SaleInfo" import { navigate } from "app/system/navigation/navigate" +import { Dimensions } from "react-native" import { createFragmentContainer, graphql } from "react-relay" import { useTracking } from "react-tracking" @@ -109,7 +110,14 @@ export const SaleCard: React.FC = ({ }} > - + {!!sale?.coverImage?.url && ( + + )} + {!!sale.partner?.name && ( diff --git a/src/app/Scenes/MyCollection/Screens/Artwork/MyCollectionArtwork.tsx b/src/app/Scenes/MyCollection/Screens/Artwork/MyCollectionArtwork.tsx index 196970d2784..a0e6fb2112b 100644 --- a/src/app/Scenes/MyCollection/Screens/Artwork/MyCollectionArtwork.tsx +++ b/src/app/Scenes/MyCollection/Screens/Artwork/MyCollectionArtwork.tsx @@ -6,7 +6,7 @@ import { RetryErrorBoundary } from "app/Components/RetryErrorBoundary" import { MyCollectionArtworkAboutWork } from "app/Scenes/MyCollection/Screens/Artwork/Components/ArtworkAbout/MyCollectionArtworkAboutWork" import { MyCollectionArtworkArticles } from "app/Scenes/MyCollection/Screens/Artwork/Components/ArtworkAbout/MyCollectionArtworkArticles" import { GlobalStore } from "app/store/GlobalStore" -import { goBack, navigate, popToRoot } from "app/system/navigation/navigate" +import { goBack, navigate } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" import { getVortexMedium } from "app/utils/marketPriceInsightHelpers" @@ -72,7 +72,6 @@ const MyCollectionArtwork: React.FC = ({ navigate(`my-collection/artworks/${artwork?.internalID}/edit`, { passProps: { mode: "edit", - onDelete: popToRoot, }, }) }, [artwork]) diff --git a/src/app/Scenes/MyCollection/Screens/ArtworkForm/Components/MyCollectionArtworkFormDeleteArtworkModal.tsx b/src/app/Scenes/MyCollection/Screens/ArtworkForm/Components/MyCollectionArtworkFormDeleteArtworkModal.tsx index 36efd702829..9f13052d5df 100644 --- a/src/app/Scenes/MyCollection/Screens/ArtworkForm/Components/MyCollectionArtworkFormDeleteArtworkModal.tsx +++ b/src/app/Scenes/MyCollection/Screens/ArtworkForm/Components/MyCollectionArtworkFormDeleteArtworkModal.tsx @@ -8,7 +8,8 @@ import { Text, } from "@artsy/palette-mobile" import { MyCollectionArtworkFormDeleteArtworkModalQuery } from "__generated__/MyCollectionArtworkFormDeleteArtworkModalQuery.graphql" -import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" +import LoadingModal from "app/Components/Modals/LoadingModal" +import { NoFallback, withSuspense } from "app/utils/hooks/withSuspense" import { useState } from "react" import { Modal } from "react-native" import { graphql, useLazyLoadQuery } from "react-relay" @@ -105,6 +106,6 @@ export const MyCollectionArtworkFormDeleteArtworkModal: React.FC ) }, - LoadingFallback: SpinnerFallback, + LoadingFallback: LoadingModal, ErrorFallback: NoFallback, }) diff --git a/src/app/Scenes/MyCollection/Screens/MyCollectionAddCollectedArtists/MyCollectionAddCollectedArtists.tsx b/src/app/Scenes/MyCollection/Screens/MyCollectionAddCollectedArtists/MyCollectionAddCollectedArtists.tsx index c33e9250c3f..e49a6fed617 100644 --- a/src/app/Scenes/MyCollection/Screens/MyCollectionAddCollectedArtists/MyCollectionAddCollectedArtists.tsx +++ b/src/app/Scenes/MyCollection/Screens/MyCollectionAddCollectedArtists/MyCollectionAddCollectedArtists.tsx @@ -6,12 +6,15 @@ import { MyCollectionAddCollectedArtistsAutosuggest } from "app/Scenes/MyCollect import { MyCollectionAddCollectedArtistsStore } from "app/Scenes/MyCollection/Screens/MyCollectionAddCollectedArtists/MyCollectionAddCollectedArtistsStore" import { useSubmitMyCollectionArtists } from "app/Scenes/MyCollection/hooks/useSubmitMyCollectionArtists" import { dismissModal, goBack } from "app/system/navigation/navigate" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { pluralize } from "app/utils/pluralize" import { refreshMyCollection } from "app/utils/refreshHelpers" import { Suspense } from "react" export const MyCollectionAddCollectedArtists: React.FC<{}> = () => { const { bottom } = useScreenDimensions().safeAreaInsets + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const toast = useToast() const { submit, isSubmitting: isLoading } = useSubmitMyCollectionArtists( "MyCollectionAddCollectedArtists" @@ -33,9 +36,13 @@ export const MyCollectionAddCollectedArtists: React.FC<{}> = () => { return ( - - Add Artists You Collect - + {enableNewNavigation ? ( + + ) : ( + + Add Artists You Collect + + )} null}> diff --git a/src/app/Scenes/MyProfile/MyProfile.tsx b/src/app/Scenes/MyProfile/MyProfile.tsx index 73d6776588c..5f461358dde 100644 --- a/src/app/Scenes/MyProfile/MyProfile.tsx +++ b/src/app/Scenes/MyProfile/MyProfile.tsx @@ -1,12 +1,14 @@ import { NavigationContainer } from "@react-navigation/native" -import { createStackNavigator } from "@react-navigation/stack" +import { createStackNavigator, StackScreenProps } from "@react-navigation/stack" import { MyCollectionArtworkForm } from "app/Scenes/MyCollection/Screens/ArtworkForm/MyCollectionArtworkForm" import { MyProfileEditFormScreen } from "./MyProfileEditForm" import { MyProfileHeaderMyCollectionAndSavedWorksQueryRenderer } from "./MyProfileHeaderMyCollectionAndSavedWorks" const Stack = createStackNavigator() -export const MyProfile = () => { +type MyProfileProps = StackScreenProps + +export const MyProfile: React.FC = () => { return ( void } -export const MyProfileEditForm: React.FC = ({ onSuccess }) => { +export const MyProfileEditForm: React.FC = () => { const { trackEvent } = useTracking() const data = useLazyLoadQuery(MyProfileEditFormScreenQuery, {}) const { updateProfile, isLoading, setIsLoading } = useEditProfile() @@ -127,7 +129,9 @@ export const MyProfileEditForm: React.FC = ({ onSuccess initialValues: { name: me?.name ?? "", displayLocation: { display: buildLocationDisplay(me?.location ?? null) }, - location: { ...me?.location }, + location: { + ...me?.location, + }, profession: me?.profession ?? "", otherRelevantPositions: me?.otherRelevantPositions ?? "", photo: me?.icon?.url || "", @@ -145,9 +149,7 @@ export const MyProfileEditForm: React.FC = ({ onSuccess setIsLoading(false) } - InteractionManager.runAfterInteractions(() => { - onSuccess?.() - }) + fetchProfileData() navigation.goBack() }, validationSchema: editMyProfileSchema, @@ -184,22 +186,10 @@ export const MyProfileEditForm: React.FC = ({ onSuccess } }, [showVerificationBannerForEmail, showVerificationBannerForID]) - const onLeftButtonPressHandler = () => { - navigation.goBack() - } - const showCompleteYourProfileBanner = !me?.collectorProfile?.isProfileComplete return ( <> - - Edit Profile - - {!!showCompleteYourProfileBanner && ( = (props) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - }> - - - + + + }> + + + + ) } const LoadingSkeleton = () => { return ( - - - Edit Profile - - - - + diff --git a/src/app/Scenes/MyProfile/MyProfileHeader.tests.tsx b/src/app/Scenes/MyProfile/MyProfileHeader.tests.tsx index d01818bb91c..d6f4b72db48 100644 --- a/src/app/Scenes/MyProfile/MyProfileHeader.tests.tsx +++ b/src/app/Scenes/MyProfile/MyProfileHeader.tests.tsx @@ -24,9 +24,7 @@ describe("MyProfileHeader", () => { expect(profileImage).toBeTruthy() fireEvent.press(profileImage) expect(navigate).toHaveBeenCalledTimes(1) - expect(navigate).toHaveBeenCalledWith("/my-profile/edit", { - passProps: { onSuccess: expect.anything() }, - }) + expect(navigate).toHaveBeenCalledWith("/my-profile/edit") }) describe("settings screen", () => { diff --git a/src/app/Scenes/MyProfile/MyProfileHeader.tsx b/src/app/Scenes/MyProfile/MyProfileHeader.tsx index 5bbcf774501..03cde167f41 100644 --- a/src/app/Scenes/MyProfile/MyProfileHeader.tsx +++ b/src/app/Scenes/MyProfile/MyProfileHeader.tsx @@ -1,39 +1,38 @@ import { ActionType, ContextModule, OwnerType, TappedCompleteYourProfile } from "@artsy/cohesion" import { + BellIcon, + Button, Flex, + HeartIcon, + Image, MapPinIcon, + MultiplePersonsIcon, + PersonIcon, SettingsIcon, + ShieldFilledIcon, + SimpleMessage, + Skeleton, + SkeletonBox, + SkeletonText, Spacer, Text, Touchable, useColor, - PersonIcon, useSpace, VerifiedPersonIcon, - ShieldFilledIcon, - HeartIcon, - MultiplePersonsIcon, - BellIcon, - SkeletonBox, - Skeleton, - SkeletonText, - Button, - Image, - SimpleMessage, } from "@artsy/palette-mobile" import { MyProfileHeaderQuery } from "__generated__/MyProfileHeaderQuery.graphql" import { MyProfileHeader_me$key } from "__generated__/MyProfileHeader_me.graphql" import { navigate } from "app/system/navigation/navigate" +import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { withSuspense } from "app/utils/hooks/withSuspense" -import { useRefetch } from "app/utils/relayHelpers" import { TouchableOpacity } from "react-native" -import { useFragment, useLazyLoadQuery, graphql } from "react-relay" +import { fetchQuery, graphql, useFragment, useLazyLoadQuery } from "react-relay" interface MyProfileHeaderProps { meProp: MyProfileHeader_me$key } export const MyProfileHeader: React.FC = ({ meProp }) => { - const { refetch } = useRefetch() const me = useFragment(myProfileHeaderFragment, meProp) const space = useSpace() @@ -54,15 +53,7 @@ export const MyProfileHeader: React.FC = ({ meProp }) => { accessibilityRole="button" haptic hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }} - onPress={() => - navigate("/my-profile/settings", { - passProps: { - onSuccess: () => { - refetch() - }, - }, - }) - } + onPress={() => navigate("/my-profile/settings")} style={{ height: "100%" }} > @@ -73,13 +64,7 @@ export const MyProfileHeader: React.FC = ({ meProp }) => { { - navigate("/my-profile/edit", { - passProps: { - onSuccess: () => { - refetch() - }, - }, - }) + navigate("/my-profile/edit") }} testID="profile-image" style={{ @@ -139,7 +124,7 @@ export const MyProfileHeader: React.FC = ({ meProp }) => { )} - + {/* Activity */} @@ -208,7 +193,13 @@ const MyProfileHeaderPlaceholder: React.FC<{}> = () => { - + + + + + + + @@ -285,6 +276,10 @@ const myProfileHeaderQuery = graphql` } ` +export const fetchProfileData = async () => { + return fetchQuery(getRelayEnvironment(), myProfileHeaderQuery, {}) +} + export const MyProfileHeaderQueryRenderer = withSuspense({ Component: (props) => { const data = useLazyLoadQuery( diff --git a/src/app/Scenes/MyProfile/MyProfilePayment.tsx b/src/app/Scenes/MyProfile/MyProfilePayment.tsx index 03696732064..adcd4e9af51 100644 --- a/src/app/Scenes/MyProfile/MyProfilePayment.tsx +++ b/src/app/Scenes/MyProfile/MyProfilePayment.tsx @@ -1,4 +1,4 @@ -import { Spacer, Flex, Text } from "@artsy/palette-mobile" +import { Flex, Spacer, Text } from "@artsy/palette-mobile" import { MyProfilePaymentDeleteCardMutation } from "__generated__/MyProfilePaymentDeleteCardMutation.graphql" import { MyProfilePaymentQuery } from "__generated__/MyProfilePaymentQuery.graphql" import { MyProfilePayment_me$data } from "__generated__/MyProfilePayment_me.graphql" @@ -8,10 +8,11 @@ import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { navigate } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderText } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { times } from "lodash" -import React, { useCallback, useEffect, useReducer, useState } from "react" +import React, { Fragment, useCallback, useEffect, useReducer, useState } from "react" import { ActivityIndicator, Alert, @@ -133,56 +134,49 @@ const MyProfilePayment: React.FC<{ me: MyProfilePayment_me$data; relay: RelayPag const creditCards = extractNodes(me.creditCards) return ( - - } - data={creditCards} - keyExtractor={(item) => item.internalID} - contentContainerStyle={{ paddingTop: creditCards.length === 0 ? 10 : 20 }} - renderItem={({ item }) => ( - - - {deletingIDs[item.internalID] ? ( - - ) : ( - onRemove(item.internalID)} - hitSlop={{ top: 10, left: 20, right: 20, bottom: 10 }} - > - - Remove - - - )} - - )} - onEndReached={onLoadMore} - ItemSeparatorComponent={() => } - ListFooterComponent={ - - navigate("/my-profile/payment/new-card")} - /> - {!!isLoadingMore && } - - } - /> - + } + data={creditCards} + keyExtractor={(item) => item.internalID} + contentContainerStyle={{ paddingTop: creditCards.length === 0 ? 10 : 20 }} + renderItem={({ item }) => ( + + + {deletingIDs[item.internalID] ? ( + + ) : ( + onRemove(item.internalID)} + hitSlop={{ top: 10, left: 20, right: 20, bottom: 10 }} + > + + Remove + + + )} + + )} + onEndReached={onLoadMore} + ItemSeparatorComponent={() => } + ListFooterComponent={ + + navigate("/my-profile/payment/new-card")} /> + {!!isLoadingMore && } + + } + /> ) } export const MyProfilePaymentPlaceholder: React.FC<{}> = () => ( - - - {times(2).map((index: number) => ( - - - - ))} - - + + {times(2).map((index: number) => ( + + + + ))} + ) const MyProfilePaymentContainer = createPaginationContainer( @@ -228,22 +222,31 @@ const MyProfilePaymentContainer = createPaginationContainer( ) export const MyProfilePaymentQueryRenderer: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - environment={getRelayEnvironment()} - query={graphql` - query MyProfilePaymentQuery($count: Int!) { - me { - ...MyProfilePayment_me @arguments(count: $count) + + + environment={getRelayEnvironment()} + query={graphql` + query MyProfilePaymentQuery($count: Int!) { + me { + ...MyProfilePayment_me @arguments(count: $count) + } } - } - `} - render={renderWithPlaceholder({ - Container: MyProfilePaymentContainer, - renderPlaceholder: () => , - })} - variables={{ count: NUM_CARDS_TO_FETCH }} - cacheConfig={{ force: true }} - /> + `} + render={renderWithPlaceholder({ + Container: MyProfilePaymentContainer, + renderPlaceholder: () => , + })} + variables={{ count: NUM_CARDS_TO_FETCH }} + cacheConfig={{ force: true }} + /> + ) } diff --git a/src/app/Scenes/MyProfile/MyProfilePaymentNewCreditCard.tsx b/src/app/Scenes/MyProfile/MyProfilePaymentNewCreditCard.tsx index 342cf35e4fa..f6b5953d0e5 100644 --- a/src/app/Scenes/MyProfile/MyProfilePaymentNewCreditCard.tsx +++ b/src/app/Scenes/MyProfile/MyProfilePaymentNewCreditCard.tsx @@ -1,15 +1,19 @@ -import { Input, Spacer } from "@artsy/palette-mobile" +import { Flex, Input, Spacer, Text, Touchable } from "@artsy/palette-mobile" +import { useNavigation } from "@react-navigation/native" import { useStripe } from "@stripe/stripe-react-native" import { CreateCardTokenParams } from "@stripe/stripe-react-native/lib/typescript/src/types/Token" import { MyProfilePaymentNewCreditCardSaveCardMutation } from "__generated__/MyProfilePaymentNewCreditCardSaveCardMutation.graphql" import { CountrySelect } from "app/Components/CountrySelect" import { CreditCardField } from "app/Components/CreditCardField/CreditCardField" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { Select } from "app/Components/Select/SelectV2" import { Stack } from "app/Components/Stack" -import { MyAccountFieldEditScreen } from "app/Scenes/MyAccount/Components/MyAccountFieldEditScreen" +import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { Action, Computed, action, computed, useLocalStore } from "easy-peasy" -import React, { useRef } from "react" +import React, { Fragment, useEffect, useRef } from "react" +import { Alert, ScrollView } from "react-native" import { commitMutation, graphql } from "react-relay" import { __triggerRefresh } from "./MyProfilePayment" @@ -64,6 +68,7 @@ interface Store { export const MyProfilePaymentNewCreditCard: React.FC<{}> = ({}) => { const { createToken } = useStripe() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") const [state, actions] = useLocalStore(() => ({ fields: { @@ -91,7 +96,30 @@ export const MyProfilePaymentNewCreditCard: React.FC<{}> = ({}) => { const stateRef = useRef(null) const countryRef = useRef>(null) - const screenRef = useRef(null) + const navigation = useNavigation() + + const scrollViewRef = useRef(null) + + useEffect(() => { + const isValid = state.allPresent + + navigation.setOptions({ + headerRight: () => { + return ( + { + handleSave() + }} + disabled={!isValid} + > + + Save + + + ) + }, + }) + }, [navigation, state.allPresent]) const buildTokenParams = (): CreateCardTokenParams => { return { @@ -108,124 +136,131 @@ export const MyProfilePaymentNewCreditCard: React.FC<{}> = ({}) => { } } - return ( - { - try { - const tokenBody = buildTokenParams() - const stripeResult = await createToken(tokenBody) - const tokenId = stripeResult.token?.id + const handleSave = async () => { + try { + const tokenBody = buildTokenParams() + const stripeResult = await createToken(tokenBody) + const tokenId = stripeResult.token?.id - if (!stripeResult || stripeResult.error || !tokenId) { - throw new Error( - `Unexpected stripe card tokenization result ${JSON.stringify(stripeResult.error)}` - ) - } - const gravityResult = await saveCreditCard(tokenId) - if (gravityResult.createCreditCard?.creditCardOrError?.creditCard) { - await __triggerRefresh?.() - } else { - // TODO: we can probably present these errors to the user? - throw new Error( - `Error trying to save card ${JSON.stringify( - gravityResult.createCreditCard?.creditCardOrError?.mutationError - )}` - ) - } - dismiss() - } catch (e) { - console.error(e) - alert( - "Something went wrong while attempting to save your credit card. Please try again or contact us." - ) - } - }} - > - - <> - { - actions.fields.creditCard.setValue({ - valid: cardDetails.complete, - params: { - expMonth: cardDetails.expiryMonth, - expYear: cardDetails.expiryYear, - last4: cardDetails.last4, - }, - }) - }} - /> - + if (!stripeResult || stripeResult.error || !tokenId) { + throw new Error( + `Unexpected stripe card tokenization result ${JSON.stringify(stripeResult.error)}` + ) + } + const gravityResult = await saveCreditCard(tokenId) + if (gravityResult.createCreditCard?.creditCardOrError?.creditCard) { + await __triggerRefresh?.() + } else { + // TODO: we can probably present these errors to the user? + throw new Error( + `Error trying to save card ${JSON.stringify( + gravityResult.createCreditCard?.creditCardOrError?.mutationError + )}` + ) + } + goBack() + } catch (e) { + console.error(e) + Alert.alert( + "Something went wrong while attempting to save your credit card. Please try again or contact us." + ) + } + } + + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + return ( + + + + + <> + { + actions.fields.creditCard.setValue({ + valid: cardDetails.complete, + params: { + expMonth: cardDetails.expiryMonth, + expYear: cardDetails.expiryYear, + last4: cardDetails.last4, + }, + }) + }} + /> + - addressLine1Ref.current?.focus()} - /> - addressLine2Ref.current?.focus()} - /> - cityRef.current?.focus()} - /> - postalCodeRef.current?.focus()} - /> - stateRef.current?.focus()} - /> + addressLine1Ref.current?.focus()} + /> + addressLine2Ref.current?.focus()} + /> + cityRef.current?.focus()} + /> + postalCodeRef.current?.focus()} + /> + stateRef.current?.focus()} + /> - { - stateRef.current?.blur() - screenRef.current?.scrollToEnd() - setTimeout(() => { - countryRef.current?.open() - }, 100) - }} - returnKeyType="next" - /> + { + stateRef.current?.blur() + scrollViewRef.current?.scrollToEnd() + setTimeout(() => { + countryRef.current?.open() + }, 100) + }} + returnKeyType="next" + /> - + - - - + + + +
+ ) } diff --git a/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx b/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx index df77fcc4cb8..159439d2ee3 100644 --- a/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx +++ b/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx @@ -1,4 +1,4 @@ -import { Flex, Box, Text, Separator, Join, Button } from "@artsy/palette-mobile" +import { Box, Button, Flex, Join, Separator, Text } from "@artsy/palette-mobile" import { MyProfilePushNotificationsQuery } from "__generated__/MyProfilePushNotificationsQuery.graphql" import { MyProfilePushNotifications_me$data } from "__generated__/MyProfilePushNotifications_me.graphql" import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" @@ -14,16 +14,8 @@ import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { requestSystemPermissions } from "app/utils/requestPushNotificationsPermission" import useAppState from "app/utils/useAppState" import { debounce } from "lodash" -import React, { useCallback, useEffect, useState } from "react" -import { - ActivityIndicator, - Alert, - Linking, - Platform, - RefreshControl, - ScrollView, - View, -} from "react-native" +import React, { Fragment, useCallback, useEffect, useState } from "react" +import { Alert, Linking, Platform, RefreshControl, ScrollView, View } from "react-native" import { createRefetchContainer, graphql, QueryRenderer, RelayRefetchProp } from "react-relay" const INSTRUCTIONS = Platform.select({ @@ -302,21 +294,12 @@ export const MyProfilePushNotifications: React.FC<{ // TODO: the below logic may be broken on Android 13 with runtime push permissions return ( - : null} - > - } - > - {notificationAuthorizationStatus === PushAuthorizationStatus.Denied && ( - - )} - {notificationAuthorizationStatus === PushAuthorizationStatus.NotDetermined && - Platform.OS === "ios" && } - {renderContent()} - - + }> + {notificationAuthorizationStatus === PushAuthorizationStatus.Denied && } + {notificationAuthorizationStatus === PushAuthorizationStatus.NotDetermined && + Platform.OS === "ios" && } + {renderContent()} + ) } @@ -349,23 +332,32 @@ const MyProfilePushNotificationsContainer = createRefetchContainer( ) export const MyProfilePushNotificationsQueryRenderer: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - environment={getRelayEnvironment()} - query={graphql` - query MyProfilePushNotificationsQuery { - me { - ...MyProfilePushNotifications_me + + + environment={getRelayEnvironment()} + query={graphql` + query MyProfilePushNotificationsQuery { + me { + ...MyProfilePushNotifications_me + } } - } - `} - render={renderWithPlaceholder({ - Container: MyProfilePushNotificationsContainer, - renderPlaceholder: () => ( - - ), - })} - variables={{}} - /> + `} + render={renderWithPlaceholder({ + Container: MyProfilePushNotificationsContainer, + renderPlaceholder: () => ( + + ), + })} + variables={{}} + /> + ) } diff --git a/src/app/Scenes/MyProfile/MyProfileSettings.tests.tsx b/src/app/Scenes/MyProfile/MyProfileSettings.tests.tsx index e1fd7903019..c3409e04604 100644 --- a/src/app/Scenes/MyProfile/MyProfileSettings.tests.tsx +++ b/src/app/Scenes/MyProfile/MyProfileSettings.tests.tsx @@ -1,10 +1,17 @@ import { screen } from "@testing-library/react-native" +import { __globalStoreTestUtils__ } from "app/store/GlobalStore" import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" import { MyProfileSettings } from "./MyProfileSettings" jest.mock("./LoggedInUserInfo") describe(MyProfileSettings, () => { + beforeEach(() => { + __globalStoreTestUtils__?.injectFeatureFlags({ + AREnableNewNavigation: true, + }) + }) + it("renders Edit Profile", () => { renderWithWrappers() expect(screen.getByText("Edit Profile")).toBeOnTheScreen() diff --git a/src/app/Scenes/MyProfile/MyProfileSettings.tsx b/src/app/Scenes/MyProfile/MyProfileSettings.tsx index a9a3e07a2ac..36d4ae4d5b2 100644 --- a/src/app/Scenes/MyProfile/MyProfileSettings.tsx +++ b/src/app/Scenes/MyProfile/MyProfileSettings.tsx @@ -1,41 +1,36 @@ import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion" -import { Button, Flex, Separator, Spacer, Text, useColor } from "@artsy/palette-mobile" -import { FancyModalHeader } from "app/Components/FancyModal/FancyModalHeader" +import { Button, Flex, Separator, Spacer, Text, useColor, useSpace } from "@artsy/palette-mobile" import { MenuItem } from "app/Components/MenuItem" +import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { presentEmailComposer } from "app/NativeModules/presentEmailComposer" import { GlobalStore } from "app/store/GlobalStore" import { navigate } from "app/system/navigation/navigate" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { Fragment } from "react" import { Alert, ScrollView } from "react-native" import { useTracking } from "react-tracking" -interface MyProfileSettingsProps { - onSuccess?: () => void -} - -export const MyProfileSettings: React.FC = ({ onSuccess }) => { +export const MyProfileSettings: React.FC = () => { const color = useColor() + const space = useSpace() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const tracking = useTracking() const separatorColor = color("black5") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) return ( - <> - Account - - + + Settings - - navigate("my-profile/edit", { - passProps: { - onSuccess, - }, - }) - } - /> + navigate("my-profile/edit")} /> navigate("my-account")} /> @@ -84,7 +79,7 @@ export const MyProfileSettings: React.FC = ({ onSuccess
- + ) } diff --git a/src/app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep.tsx b/src/app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep.tsx index 4c9d0fc6093..6fdaa62881a 100644 --- a/src/app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep.tsx +++ b/src/app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep.tsx @@ -108,6 +108,7 @@ export const LoginOTPStep: React.FC = () => { handleChange("otp")(text) }} onBlur={() => validateForm()} + onSubmitEditing={handleSubmit} /> diff --git a/src/app/Scenes/OrderHistory/OrderDetails/Components/OrderDetails.tsx b/src/app/Scenes/OrderHistory/OrderDetails/Components/OrderDetails.tsx index 1cf3e5d7753..803d7687d2e 100644 --- a/src/app/Scenes/OrderHistory/OrderDetails/Components/OrderDetails.tsx +++ b/src/app/Scenes/OrderHistory/OrderDetails/Components/OrderDetails.tsx @@ -5,9 +5,11 @@ import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { PendingOfferSection } from "app/Scenes/OrderHistory/OrderDetails/Components/PendingOfferSection" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox, PlaceholderText } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { compact } from "lodash" +import { Fragment } from "react" import { SectionList } from "react-native" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" import { ArtworkInfoSectionFragmentContainer } from "./ArtworkInfoSection" @@ -100,100 +102,96 @@ const OrderDetails: React.FC = ({ order }) => { ]) return ( - - item.key + index.toString()} - renderItem={({ item }) => { - return ( - - {item} - - ) - }} - stickySectionHeadersEnabled={false} - renderSectionHeader={({ section: { title, data } }) => - title && data ? ( - - - {title} - - - ) : null - } - SectionSeparatorComponent={(data) => ( - - )} - /> - + item.key + index.toString()} + renderItem={({ item }) => { + return ( + + {item} + + ) + }} + stickySectionHeadersEnabled={false} + renderSectionHeader={({ section: { title, data } }) => + title && data ? ( + + + {title} + + + ) : null + } + SectionSeparatorComponent={(data) => ( + + )} + /> ) } export const OrderDetailsPlaceholder: React.FC<{}> = () => ( - - - - - - - - - - - - - - - + + + + + + + - - + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - + + + + + - + + + +
) export const OrderDetailsContainer = createFragmentContainer(OrderDetails, { @@ -243,22 +241,31 @@ export const OrderDetailsContainer = createFragmentContainer(OrderDetails, { }) export const OrderDetailsQueryRender: React.FC<{ orderID: string }> = ({ orderID: orderID }) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - environment={getRelayEnvironment()} - query={graphql` - query OrderDetailsQuery($orderID: ID!) { - order: commerceOrder(id: $orderID) @optionalField { - ...OrderDetails_order + + + environment={getRelayEnvironment()} + query={graphql` + query OrderDetailsQuery($orderID: ID!) { + order: commerceOrder(id: $orderID) @optionalField { + ...OrderDetails_order + } } - } - `} - render={renderWithPlaceholder({ - Container: OrderDetailsContainer, - renderPlaceholder: () => , - })} - variables={{ orderID }} - cacheConfig={{ force: true }} - /> + `} + render={renderWithPlaceholder({ + Container: OrderDetailsContainer, + renderPlaceholder: () => , + })} + variables={{ orderID }} + cacheConfig={{ force: true }} + /> + ) } diff --git a/src/app/Scenes/OrderHistory/OrderDetails/OrderDetails.tests.tsx b/src/app/Scenes/OrderHistory/OrderDetails/OrderDetails.tests.tsx index 34bfc326caf..0a2413b3761 100644 --- a/src/app/Scenes/OrderHistory/OrderDetails/OrderDetails.tests.tsx +++ b/src/app/Scenes/OrderHistory/OrderDetails/OrderDetails.tests.tsx @@ -1,11 +1,9 @@ +import { screen } from "@testing-library/react-native" import { OrderDetailsTestsQuery } from "__generated__/OrderDetailsTestsQuery.graphql" -import { extractText } from "app/utils/tests/extractText" import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { resolveMostRecentRelayOperation } from "app/utils/tests/resolveMostRecentRelayOperation" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { SectionList } from "react-native" -import { graphql, QueryRenderer } from "react-relay" -import { act } from "react-test-renderer" -import { createMockEnvironment, MockPayloadGenerator } from "relay-test-utils" +import { graphql } from "react-relay" import { ArtworkInfoSectionFragmentContainer } from "./Components/ArtworkInfoSection" import { OrderDetailsContainer, @@ -27,109 +25,90 @@ const order = { } describe(OrderDetailsQueryRender, () => { - let mockEnvironment: ReturnType - beforeEach(() => (mockEnvironment = createMockEnvironment())) - - const TestRenderer = () => ( - - environment={mockEnvironment} - query={graphql` - query OrderDetailsTestsQuery @relay_test_operation { - commerceOrder(id: "order-id") { - ...OrderDetails_order - } + const { renderWithRelay } = setupTestWrapper({ + Component: ({ order }) => { + if (!order) { + return null + } + return + }, + query: graphql` + query OrderDetailsTestsQuery @relay_test_operation { + order: commerceOrder(id: "order-id") { + ...OrderDetails_order } - `} - variables={{}} - render={({ props }) => { - if (props?.commerceOrder) { - return - } - }} - /> - ) - - const getWrapper = (mockResolvers = {}) => { - const tree = renderWithWrappersLEGACY() - act(() => { - mockEnvironment.mock.resolveMostRecentOperation((operation) => - MockPayloadGenerator.generate(operation, mockResolvers) - ) - }) - return tree - } + } + `, + }) it("renders without throwing an error", () => { - const tree = renderWithWrappersLEGACY().root - resolveMostRecentRelayOperation(mockEnvironment, { CommerceOrder: () => order }) - expect(tree.findByType(SectionList)).toBeTruthy() - expect(tree.findByType(OrderDetailsHeaderFragmentContainer)).toBeTruthy() - expect(tree.findByType(ArtworkInfoSectionFragmentContainer)).toBeTruthy() - expect(tree.findByType(SummarySectionFragmentContainer)).toBeTruthy() - expect(tree.findByType(PaymentMethodSummaryItemFragmentContainer)).toBeTruthy() - expect(tree.findByType(TrackOrderSectionFragmentContainer)).toBeTruthy() - expect(tree.findByType(ShipsToSectionFragmentContainer)).toBeTruthy() - expect(tree.findByType(SoldBySectionFragmentContainer)).toBeTruthy() - expect(tree.findAllByType(WirePaymentSectionFragmentContainer)).toHaveLength(0) + renderWithRelay({ CommerceOrder: () => order }) + + expect(screen.UNSAFE_getAllByType(SectionList)).toBeTruthy() + expect(screen.UNSAFE_getByType(OrderDetailsHeaderFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(ArtworkInfoSectionFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(SummarySectionFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(PaymentMethodSummaryItemFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(TrackOrderSectionFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(ShipsToSectionFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(SoldBySectionFragmentContainer)).toBeTruthy() + expect(() => screen.UNSAFE_getAllByType(WirePaymentSectionFragmentContainer)).toThrow() }) it("not render ShipsToSection when CommercePickup", () => { - const tree = renderWithWrappersLEGACY().root order.requestedFulfillment.__typename = "CommercePickup" - resolveMostRecentRelayOperation(mockEnvironment, { CommerceOrder: () => order }) - const sections: SectionListItem[] = tree.findByType(SectionList).props.sections + + renderWithRelay({ CommerceOrder: () => order }) + + const sections: SectionListItem[] = screen.UNSAFE_getByType(SectionList).props.sections expect(sections.filter(({ key }) => key === "ShipTo_Section")).toHaveLength(0) }) it("not render TrackOrderSection when CommercePickup", () => { - const tree = renderWithWrappersLEGACY().root order.requestedFulfillment.__typename = "CommercePickup" - resolveMostRecentRelayOperation(mockEnvironment, { CommerceOrder: () => order }) - const sections: SectionListItem[] = tree.findByType(SectionList).props.sections + + renderWithRelay({ CommerceOrder: () => order }) + + const sections: SectionListItem[] = screen.UNSAFE_getByType(SectionList).props.sections expect(sections.filter(({ key }) => key === "TrackOrder_Section")).toHaveLength(0) }) it("not render SoldBySection when partnerName null", () => { - const tree = renderWithWrappersLEGACY().root - resolveMostRecentRelayOperation(mockEnvironment, { + renderWithRelay({ CommerceOrder: () => ({ ...order, lineItems: { edges: [{ node: { artwork: { partner: null } } }] }, }), }) - const sections: SectionListItem[] = tree.findByType(SectionList).props.sections + + const sections: SectionListItem[] = screen.UNSAFE_getByType(SectionList).props.sections expect(sections.filter(({ key }) => key === "Sold_By")).toHaveLength(0) }) it("renders without throwing an error", () => { - getWrapper() + renderWithRelay({ CommerceOrder: () => order }) }) it("renders props for OrderDetails if feature flag is on", () => { - const tree = getWrapper({ + renderWithRelay({ CommerceOrder: () => ({ internalID: "222", requestedFulfillment: { __typename: "CommerceShip", name: "my name" }, }), }) - expect(extractText(tree.root)).toContain("my name") - }) - - it("doesn't render MyCollections app if feature flag is not on", () => { - const tree = getWrapper() - expect(extractText(tree.root)).not.toContain("my name") + expect(screen.getByText(/my name/)).toBeTruthy() }) it("Loads OrderHistoryQueryRender with OrderDetailsPlaceholder", () => { const tree = renderWithWrappersLEGACY( ).root + expect(tree.findAllByType(OrderDetailsPlaceholder)).toHaveLength(1) }) it("renders WirePaymentSection when payment method is wire transfer and order in processing approval state", () => { - const tree = renderWithWrappersLEGACY().root - resolveMostRecentRelayOperation(mockEnvironment, { + renderWithRelay({ CommerceOrder: () => ({ ...order, code: "111111111", @@ -138,6 +117,6 @@ describe(OrderDetailsQueryRender, () => { }), }) - expect(tree.findByType(WirePaymentSectionFragmentContainer)).toBeTruthy() + expect(screen.UNSAFE_getByType(WirePaymentSectionFragmentContainer)).toBeTruthy() }) }) diff --git a/src/app/Scenes/OrderHistory/OrderHistory.tsx b/src/app/Scenes/OrderHistory/OrderHistory.tsx index 989314ba64a..09c34f9d4b8 100644 --- a/src/app/Scenes/OrderHistory/OrderHistory.tsx +++ b/src/app/Scenes/OrderHistory/OrderHistory.tsx @@ -4,10 +4,11 @@ import { OrderHistory_me$data } from "__generated__/OrderHistory_me.graphql" import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox, PlaceholderButton, PlaceholderText } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { times } from "lodash" -import React, { useCallback, useState } from "react" +import React, { Fragment, useCallback, useState } from "react" import { FlatList, RefreshControl } from "react-native" import { createPaginationContainer, graphql, QueryRenderer, RelayPaginationProp } from "react-relay" import { OrderHistoryRowContainer } from "./OrderHistoryRow" @@ -18,7 +19,7 @@ export const OrderHistory: React.FC<{ me: OrderHistory_me$data; relay: RelayPagi relay, me, }) => { - const { color } = useTheme() + const { color, space } = useTheme() const [isRefreshing, setIsRefreshing] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false) @@ -41,74 +42,64 @@ export const OrderHistory: React.FC<{ me: OrderHistory_me$data; relay: RelayPagi const orders = extractNodes(me.orders) return ( - - } - data={orders} - keyExtractor={(order) => order.code} - contentContainerStyle={{ flexGrow: 1, paddingTop: orders.length === 0 ? 10 : 20 }} - renderItem={({ item }) => ( - - - - )} - ListEmptyComponent={ - - - No orders - - - } - onEndReachedThreshold={0.25} - onEndReached={onLoadMore} - ItemSeparatorComponent={() => ( - - - - )} - /> - + } + data={orders} + keyExtractor={(order) => order.code} + contentContainerStyle={{ flexGrow: 1, paddingTop: space(2) }} + renderItem={({ item }) => ( + + + + )} + ListEmptyComponent={ + + + No orders + + + } + onEndReachedThreshold={0.25} + onEndReached={onLoadMore} + ItemSeparatorComponent={() => ( + + + + )} + /> ) } export const OrderHistoryPlaceholder: React.FC<{}> = () => ( - - - {times(2).map((index: number) => ( - - - - - - - - - - - - - - - - - - + + {times(2).map((index: number) => ( + + + + + + + + + + + + + + + + + - - - - - - ))} - - +
+ + + + +
+ ))} +
) export const OrderHistoryContainer = createPaginationContainer( @@ -162,23 +153,33 @@ export const OrderHistoryContainer = createPaginationContainer( ) export const OrderHistoryQueryRender: React.FC<{}> = ({}) => { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + + const Wrapper = enableNewNavigation + ? Fragment + : ({ children }: { children: React.ReactNode }) => ( + {children} + ) + return ( - - environment={getRelayEnvironment()} - query={graphql` - query OrderHistoryQuery($count: Int!) { - me @optionalField { - name - ...OrderHistory_me @arguments(count: $count) + + + environment={getRelayEnvironment()} + query={graphql` + query OrderHistoryQuery($count: Int!) { + me @optionalField { + name + ...OrderHistory_me @arguments(count: $count) + } } - } - `} - render={renderWithPlaceholder({ - Container: OrderHistoryContainer, - renderPlaceholder: () => , - })} - variables={{ count: NUM_ORDERS_TO_FETCH }} - cacheConfig={{ force: true }} - /> + `} + render={renderWithPlaceholder({ + Container: OrderHistoryContainer, + renderPlaceholder: () => , + })} + variables={{ count: NUM_ORDERS_TO_FETCH }} + cacheConfig={{ force: true }} + /> + ) } diff --git a/src/app/Scenes/Partner/Components/PartnerShowRailItem.tsx b/src/app/Scenes/Partner/Components/PartnerShowRailItem.tsx index edd436e5c37..dca85d2c204 100644 --- a/src/app/Scenes/Partner/Components/PartnerShowRailItem.tsx +++ b/src/app/Scenes/Partner/Components/PartnerShowRailItem.tsx @@ -1,11 +1,10 @@ -import { Spacer, Flex, Text, useScreenDimensions, Image } from "@artsy/palette-mobile" +import { Flex, Image, Spacer, Text, Touchable, useScreenDimensions } from "@artsy/palette-mobile" import { PartnerShowRailItem_show$data } from "__generated__/PartnerShowRailItem_show.graphql" import { exhibitionDates } from "app/Scenes/Map/exhibitionPeriodParser" import { navigate } from "app/system/navigation/navigate" import { Schema } from "app/utils/track" import { first } from "lodash" import React from "react" -import { TouchableWithoutFeedback } from "react-native" import { createFragmentContainer, graphql } from "react-relay" import { useTracking } from "react-tracking" @@ -29,7 +28,7 @@ export const PartnerShowRailItem: React.FC = (props) => { const sectionWidth = windowWidth - 100 return ( - + {!!imageURL && ( @@ -44,7 +43,7 @@ export const PartnerShowRailItem: React.FC = (props) => { )} - + ) } diff --git a/src/app/Scenes/Partner/Components/PartnerShows.tsx b/src/app/Scenes/Partner/Components/PartnerShows.tsx index 990c0fd2866..b162c89639e 100644 --- a/src/app/Scenes/Partner/Components/PartnerShows.tsx +++ b/src/app/Scenes/Partner/Components/PartnerShows.tsx @@ -1,4 +1,4 @@ -import { Spacer, Flex, Box, Text, useSpace, Tabs } from "@artsy/palette-mobile" +import { Box, Flex, Spacer, Tabs, Text, Touchable, useSpace } from "@artsy/palette-mobile" import { themeGet } from "@styled-system/theme-get" import { PartnerShows_partner$data } from "__generated__/PartnerShows_partner.graphql" import { TabEmptyState } from "app/Components/TabEmptyState" @@ -6,7 +6,7 @@ import { TabEmptyState } from "app/Components/TabEmptyState" import { navigate } from "app/system/navigation/navigate" import { extractNodes } from "app/utils/extractNodes" import { useState } from "react" -import { ActivityIndicator, ImageBackground, TouchableWithoutFeedback } from "react-native" +import { ActivityIndicator, ImageBackground } from "react-native" import { createPaginationContainer, graphql, RelayPaginationProp } from "react-relay" import styled from "styled-components/native" import { PartnerShowsRailContainer as PartnerShowsRail } from "./PartnerShowsRail" @@ -35,7 +35,7 @@ const ShowGridItem: React.FC = (props) => { const styles = itemIndex % 2 === 0 ? { paddingRight: space(1) } : { paddingLeft: space(1) } return ( - + {showImageURL ? ( @@ -48,7 +48,7 @@ const ShowGridItem: React.FC = (props) => { {show.exhibitionPeriod} - + ) diff --git a/src/app/Scenes/Partner/Components/PartnerShowsRail.tsx b/src/app/Scenes/Partner/Components/PartnerShowsRail.tsx index c1a28ea1a56..6f629fcc624 100644 --- a/src/app/Scenes/Partner/Components/PartnerShowsRail.tsx +++ b/src/app/Scenes/Partner/Components/PartnerShowsRail.tsx @@ -1,4 +1,4 @@ -import { Spacer, Text } from "@artsy/palette-mobile" +import { Flex, Text } from "@artsy/palette-mobile" import { PartnerShowsRail_partner$data } from "__generated__/PartnerShowsRail_partner.graphql" import { extractNodes } from "app/utils/extractNodes" import { isCloseToEdge } from "app/utils/isCloseToEdge" @@ -32,25 +32,24 @@ const PartnerShowsRail: React.FC<{ }) } + if (!currentAndUpcomingShows?.length) { + return null + } + return ( - <> - {!!currentAndUpcomingShows && !!currentAndUpcomingShows.length && ( - <> - Current and upcoming shows - item.id} - renderItem={({ item }) => { - return - }} - /> - - - )} - + + Current and upcoming shows + item.id} + renderItem={({ item }) => { + return + }} + /> + ) } diff --git a/src/app/Scenes/PrivacyRequest/PrivacyRequest.tsx b/src/app/Scenes/PrivacyRequest/PrivacyRequest.tsx index 80837d8d581..0b52732af05 100644 --- a/src/app/Scenes/PrivacyRequest/PrivacyRequest.tsx +++ b/src/app/Scenes/PrivacyRequest/PrivacyRequest.tsx @@ -1,5 +1,4 @@ -import { Spacer, Box, Text, LinkText, Join, Button } from "@artsy/palette-mobile" -import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" +import { Box, Button, Join, LinkText, Spacer, Text } from "@artsy/palette-mobile" import { presentEmailComposer } from "app/NativeModules/presentEmailComposer" import { navigate } from "app/system/navigation/navigate" import React from "react" @@ -7,42 +6,40 @@ import { View } from "react-native" export const PrivacyRequest: React.FC = () => { return ( - - - - - }> - - Please see Artsy’s{" "} - navigate("/privacy")}>Privacy Policy for more - information about the information we collect, how we use it, and why we use it. - - - To submit a personal data request tap the button below or email{" "} - presentEmailComposer("privacy@artsy.net", "Personal Data Request")} - > - privacy@artsy.net. - {" "} - - - - - - + privacy@artsy.net. + {" "} + + + + + ) } diff --git a/src/app/Scenes/Sale/Sale.tests.tsx b/src/app/Scenes/Sale/Sale.tests.tsx index 3bc38010eaf..959a54e0e77 100644 --- a/src/app/Scenes/Sale/Sale.tests.tsx +++ b/src/app/Scenes/Sale/Sale.tests.tsx @@ -1,6 +1,6 @@ import { waitFor } from "@testing-library/react-native" import { CascadingEndTimesBanner } from "app/Scenes/Artwork/Components/CascadingEndTimesBanner" -import { navigate, popParentViewController } from "app/system/navigation/navigate" +import { navigate } from "app/system/navigation/navigate" import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" import { DateTime } from "luxon" import { Suspense } from "react" @@ -9,7 +9,6 @@ import { RegisterToBidButtonContainer } from "./Components/RegisterToBidButton" import { SaleQueryRenderer } from "./Sale" jest.mock("app/system/navigation/navigate", () => ({ - popParentViewController: jest.fn(), navigate: jest.fn(), })) @@ -52,9 +51,11 @@ describe("Sale", () => { expect(navigate).toHaveBeenCalledTimes(0) await waitFor(() => expect(navigate).toHaveBeenCalledTimes(1)) await waitFor(() => - expect(navigate).toHaveBeenCalledWith("https://live-staging.artsy.net/live-sale-slug") + expect(navigate).toHaveBeenCalledWith("https://live-staging.artsy.net/live-sale-slug", { + replaceActiveModal: true, + replaceActiveScreen: true, + }) ) - await waitFor(() => expect(popParentViewController).toHaveBeenCalledTimes(1)) }) it("switches to live auction view when sale goes live with no endAt", async () => { @@ -79,9 +80,11 @@ describe("Sale", () => { expect(navigate).toHaveBeenCalledTimes(0) await waitFor(() => expect(navigate).toHaveBeenCalledTimes(1)) await waitFor(() => - expect(navigate).toHaveBeenCalledWith("https://live-staging.artsy.net/live-sale-slug") + expect(navigate).toHaveBeenCalledWith("https://live-staging.artsy.net/live-sale-slug", { + replaceActiveModal: true, + replaceActiveScreen: true, + }) ) - await waitFor(() => expect(popParentViewController).toHaveBeenCalledTimes(1)) }) it("doesn't switch to live auction view when sale is closed", async () => { @@ -104,7 +107,6 @@ describe("Sale", () => { await waitFor(() => { expect(navigate).toHaveBeenCalledTimes(0) - expect(popParentViewController).toHaveBeenCalledTimes(0) }) }) diff --git a/src/app/Scenes/Sale/Sale.tsx b/src/app/Scenes/Sale/Sale.tsx index e3376fa964a..7aa79f41dfd 100644 --- a/src/app/Scenes/Sale/Sale.tsx +++ b/src/app/Scenes/Sale/Sale.tsx @@ -17,7 +17,7 @@ import { LoadFailureView } from "app/Components/LoadFailureView" import Spinner from "app/Components/Spinner" import { CascadingEndTimesBanner } from "app/Scenes/Artwork/Components/CascadingEndTimesBanner" import { unsafe__getEnvironment } from "app/store/GlobalStore" -import { navigate, popParentViewController } from "app/system/navigation/navigate" +import { navigate } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { AboveTheFoldQueryRenderer } from "app/utils/AboveTheFoldQueryRenderer" import { AuctionWebsocketContextProvider } from "app/utils/Websockets/auctions/AuctionSocketContext" @@ -137,8 +137,10 @@ export const Sale: React.FC = ({ sale, me, below, relay }) => { const switchToLive = () => { const liveBaseURL = unsafe__getEnvironment().predictionURL const liveAuctionURL = `${liveBaseURL}/${sale.slug}` - navigate(liveAuctionURL) - setTimeout(popParentViewController, 500) + navigate(liveAuctionURL, { + replaceActiveScreen: true, + replaceActiveModal: true, + }) } const viewConfigRef = useRef({ viewAreaCoveragePercentThreshold: 30 }) diff --git a/src/app/Scenes/SaleInfo/SaleInfo.tsx b/src/app/Scenes/SaleInfo/SaleInfo.tsx index 01a7188277d..b135e6d885a 100644 --- a/src/app/Scenes/SaleInfo/SaleInfo.tsx +++ b/src/app/Scenes/SaleInfo/SaleInfo.tsx @@ -1,5 +1,5 @@ import { ContextModule, OwnerType } from "@artsy/cohesion" -import { Flex, Text, Separator, Join } from "@artsy/palette-mobile" +import { Flex, Text, Separator, Join, useSpace } from "@artsy/palette-mobile" import { SaleInfoQueryRendererQuery } from "__generated__/SaleInfoQueryRendererQuery.graphql" import { SaleInfo_me$data } from "__generated__/SaleInfo_me.graphql" import { SaleInfo_sale$data } from "__generated__/SaleInfo_sale.graphql" @@ -62,6 +62,8 @@ const AuctionIsLive = () => ( const markdownRules = defaultRules({ useNewTextStyles: true }) export const SaleInfo: React.FC = ({ sale, me }) => { + const space = useSpace() + const panResponder = useRef(null) useEffect(() => { panResponder.current = PanResponder.create({ @@ -92,10 +94,10 @@ export const SaleInfo: React.FC = ({ sale, me }) => { return ( - + }> {/* About Auction */} - + About this auction {sale.name} @@ -200,8 +202,10 @@ const BuyersPremium: React.FC<{ sale: SaleInfo_sale$data }> = (props) => { const SaleInfoPlaceholder = () => ( }> - - About this auction + + + About this auction + diff --git a/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tests.tsx b/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tests.tsx index e3692a2898f..34eea26a748 100644 --- a/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tests.tsx +++ b/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tests.tsx @@ -271,13 +271,7 @@ describe("SavedSearchAlertForm", () => { }) it("calls delete mutation when the delete alert button is pressed", async () => { - const onDeletePressMock = jest.fn() - renderWithWrappers( - - ) + renderWithWrappers() fireEvent.press(screen.getByTestId("delete-alert-button")) fireEvent.press(screen.getByTestId("dialog-primary-action-button")) @@ -285,12 +279,6 @@ describe("SavedSearchAlertForm", () => { expect(mockEnvironment.mock.getMostRecentOperation().request.node.operation.name).toBe( "deleteSavedSearchAlertMutation" ) - - await waitFor(() => { - resolveMostRecentRelayOperation(mockEnvironment) - }) - - expect(onDeletePressMock).toHaveBeenCalled() }) }) }) diff --git a/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tsx b/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tsx index acefca5ae91..98af23efd0d 100644 --- a/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tsx +++ b/src/app/Scenes/SavedSearchAlert/SavedSearchAlertForm.tsx @@ -9,7 +9,7 @@ import { import { updateMyUserProfile } from "app/Scenes/MyAccount/updateMyUserProfile" import { getAlertByCriteria } from "app/Scenes/SavedSearchAlert/queries/getAlertByCriteria" import { GlobalStore } from "app/store/GlobalStore" -import { goBack, navigate } from "app/system/navigation/navigate" +import { goBack, navigate, popToRoot } from "app/system/navigation/navigate" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { refreshSavedAlerts } from "app/utils/refreshHelpers" import { FormikProvider, useFormik } from "formik" @@ -56,7 +56,6 @@ export const SavedSearchAlertForm: React.FC = (props) userAllowsEmails, contentContainerStyle, onComplete, - onDeleteComplete, ...other } = props const enableAlertsFiltersSizeFiltering = useFeatureFlag("AREnableAlertsFiltersSizeFiltering") @@ -276,7 +275,7 @@ export const SavedSearchAlertForm: React.FC = (props) try { await deleteSavedSearchMutation(savedSearchAlertId) tracking.trackEvent(tracks.deletedSavedSearch(savedSearchAlertId)) - onDeleteComplete?.() + popToRoot() } catch (error) { console.error(error) } diff --git a/src/app/Scenes/Search/Search.tests.tsx b/src/app/Scenes/Search/Search.tests.tsx index 9066bd5afcf..2aa1b3c387c 100644 --- a/src/app/Scenes/Search/Search.tests.tsx +++ b/src/app/Scenes/Search/Search.tests.tsx @@ -11,7 +11,7 @@ jest.mock("lodash/throttle", () => (fn: any) => { describe("Search", () => { const { renderWithRelay } = setupTestWrapper({ - Component: () => , + Component: () => , }) it("should render a text input with placeholder and no pills", async () => { diff --git a/src/app/Scenes/Search/Search.tsx b/src/app/Scenes/Search/Search.tsx index ec4e114c54e..7cfc51b49a5 100644 --- a/src/app/Scenes/Search/Search.tsx +++ b/src/app/Scenes/Search/Search.tsx @@ -1,6 +1,7 @@ import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion" -import { Spacer, Flex, Box } from "@artsy/palette-mobile" +import { Spacer, Flex, Box, Screen } from "@artsy/palette-mobile" import { useNavigation } from "@react-navigation/native" +import { StackScreenProps } from "@react-navigation/stack" import { SearchQuery, SearchQuery$variables } from "__generated__/SearchQuery.graphql" import { SearchInput } from "app/Components/SearchInput" import { SearchPills } from "app/Scenes/Search/SearchPills" @@ -186,10 +187,14 @@ export const SearchScreenQuery = graphql` } ` -export const SearchScreen: React.FC = () => ( - }> - - +type SearchScreenProps = StackScreenProps + +export const SearchScreen: React.FC = () => ( + + }> + + + ) const Scrollable = styled(ScrollView).attrs(() => ({ diff --git a/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryForm.tsx b/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryForm.tsx index 280979856a8..f9976f4f38b 100644 --- a/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryForm.tsx +++ b/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryForm.tsx @@ -2,7 +2,6 @@ import { Box, Button, Input, LinkText, Spacer, Text } from "@artsy/palette-mobil import { useNavigation } from "@react-navigation/native" import { PhoneInput } from "app/Components/Input/PhoneInput" import { navigate } from "app/system/navigation/navigate" -import { useScreenDimensions } from "app/utils/hooks" import { useFormikContext } from "formik" import { useEffect, useRef } from "react" import { Platform, ScrollView } from "react-native" @@ -13,7 +12,6 @@ export const ConsignmentInquiryForm: React.FC<{ canPopScreen: boolean recipientName?: string }> = ({ confirmLeaveEdit, canPopScreen, recipientName }) => { - const { safeAreaInsets } = useScreenDimensions() const { values, handleChange, errors, handleSubmit, isValid, dirty, validateField } = useFormikContext() @@ -78,7 +76,7 @@ export const ConsignmentInquiryForm: React.FC<{ keyboardDismissMode="interactive" keyboardShouldPersistTaps="handled" > - + {!!recipientName ? `Contact ${recipientName}` : "Contact a specialist"} diff --git a/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryScreen.tsx b/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryScreen.tsx index 66c11e98c0b..e960ad8808f 100644 --- a/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryScreen.tsx +++ b/src/app/Scenes/SellWithArtsy/ConsignmentInquiry/ConsignmentInquiryScreen.tsx @@ -3,14 +3,13 @@ import { SentConsignmentInquiry } from "@artsy/cohesion/dist/Schema/Events/Consi import { ConsignmentInquiryScreenMutation } from "__generated__/ConsignmentInquiryScreenMutation.graphql" import { AbandonFlowModal } from "app/Components/AbandonFlowModal" import { FancyModal } from "app/Components/FancyModal/FancyModal" -import { FancyModalHeader } from "app/Components/FancyModal/FancyModalHeader" import { useToast } from "app/Components/Toast/toastHook" import { goBack } from "app/system/navigation/navigate" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { ArtsyKeyboardAvoidingView } from "app/utils/ArtsyKeyboardAvoidingView" import { FormikProvider, useFormik } from "formik" import { useState } from "react" -import { Environment, graphql, commitMutation } from "react-relay" +import { commitMutation, Environment, graphql } from "react-relay" import { useTracking } from "react-tracking" import * as Yup from "yup" import { ConsignmentInquiryConfirmation } from "./ConsignmentInquiryConfirmation" @@ -135,8 +134,6 @@ export const ConsignmentInquiryScreen: React.FC = ({ <> - - setShowAbandonModal(v)} canPopScreen={canPopScreen} diff --git a/src/app/Scenes/SellWithArtsy/SellWithArtsyHome.tsx b/src/app/Scenes/SellWithArtsy/SellWithArtsyHome.tsx index 8a454d62f81..d3fb4fa2103 100644 --- a/src/app/Scenes/SellWithArtsy/SellWithArtsyHome.tsx +++ b/src/app/Scenes/SellWithArtsy/SellWithArtsyHome.tsx @@ -14,8 +14,9 @@ import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper" import { RefreshEvents, SELL_SCREEN_REFRESH_KEY } from "app/utils/refreshHelpers" import { useSwitchStatusBarStyle } from "app/utils/useStatusBarStyle" import { compact } from "lodash" -import { Suspense, useEffect, useReducer } from "react" +import { RefObject, Suspense, useEffect, useReducer } from "react" import { StatusBarStyle } from "react-native" +import { FlatList } from "react-native-gesture-handler" import { graphql, useLazyLoadQuery } from "react-relay" import { useTracking } from "react-tracking" import { Footer } from "./Components/Footer" @@ -140,7 +141,9 @@ export const SellWithArtsyHome: React.FC = () => { renderItem={({ item }) => item.content} ItemSeparatorComponent={() => } showsVerticalScrollIndicator={false} - innerRef={scrollViewRef} + windowSize={3} + initialNumToRender={__TEST__ ? 21 : 5} + innerRef={scrollViewRef as RefObject} /> diff --git a/src/app/Scenes/SellWithArtsy/SubmitArtwork/UploadPhotos/utils/index.tsx b/src/app/Scenes/SellWithArtsy/SubmitArtwork/UploadPhotos/utils/index.tsx index 4bec8e0087f..b644c3335c4 100644 --- a/src/app/Scenes/SellWithArtsy/SubmitArtwork/UploadPhotos/utils/index.tsx +++ b/src/app/Scenes/SellWithArtsy/SubmitArtwork/UploadPhotos/utils/index.tsx @@ -1,3 +1,4 @@ +import { StackScreenProps } from "@react-navigation/stack" import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType" import { SellWithArtsyHomeQueryRenderer } from "app/Scenes/SellWithArtsy/SellWithArtsyHome" import { GlobalStore } from "app/store/GlobalStore" @@ -9,7 +10,9 @@ export interface SellTabProps { overwriteHardwareBackButtonPath?: BottomTabType } -export const SellWithArtsy: React.FC = () => { +type SellWithArtsyProps = StackScreenProps + +export const SellWithArtsy: React.FC = () => { const sellTabProps = GlobalStore.useAppState((state) => { return state.bottomTabs.sessionState.tabProps.sell ?? {} }) as SellTabProps diff --git a/src/app/Scenes/Show/Show.tsx b/src/app/Scenes/Show/Show.tsx index 7af84dfbcf9..f3c47e9d69f 100644 --- a/src/app/Scenes/Show/Show.tsx +++ b/src/app/Scenes/Show/Show.tsx @@ -1,18 +1,16 @@ -import { Spacer, Flex, Box, Separator } from "@artsy/palette-mobile" +import { Box, Flex, Separator, Spacer } from "@artsy/palette-mobile" import { ShowQuery } from "__generated__/ShowQuery.graphql" import { Show_show$data } from "__generated__/Show_show.graphql" import { ArtworkFiltersStoreProvider } from "app/Components/ArtworkFilter/ArtworkFilterStore" import { PlaceholderGrid } from "app/Components/ArtworkGrids/GenericGrid" import { HeaderArtworksFilterWithTotalArtworks as HeaderArtworksFilter } from "app/Components/HeaderArtworksFilter/HeaderArtworksFilterWithTotalArtworks" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" -import { useScreenDimensions } from "app/utils/hooks" import { PlaceholderBox, PlaceholderText } from "app/utils/placeholders" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { ProvideScreenTracking, Schema } from "app/utils/track" import { times } from "lodash" import React, { useRef, useState } from "react" import { Animated } from "react-native" -import { useSafeAreaInsets } from "react-native-safe-area-context" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" import { ShowArtworksWithNavigation as ShowArtworks } from "./Components/ShowArtworks" import { ShowArtworksEmptyStateFragmentContainer } from "./Components/ShowArtworksEmptyState" @@ -48,7 +46,7 @@ export const Show: React.FC = ({ show }) => { const artworkProps = { show, visible, toggleFilterArtworksModal } const sections: Section[] = [ - { key: "header", element: }, + { key: "header", element: }, ...(Boolean(show.images?.length) ? [{ key: "install-shots", element: }] @@ -109,11 +107,9 @@ export const Show: React.FC = ({ show }) => { keyExtractor={({ key }) => key} stickyHeaderIndices={[sections.findIndex((section) => section.key === "filter") + 1]} viewabilityConfig={viewConfigRef.current} - ListHeaderComponent={} ListFooterComponent={} ItemSeparatorComponent={() => } contentContainerStyle={{ - paddingTop: useScreenDimensions().safeAreaInsets.top, paddingBottom: 40, }} renderItem={({ item: { element } }) => element} @@ -178,9 +174,8 @@ export const ShowQueryRenderer: React.FC = ({ showID }) } export const ShowPlaceholder: React.FC = () => { - const saInsets = useSafeAreaInsets() return ( - + {/* Title */} diff --git a/src/app/routes.ts b/src/app/routes.ts index e53d1e9239b..a6b8891e21a 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -349,14 +349,12 @@ export function getDomainMap(): Record { // Webview routes addWebViewRoute("/auction-faq", { alwaysPresentModally: true, - safeAreaEdges: ["bottom"], }), addWebViewRoute("/buy-now-feature-faq"), addWebViewRoute("/buyer-guarantee"), addWebViewRoute("/categories"), addWebViewRoute("/conditions-of-sale", { alwaysPresentModally: true, - safeAreaEdges: ["bottom"], }), addWebViewRoute("/identity-verification-faq"), addWebViewRoute("/meet-the-specialists"), @@ -364,16 +362,13 @@ export function getDomainMap(): Record { mimicBrowserBackButton: true, useRightCloseButton: true, alwaysPresentModally: true, - safeAreaEdges: ["bottom"], }), addWebViewRoute("/price-database"), addWebViewRoute("/privacy", { alwaysPresentModally: true, - safeAreaEdges: ["bottom"], }), addWebViewRoute("/terms", { alwaysPresentModally: true, - safeAreaEdges: ["bottom"], }), addWebViewRoute("/unsubscribe"), diff --git a/src/app/store/GlobalStore.tsx b/src/app/store/GlobalStore.tsx index 8aa7541781a..3623df31508 100644 --- a/src/app/store/GlobalStore.tsx +++ b/src/app/store/GlobalStore.tsx @@ -250,3 +250,7 @@ export function unsafe__getEnvironment() { } = globalStoreInstance().getState().devicePrefs return { ...strings, stripePublishableKey, env, userIsDev: value } } + +export function unsafe_getDevPrefs() { + return globalStoreInstance().getState().devicePrefs +} diff --git a/src/app/store/__tests__/AuthModel.tests.ts b/src/app/store/__tests__/AuthModel.tests.ts index 2b8008b8da4..0468103db77 100644 --- a/src/app/store/__tests__/AuthModel.tests.ts +++ b/src/app/store/__tests__/AuthModel.tests.ts @@ -15,8 +15,6 @@ import { } from "react-native-fbsdk-next" import Keychain from "react-native-keychain" -jest.unmock("app/NativeModules/LegacyNativeModules") - const mockFetch = jest.fn() ;(global as any).fetch = mockFetch diff --git a/src/app/store/__tests__/migration.tests.ts b/src/app/store/__tests__/migration.tests.ts index 75fb51fc5e2..2396f500878 100644 --- a/src/app/store/__tests__/migration.tests.ts +++ b/src/app/store/__tests__/migration.tests.ts @@ -7,25 +7,6 @@ import { sanitize } from "app/store/persistence" import { max, min, range } from "lodash" import { Platform } from "react-native" -jest.mock("app/NativeModules/LegacyNativeModules", () => ({ - LegacyNativeModules: { - ...jest.requireActual("app/NativeModules/LegacyNativeModules").LegacyNativeModules, - ARNotificationsManager: { - ...jest.requireActual("app/NativeModules/LegacyNativeModules").LegacyNativeModules - .ARNotificationsManager, - nativeState: { - userAgent: "Jest Unit Tests", - authenticationToken: null, - onboardingState: "none", - launchCount: 1, - deviceId: "testDevice", - userID: null, - userEmail: null, - }, - }, - }, -})) - describe(migrate, () => { it("leaves an object untouched if there are no migrations pending", () => { const result = migrate({ diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index 870dd99f235..909ad39d521 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -293,6 +293,11 @@ export const features = { readyForRelease: false, showInDevMenu: true, }, + AREnableNewNavigation: { + description: "Enable new navigation infra (Requires App Restart!)", + readyForRelease: false, + showInDevMenu: true, + }, AREnablePaymentFailureBanner: { description: "Enable payment failure banner", readyForRelease: true, diff --git a/src/app/system/devTools/DevMenu/Components/NavButtons.tsx b/src/app/system/devTools/DevMenu/Components/NavButtons.tsx index db82dbfc302..1039ab5f3fb 100644 --- a/src/app/system/devTools/DevMenu/Components/NavButtons.tsx +++ b/src/app/system/devTools/DevMenu/Components/NavButtons.tsx @@ -7,7 +7,7 @@ export const NavButtons: React.FC<{ onClose(): void }> = ({ onClose }) => { const isLoggedIn = !!GlobalStore.useAppState((state) => !!state.auth.userID) return ( - + }> {!!isLoggedIn && ( goBack() }: { onClose(): void }) => { const userEmail = GlobalStore.useAppState((s) => s.auth.userEmail) - const space = useSpace() + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + const navigation = useNavigation>() const handleBackButton = () => { onClose() @@ -26,45 +32,54 @@ export const DevMenu = ({ onClose = () => goBack() }: { onClose(): void }) => { useBackHandler(handleBackButton) - return ( - - - - Dev Settings - - - - - - Build:{" "} - - v{DeviceInfo.getVersion()}, build {DeviceInfo.getBuildNumber()} ( - {ArtsyNativeModule.gitCommitShortHash}) - - - - Email: {userEmail} - + useEffect(() => { + if (enableNewNavigation) { + navigation?.setOptions({ + headerRight: () => ( + + + + ), + }) + } + }, [navigation]) - NativeModules?.DevMenu?.show()} - /> + return ( + + {!!enableNewNavigation && } - + {!enableNewNavigation && ( + + + Dev Settings + + + + )} - }> - - - - - - - - - + + Build:{" "} + + v{DeviceInfo.getVersion()}, build {DeviceInfo.getBuildNumber()} ( + {ArtsyNativeModule.gitCommitShortHash}) + + + + Email: {userEmail} + + NativeModules?.DevMenu?.show()} /> + + }> + + + + + + + + ) } diff --git a/src/app/system/navigation/navigate.tests.tsx b/src/app/system/navigation/navigate.tests.tsx index e05bdc594d4..aeed5d90f1a 100644 --- a/src/app/system/navigation/navigate.tests.tsx +++ b/src/app/system/navigation/navigate.tests.tsx @@ -237,6 +237,7 @@ describe(navigate, () => { [ "inbox", { + "hidesBackButton": true, "moduleName": "Conversation", "onlyShowInTabName": "inbox", "props": { diff --git a/src/app/system/navigation/navigate.ts b/src/app/system/navigation/navigate.ts index 32d576b0f45..e14c099bb58 100644 --- a/src/app/system/navigation/navigate.ts +++ b/src/app/system/navigation/navigate.ts @@ -1,16 +1,24 @@ import { EventEmitter } from "events" import { ActionType, OwnerType, Screen } from "@artsy/cohesion" +import { + CommonActions, + NavigationContainerRef, + StackActions, + TabActions, +} from "@react-navigation/native" import { addBreadcrumb, captureMessage } from "@sentry/react-native" -import { AppModule, ViewOptions, modules } from "app/AppRegistry" +import { AppModule, modules, ViewOptions } from "app/AppRegistry" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType" import { matchRoute } from "app/routes" -import { GlobalStore, unsafe__getSelectedTab } from "app/store/GlobalStore" +import { GlobalStore, unsafe__getSelectedTab, unsafe_getFeatureFlag } from "app/store/GlobalStore" import { propsStore } from "app/store/PropsStore" import { postEventToProviders } from "app/utils/track/providers" import { visualize } from "app/utils/visualizer" import { InteractionManager, Linking, Platform } from "react-native" +export const internal_navigationRef = { current: null as NavigationContainerRef | null } + export interface ViewDescriptor extends ViewOptions { type: "react" | "native" moduleName: AppModule @@ -116,6 +124,34 @@ export async function navigate(url: string, options: NavigateOptions = {}) { ...module.options, } + const enableNewNavigation = unsafe_getFeatureFlag("AREnableNewNavigation") + + if (enableNewNavigation) { + if (internal_navigationRef.current?.isReady()) { + if (replaceActiveModal || replaceActiveScreen) { + internal_navigationRef.current.dispatch( + StackActions.replace(result.module, { ...result.params, ...options.passProps }) + ) + } else { + if (module.options.onlyShowInTabName) { + switchTab(module.options.onlyShowInTabName) + // We wait for a frame to allow the tab to be switched before we navigate + // This allows us to also override the back button behavior in the tab + requestAnimationFrame(() => { + internal_navigationRef.current?.dispatch( + CommonActions.navigate(result.module, { ...result.params, ...options.passProps }) + ) + }) + } else { + internal_navigationRef.current?.dispatch( + CommonActions.navigate(result.module, { ...result.params, ...options.passProps }) + ) + } + } + } + return + } + // Set props which we will reinject later. See HACKS.md propsStore.setPendingProps(screenDescriptor.moduleName, screenDescriptor.props) @@ -165,6 +201,8 @@ export async function navigate(url: string, options: NavigateOptions = {}) { export const navigationEvents = new EventEmitter() export function switchTab(tab: BottomTabType, props?: object) { + const enableNewNavigation = unsafe_getFeatureFlag("AREnableNewNavigation") + // root tabs are only mounted once so cannot be tracked // like other screens manually track screen views here // home handles this on its own since it is default tab @@ -175,8 +213,15 @@ export function switchTab(tab: BottomTabType, props?: object) { if (props) { GlobalStore.actions.bottomTabs.setTabProps({ tab, props }) } + GlobalStore.actions.bottomTabs.setSelectedTab(tab) - LegacyNativeModules.ARScreenPresenterModule.switchTab(tab) + + if (enableNewNavigation) { + internal_navigationRef?.current?.dispatch(TabActions.jumpTo(tab, props)) + return + } else { + LegacyNativeModules.ARScreenPresenterModule.switchTab(tab) + } } const tracks = { @@ -208,10 +253,16 @@ const tracks = { } export function dismissModal(after?: () => void) { + const enableNewNavigation = unsafe_getFeatureFlag("AREnableNewNavigation") + // We wait for interaction to finish before dismissing the modal, otherwise, // we might get a race condition that causes the UI to freeze InteractionManager.runAfterInteractions(() => { - LegacyNativeModules.ARScreenPresenterModule.dismissModal() + if (enableNewNavigation) { + internal_navigationRef?.current?.dispatch(StackActions.pop()) + } else { + LegacyNativeModules.ARScreenPresenterModule.dismissModal() + } if (Platform.OS === "android") { navigationEvents.emit("modalDismissed") } @@ -221,16 +272,27 @@ export function dismissModal(after?: () => void) { } export function goBack(backProps?: GoBackProps) { - LegacyNativeModules.ARScreenPresenterModule.goBack(unsafe__getSelectedTab()) + const enableNewNavigation = unsafe_getFeatureFlag("AREnableNewNavigation") + navigationEvents.emit("goBack", backProps) -} -export function popParentViewController() { - LegacyNativeModules.ARScreenPresenterModule.popStack(unsafe__getSelectedTab()) + if (enableNewNavigation) { + if (internal_navigationRef.current?.isReady()) { + internal_navigationRef.current.dispatch(StackActions.pop()) + } + return + } + + LegacyNativeModules.ARScreenPresenterModule.goBack(unsafe__getSelectedTab()) } export function popToRoot() { - LegacyNativeModules.ARScreenPresenterModule.popToRootAndScrollToTop(unsafe__getSelectedTab()) + const enableNewNavigation = unsafe_getFeatureFlag("AREnableNewNavigation") + if (enableNewNavigation) { + internal_navigationRef?.current?.dispatch(StackActions.popToTop()) + } else { + LegacyNativeModules.ARScreenPresenterModule.popToRootAndScrollToTop(unsafe__getSelectedTab()) + } } export enum EntityType { diff --git a/src/app/system/navigation/routes.tests.ts b/src/app/system/navigation/routes.tests.ts index cb2eb43f06e..3e5a33220db 100644 --- a/src/app/system/navigation/routes.tests.ts +++ b/src/app/system/navigation/routes.tests.ts @@ -667,33 +667,27 @@ describe("artsy.net routes", () => { it("routes to Terms and Conditions", () => { expect(matchRoute("/terms")).toMatchInlineSnapshot(` - { - "module": "ModalWebView", - "params": { - "alwaysPresentModally": true, - "safeAreaEdges": [ - "bottom", - ], - "url": "/terms", - }, - "type": "match", - } + { + "module": "ModalWebView", + "params": { + "alwaysPresentModally": true, + "url": "/terms", + }, + "type": "match", + } `) }) it("routes to Privacy Policy", () => { expect(matchRoute("/privacy")).toMatchInlineSnapshot(` - { - "module": "ModalWebView", - "params": { - "alwaysPresentModally": true, - "safeAreaEdges": [ - "bottom", - ], - "url": "/privacy", - }, - "type": "match", - } + { + "module": "ModalWebView", + "params": { + "alwaysPresentModally": true, + "url": "/privacy", + }, + "type": "match", + } `) }) @@ -876,9 +870,6 @@ describe("artsy.net routes", () => { "module": "ModalWebView", "params": { "alwaysPresentModally": true, - "safeAreaEdges": [ - "bottom", - ], "url": "/conditions-of-sale", }, "type": "match", @@ -1151,9 +1142,6 @@ describe("artsy.net routes", () => { "module": "ModalWebView", "params": { "alwaysPresentModally": true, - "safeAreaEdges": [ - "bottom", - ], "url": "/privacy", }, "type": "match", diff --git a/src/app/system/relay/defaultEnvironment.ts b/src/app/system/relay/defaultEnvironment.ts index 2fab3724380..c961a1a49bd 100644 --- a/src/app/system/relay/defaultEnvironment.ts +++ b/src/app/system/relay/defaultEnvironment.ts @@ -1,4 +1,5 @@ import { cacheHeaderMiddleware } from "app/system/relay/middlewares/cacheHeaderMiddleware" +import { logRelay } from "app/utils/loggers" import { Environment as IEnvironment } from "react-relay" import { cacheMiddleware, @@ -42,8 +43,8 @@ const network = new RelayNetworkLayer( metaphysicsExtensionsLoggerMiddleware(), cacheHeaderMiddleware(), simpleLoggerMiddleware(), - __DEV__ ? relayErrorMiddleware() : null, - __DEV__ ? perfMiddleware() : null, + __DEV__ && logRelay ? relayErrorMiddleware() : null, + __DEV__ && logRelay ? perfMiddleware() : null, timingMiddleware(), checkAuthenticationMiddleware(), // KEEP AS CLOSE TO THE BOTTOM OF THIS ARRAY AS POSSIBLE. It needs to run as early as possible in the middlewares. ], diff --git a/src/app/utils/bottomTabsHelper.tsx b/src/app/utils/bottomTabsHelper.tsx index 0c673a33ad5..37c59856293 100644 --- a/src/app/utils/bottomTabsHelper.tsx +++ b/src/app/utils/bottomTabsHelper.tsx @@ -1,6 +1,8 @@ import EventEmitter from "events" +import { useScrollToTop } from "@react-navigation/native" import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType" -import { useEffect, useRef } from "react" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import React, { useEffect, useRef } from "react" import { FlatList, ScrollView } from "react-native" export const BottomTabsEvents = new EventEmitter() @@ -13,11 +15,21 @@ export const scrollTabToTop = (tab: BottomTabType) => { } export const useBottomTabsScrollToTop = (tab: BottomTabType, onScrollToTop?: () => void) => { - const ref = useRef(null) + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + + const ref = useRef(null) + + useScrollToTop( + React.useRef({ + scrollToTop: () => { + handleScrollToTopEvent() + }, + }) + ) const handleScrollToTopEvent = () => { - const flatListRef = ref as React.RefObject | null - const scrollViewRef = ref as React.RefObject | null + const flatListRef = ref as unknown as React.RefObject | null + const scrollViewRef = ref as unknown as React.RefObject | null flatListRef?.current?.scrollToOffset?.({ offset: 0 }) scrollViewRef?.current?.scrollTo?.({}) @@ -26,6 +38,10 @@ export const useBottomTabsScrollToTop = (tab: BottomTabType, onScrollToTop?: () } useEffect(() => { + if (enableNewNavigation) { + return + } + BottomTabsEvents.addListener(`${SCROLL_TO_TOP_EVENT}-${tab}`, handleScrollToTopEvent) return () => { diff --git a/src/app/utils/hooks/useBackHandler.tests.ts b/src/app/utils/hooks/useBackHandler.tests.ts index 69924344f14..88753a39c8a 100644 --- a/src/app/utils/hooks/useBackHandler.tests.ts +++ b/src/app/utils/hooks/useBackHandler.tests.ts @@ -1,12 +1,20 @@ import { renderHook } from "@testing-library/react-hooks" import { BackHandler } from "react-native" -import { useBackHandler, useAndroidGoBack } from "./useBackHandler" +import { useBackHandler } from "./useBackHandler" jest.mock("react-native", () => ({ BackHandler: { addEventListener: jest.fn(), removeEventListener: jest.fn(), }, + Platform: { + OS: "ios", + }, + NativeModules: { + ArtsyNativeModule: { + gitCommitShortHash: "1234567", + }, + }, })) describe("useBackHandler Hooks", () => { @@ -60,25 +68,4 @@ describe("useBackHandler Hooks", () => { expect(removeEventListenerMock).toBeCalledWith("hardwareBackPress", handler) }) }) - - describe("useAndroidGoBack", () => { - it("should add back press listener on screen mount", () => { - renderHook(() => useAndroidGoBack()) - - expect(addEventListenerMock).toHaveBeenCalledTimes(1) - expect(removeEventListenerMock).toHaveBeenCalledTimes(0) - }) - - it("should remove back press listener on screen unmount", () => { - const { unmount } = renderHook(() => useAndroidGoBack()) - - expect(addEventListenerMock).toHaveBeenCalledTimes(1) - expect(removeEventListenerMock).toHaveBeenCalledTimes(0) - - unmount() - - expect(addEventListenerMock).toHaveBeenCalledTimes(1) - expect(removeEventListenerMock).toHaveBeenCalledTimes(1) - }) - }) }) diff --git a/src/app/utils/hooks/useBackHandler.ts b/src/app/utils/hooks/useBackHandler.ts index 274d9a7ab52..b998bfe930b 100644 --- a/src/app/utils/hooks/useBackHandler.ts +++ b/src/app/utils/hooks/useBackHandler.ts @@ -1,5 +1,6 @@ import { useFocusEffect } from "@react-navigation/native" import { goBack } from "app/system/navigation/navigate" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { useCallback, useEffect } from "react" import { BackHandler, InteractionManager } from "react-native" @@ -23,8 +24,14 @@ export function useBackHandler(handler: () => boolean) { * */ export function useAndroidGoBack() { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + useFocusEffect( useCallback(() => { + if (enableNewNavigation) { + return + } + const onBackPress = () => { // this is needed in order to wait for the animation to finish // before moving to the previous screen for better performance diff --git a/src/app/utils/hooks/useSelectedTab.ts b/src/app/utils/hooks/useSelectedTab.ts index 523f8981066..f3d07b5610c 100644 --- a/src/app/utils/hooks/useSelectedTab.ts +++ b/src/app/utils/hooks/useSelectedTab.ts @@ -1,12 +1,20 @@ import { useNavigationState } from "@react-navigation/native" import { __unsafe_mainModalStackRef } from "app/NativeModules/ARScreenPresenterModule" import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" export function useSelectedTab(): BottomTabType { + const enableNewNavigation = useFeatureFlag("AREnableNewNavigation") + + const routeName = useNavigationState((state) => state.routes[state.index].name) const tabState = useNavigationState( (state) => state.routes.find((r) => r.state?.type === "tab")?.state ) + if (enableNewNavigation) { + return routeName as BottomTabType + } + const _unsafe_tabState = __unsafe_mainModalStackRef?.current ?.getState() ?.routes.find((r) => r.state?.type === "tab")?.state @@ -18,6 +26,7 @@ export function useSelectedTab(): BottomTabType { if (index === undefined) { return "home" } + return routes[index].name as BottomTabType } } diff --git a/src/app/utils/useDeepLinks.ts b/src/app/utils/useDeepLinks.ts index 2f9427e589f..ef0fc7af7b1 100644 --- a/src/app/utils/useDeepLinks.ts +++ b/src/app/utils/useDeepLinks.ts @@ -8,6 +8,8 @@ import { useTracking } from "react-tracking" export function useDeepLinks() { const isLoggedIn = GlobalStore.useAppState((state) => !!state.auth.userAccessToken) const isHydrated = GlobalStore.useAppState((state) => state.sessionState.isHydrated) + const isNavigationReady = GlobalStore.useAppState((state) => state.sessionState.isNavigationReady) + const launchURL = useRef(null) const { trackEvent } = useTracking() @@ -18,7 +20,7 @@ export function useDeepLinks() { handleDeepLink(url) } }) - }, []) + }, [isNavigationReady]) useEffect(() => { const subscription = Linking.addListener("url", ({ url }) => { @@ -28,7 +30,7 @@ export function useDeepLinks() { return () => { subscription.remove() } - }, [isHydrated, isLoggedIn]) + }, [isHydrated, isLoggedIn, isNavigationReady]) const handleDeepLink = async (url: string) => { let targetURL @@ -58,7 +60,7 @@ export function useDeepLinks() { // If the state is hydrated and the user is logged in // We navigate them to the the deep link - if (isHydrated && isLoggedIn) { + if (isHydrated && isLoggedIn && isNavigationReady) { // and we track the deep link navigate(deepLinkUrl) return @@ -70,13 +72,13 @@ export function useDeepLinks() { } useEffect(() => { - if (isLoggedIn && launchURL.current) { + if (isLoggedIn && launchURL.current && isNavigationReady) { // Navigate to the saved launch url navigate(launchURL.current) // Reset the launchURL launchURL.current = null } - }, [isLoggedIn, isHydrated, launchURL.current]) + }, [isLoggedIn, isHydrated, launchURL.current, isNavigationReady]) } const tracks = { diff --git a/src/app/utils/useTabBarBadge.ts b/src/app/utils/useTabBarBadge.ts new file mode 100644 index 00000000000..ad517942272 --- /dev/null +++ b/src/app/utils/useTabBarBadge.ts @@ -0,0 +1,27 @@ +import { FETCH_NOTIFICATIONS_INFO_INTERVAL } from "app/Scenes/BottomTabs/BottomTabs" +import { GlobalStore } from "app/store/GlobalStore" +import { useEffect } from "react" +import { useInterval } from "react-use" + +export const useTabBarBadge = () => { + const unreadConversationsCount = GlobalStore.useAppState( + (state) => state.bottomTabs.sessionState.unreadCounts.conversations + ) + const hasUnseenNotifications = GlobalStore.useAppState( + (state) => state.bottomTabs.hasUnseenNotifications + ) + + useEffect(() => { + GlobalStore.actions.bottomTabs.fetchNotificationsInfo() + }, []) + + useInterval(() => { + GlobalStore.actions.bottomTabs.fetchNotificationsInfo() + // run this every 60 seconds + }, FETCH_NOTIFICATIONS_INFO_INTERVAL) + + return { + unreadConversationsCount: unreadConversationsCount || undefined, + hasUnseenNotifications, + } +} diff --git a/src/setupJest.tsx b/src/setupJest.tsx index b0155eb343b..92cb5730342 100644 --- a/src/setupJest.tsx +++ b/src/setupJest.tsx @@ -126,7 +126,9 @@ jest.mock("@react-navigation/native", () => { navigate: mockNavigate, dispatch: jest.fn(), addListener: jest.fn(), + setOptions: jest.fn(), }), + useScrollToTop: jest.fn(), } }) diff --git a/yarn.lock b/yarn.lock index 569530093a1..452d8066868 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12928,10 +12928,10 @@ react-native-safe-area-context@3.4.0: resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-3.4.0.tgz#b751f492a7f7470bccb09f1a11ccc276a3c5fe98" integrity sha512-kmzSK8L9LX+1rF6+qPBZR0kjGn5rE0IHNHL4px/lNwyxA+0siekTkCG+BlzbBy4V3yKeLzQ+UxgT9mEtDHs/Tg== -react-native-screens@3.34.0: - version "3.34.0" - resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.34.0.tgz#1291a460c5bc59e2ba581b42d40fa9a58d3b1197" - integrity sha512-8ri3Pd9QcpfXnVckOe/Lnto+BXmSPHV/Q0RB0XW0gDKsCv5wi5k7ez7g1SzgiYHl29MSdiqgjH30zUyOOowOaw== +react-native-screens@3.35.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.35.0.tgz#4acf4c7d331d47d33d0214d415779540b7664e0e" + integrity sha512-rmkqb/M/SQIrXwygk6pXcOhgHltYAhidf1WceO7ujAxkr6XtwmgFyd1HIztsrJa568GrAuwPdQ11I7TpVk+XsA== dependencies: react-freeze "^1.0.0" warn-once "^0.1.0" @@ -14259,16 +14259,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14382,7 +14373,7 @@ stringify-entities@^3.1.0: character-entities-legacy "^1.0.0" xtend "^4.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14396,13 +14387,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -15818,7 +15802,7 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15836,15 +15820,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"