diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index de288b09e3b..b30e75b3ff5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ - diff --git a/android/build.gradle b/android/build.gradle index e42ca0bc556..1318dbf0444 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,10 +5,10 @@ buildscript { ext { googlePlayServicesVersion = "17.0.0" firebaseMessagingVersion = "21.1.0" // matching firebaseIidVersion to avoid duplicate class error - buildToolsVersion = "33.0.0" + buildToolsVersion = "31.0.0" minSdkVersion = 21 - compileSdkVersion = 33 - targetSdkVersion = 33 + compileSdkVersion = 31 + targetSdkVersion = 31 firebaseIidVersion = "21.1.0" // Needed for react-native-device-info googlePlayServicesAuthVersion = "16.0.1" kotlinVersion = "1.5.31" diff --git a/ios/Artsy/App/ARAnalyticsConstants.h b/ios/Artsy/App/ARAnalyticsConstants.h index 1fe24a26d0e..be3e52477cd 100644 --- a/ios/Artsy/App/ARAnalyticsConstants.h +++ b/ios/Artsy/App/ARAnalyticsConstants.h @@ -7,8 +7,10 @@ extern NSString *const ARAnalyticsAppUsageCountProperty; // Notifications +extern NSString *const ARAnalyticsEnabledNotificationsProperty; extern NSString *const ARAnalyticsNotificationReceived; extern NSString *const ARAnalyticsNotificationTapped; +extern NSString *const ARAnalyticsPushNotificationsRequested; // Push notifications diff --git a/ios/Artsy/App/ARAnalyticsConstants.m b/ios/Artsy/App/ARAnalyticsConstants.m index 9429f266891..1bc48757a30 100644 --- a/ios/Artsy/App/ARAnalyticsConstants.m +++ b/ios/Artsy/App/ARAnalyticsConstants.m @@ -2,8 +2,10 @@ NSString *const ARAnalyticsAppUsageCountProperty = @"app launched count"; +NSString *const ARAnalyticsEnabledNotificationsProperty = @"has enabled notifications"; NSString *const ARAnalyticsNotificationReceived = @"notification received"; NSString *const ARAnalyticsNotificationTapped = @"notification tapped"; +NSString *const ARAnalyticsPushNotificationsRequested = @"push notifications requested"; NSString *const ARAnalyticsPushNotificationLocal = @"Artsy notification prompt response"; NSString *const ARAnalyticsPushNotificationApple = @"Apple notification prompt response"; diff --git a/ios/Artsy/App/ARAppDelegate+Emission.m b/ios/Artsy/App/ARAppDelegate+Emission.m index 23f1fd308c7..3bbfa3418e7 100644 --- a/ios/Artsy/App/ARAppDelegate+Emission.m +++ b/ios/Artsy/App/ARAppDelegate+Emission.m @@ -65,6 +65,18 @@ - (AREmission *)setupSharedEmission [AREmission setSharedInstance:emission]; +#pragma mark - Native Module: Push Notification Permissions + + emission.APIModule.directNotificationPermissionPrompter = ^() { + ARAppNotificationsDelegate *delegate = [[JSDecoupledAppDelegate sharedAppDelegate] remoteNotificationsDelegate]; + [delegate registerForDeviceNotificationsWithApple]; + }; + + emission.APIModule.prepromptNotificationPermissionPrompter = ^() { + ARAppNotificationsDelegate *delegate = [[JSDecoupledAppDelegate sharedAppDelegate] remoteNotificationsDelegate]; + [delegate registerForDeviceNotificationsWithContext:ARAppNotificationsRequestContextOnboarding]; + }; + #pragma mark - Native Module: Follow status emission.APIModule.notificationReadStatusAssigner = ^(RCTResponseSenderBlock block) { diff --git a/ios/Artsy/App/ARAppNotificationsDelegate.h b/ios/Artsy/App/ARAppNotificationsDelegate.h index 3c46f98f197..0ce7054ac93 100644 --- a/ios/Artsy/App/ARAppNotificationsDelegate.h +++ b/ios/Artsy/App/ARAppNotificationsDelegate.h @@ -15,6 +15,10 @@ typedef NS_ENUM(NSInteger, ARAppNotificationsRequestContext) { @property (nonatomic, readwrite, assign) ARAppNotificationsRequestContext requestContext; +- (void)registerForDeviceNotificationsWithContext:(ARAppNotificationsRequestContext)requestContext; - (void)applicationDidReceiveRemoteNotification:(NSDictionary *)userInfo inApplicationState:(UIApplicationState)applicationState; +/// Used in admin tools and for react native to request permissions +- (void)registerForDeviceNotificationsWithApple; + @end diff --git a/ios/Artsy/App/ARAppNotificationsDelegate.m b/ios/Artsy/App/ARAppNotificationsDelegate.m index fe9e9604178..7b0dad3c344 100644 --- a/ios/Artsy/App/ARAppNotificationsDelegate.m +++ b/ios/Artsy/App/ARAppNotificationsDelegate.m @@ -24,6 +24,157 @@ @implementation ARAppNotificationsDelegate +#pragma mark - +#pragma mark Local Push Notification Alerts + +- (void)registerForDeviceNotificationsWithContext:(ARAppNotificationsRequestContext)requestContext +{ + self.requestContext = requestContext; + + if (![AROptions boolForOption:ARPushNotificationsSettingsPromptSeen] && + [AROptions boolForOption:ARPushNotificationsAppleDialogueRejected]) { + // if you've rejected Apple's push notification and you've not seen our prompt to send you to settings + // lets show you a prompt to go to settings + [self displayPushNotificationSettingsPrompt]; + } else if (![AROptions boolForOption:ARPushNotificationsAppleDialogueSeen] && [self shouldPresentPushNotificationAgain]) { + // As long as you've not seen Apple's dialogue already we will show you our pre-prompt. + [self displayPushNotificationLocalRequestPrompt]; + } else { + // Otherwise fallback to requesting directly with apple to make sure we have + // up to date push tokens + [self registerForDeviceNotificationsWithApple]; + } +} + +- (void)displayPushNotificationLocalRequestPrompt +{ + UIAlertController *alert = [self pushNotificationPromptAlertController]; + + UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self registerUserInterest]; + }]; + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Don't Allow" style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self registerUserDisinterest]; + }]; + [alert addAction:cancelAction]; + [alert addAction:confirmAction]; + + [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil]; +} + + +- (void)displayPushNotificationSettingsPrompt +{ + UIAlertController *alert = [self pushNotificationPromptAlertController]; + + UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:@"Go to Settings" style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self presentSettings]; + }]; + [alert addAction:settingsAction]; + alert.preferredAction = settingsAction; + [alert addAction:[UIAlertAction actionWithTitle:@"No thanks" style:UIAlertActionStyleCancel handler:nil]]; + [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil]; + + [AROptions setBool:YES forOption:ARPushNotificationsSettingsPromptSeen]; +} + +- (void)registerUserInterest +{ + NSString *analyticsContext = @""; + if (self.requestContext == ARAppNotificationsRequestContextArtistFollow) { + analyticsContext = @"ArtistFollow"; + } else if (self.requestContext == ARAppNotificationsRequestContextOnboarding) { + analyticsContext = @"Onboarding"; + } else if (self.requestContext == ARAppNotificationsRequestContextLaunch) { + analyticsContext = @"Launch"; + } + + analyticsContext = [@[@"PushNotification", analyticsContext] componentsJoinedByString:@""]; + + [[AREmission sharedInstance] sendEvent:ARAnalyticsPushNotificationLocal traits:@{ + @"action_type" : @"Tap", + @"action_name" : @"Yes", + @"context_screen" : analyticsContext, + }]; + [self registerForDeviceNotificationsWithApple]; +} + +- (void)registerUserDisinterest +{ + // Well, in that case we'll store today's date + // and prompt the user in a week's time, if they perform certain actions (e.g. follow an artist) + + NSString *analyticsContext = @""; + if (self.requestContext == ARAppNotificationsRequestContextArtistFollow) { + analyticsContext = @"ArtistFollow"; + } else if (self.requestContext == ARAppNotificationsRequestContextOnboarding) { + analyticsContext = @"Onboarding"; + } else if (self.requestContext == ARAppNotificationsRequestContextLaunch) { + analyticsContext = @"Launch"; + } + + analyticsContext = [@[@"PushNotification", analyticsContext] componentsJoinedByString:@""]; + + [[AREmission sharedInstance] sendEvent:ARAnalyticsPushNotificationLocal traits:@{ + @"action_type" : @"Tap", + @"action_name" : @"Cancel", + @"context_screen" : analyticsContext + }]; + [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:ARPushNotificationsDialogueLastSeenDate]; +} + +- (void)presentSettings +{ + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; +} + +- (UIAlertController *)pushNotificationPromptAlertController +{ + return [UIAlertController alertControllerWithTitle:@"Artsy Would Like to Send You Notifications" + message:@"Turn on notifications to get important updates about artists you follow." + preferredStyle:UIAlertControllerStyleAlert]; +} + +- (BOOL)shouldPresentPushNotificationAgain +{ + // we don't want to ask too often + // currently, we make sure at least a week has passed by since you last saw the dialogue + + NSDate *lastSeenPushNotification = [[NSUserDefaults standardUserDefaults] objectForKey:ARPushNotificationsDialogueLastSeenDate]; + + if (lastSeenPushNotification) { + NSDate *currentDate = [NSDate date]; + + NSTimeInterval timePassed = [currentDate timeIntervalSinceDate:lastSeenPushNotification]; + NSTimeInterval weekInSeconds = (60 * 60 * 24 * 7); + + return timePassed >= weekInSeconds; + } else { + // if you've never seen one before, we'll show you ;) + return YES; + } +} + +#pragma mark - +#pragma mark Push Notification Register + +- (void)registerForDeviceNotificationsWithApple +{ + ARActionLog(@"Registering with Apple for remote notifications."); + UNAuthorizationOptions authOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert); + [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { + NSString *grantedString = granted ? @"YES" : @"NO"; + [[AREmission sharedInstance] sendEvent:ARAnalyticsPushNotificationsRequested traits:@{@"granted" : grantedString}]; + [[Appboy sharedInstance] pushAuthorizationFromUserNotificationCenter:granted]; + }]; + + [[UIApplication sharedApplication] registerForRemoteNotifications]; + [AROptions setBool:YES forOption:ARPushNotificationsAppleDialogueSeen]; +} + #pragma mark - #pragma mark Push Notification Delegate @@ -45,6 +196,7 @@ - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotif }]; #if (TARGET_IPHONE_SIMULATOR == 0) ARErrorLog(@"Error registering for remote notifications: %@", error.localizedDescription); + [AROptions setBool:YES forOption:ARPushNotificationsAppleDialogueRejected]; #endif } @@ -76,9 +228,10 @@ - (void)application:(UIApplication *)application didRegisterForRemoteNotificatio ARActionLog(@"Got device notification token: %@", deviceToken); NSString *previousToken = [[NSUserDefaults standardUserDefaults] stringForKey:ARAPNSDeviceTokenKey]; - // Save device token for dev settings and to prevent excess calls to gravity if tokens don't change + // Save device token purely for the dev settings view. [[NSUserDefaults standardUserDefaults] setValue:deviceToken forKey:ARAPNSDeviceTokenKey]; + [[AREmission sharedInstance] sendIdentifyEvent:@{ARAnalyticsEnabledNotificationsProperty: @1}]; [[Appboy sharedInstance] registerDeviceToken:deviceTokenData]; // We only record device tokens on the Artsy service in case of Beta or App Store builds. diff --git a/ios/Artsy/Constants/ARDefaults.h b/ios/Artsy/Constants/ARDefaults.h index a8958ea0cf6..b63bb9c89df 100644 --- a/ios/Artsy/Constants/ARDefaults.h +++ b/ios/Artsy/Constants/ARDefaults.h @@ -8,6 +8,15 @@ extern NSString *const AROAuthTokenExpiryDateDefault; extern NSString *const ARXAppTokenKeychainKey; extern NSString *const ARXAppTokenExpiryDateDefault; +#pragma mark - +#pragma mark push notifications + +extern NSString *const ARPushNotificationsAppleDialogueSeen; +extern NSString *const ARPushNotificationsAppleDialogueRejected; +extern NSString *const ARPushNotificationsSettingsPromptSeen; +extern NSString *const ARPushNotificationFollowArtist; +extern NSString *const ARPushNotificationsDialogueLastSeenDate; + #pragma mark - #pragma mark user permissions diff --git a/ios/Artsy/Constants/ARDefaults.m b/ios/Artsy/Constants/ARDefaults.m index 0c4061443ce..79efecb6cc1 100644 --- a/ios/Artsy/Constants/ARDefaults.m +++ b/ios/Artsy/Constants/ARDefaults.m @@ -12,6 +12,12 @@ NSString *const ARXAppTokenKeychainKey = @"ARXAppTokenDefault"; NSString *const ARXAppTokenExpiryDateDefault = @"ARXAppTokenExpiryDateDefault"; +NSString *const ARPushNotificationsAppleDialogueSeen = @"eigen-push-seen-dialogue"; +NSString *const ARPushNotificationsAppleDialogueRejected = @"eigen-push-reject-dialogue"; +NSString *const ARPushNotificationsSettingsPromptSeen = @"eigen-push-seen-settings-dialogue"; +NSString *const ARPushNotificationFollowArtist = @"eigen-push-followed-artist"; +NSString *const ARPushNotificationsDialogueLastSeenDate = @"eigen-push-seen-dialogue-date"; + NSString *const ARAugmentedRealityHasSeenSetup = @"ARAugmentedRealityHasSeenSetup"; NSString *const ARAugmentedRealityHasTriedToSetup = @"ARAugmentedRealityHasTriedToSetup"; NSString *const ARAugmentedRealityCameraAccessGiven = @"ARAugmentedRealityCameraAccessGiven"; @@ -25,8 +31,18 @@ + (void)resetDefaults // Need to save launch count for analytics NSInteger launchCount = [[NSUserDefaults standardUserDefaults] integerForKey:ARAnalyticsAppUsageCountProperty]; + // Preserve notification related settings + BOOL hasSeenNotificationPrompt = [[NSUserDefaults standardUserDefaults] boolForKey:ARPushNotificationsSettingsPromptSeen]; + BOOL hasSeenNotificationDialogue = [[NSUserDefaults standardUserDefaults] boolForKey:ARPushNotificationsAppleDialogueSeen]; + BOOL userPushNotificationDecision = [[NSUserDefaults standardUserDefaults] boolForKey:ARPushNotificationsAppleDialogueRejected]; + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; + [[NSUserDefaults standardUserDefaults] setInteger:launchCount forKey:ARAnalyticsAppUsageCountProperty]; + [[NSUserDefaults standardUserDefaults] setBool:hasSeenNotificationPrompt forKey:ARPushNotificationsSettingsPromptSeen]; + [[NSUserDefaults standardUserDefaults] setBool:hasSeenNotificationDialogue forKey:ARPushNotificationsAppleDialogueSeen]; + [[NSUserDefaults standardUserDefaults] setBool:userPushNotificationDecision forKey:ARPushNotificationsAppleDialogueRejected]; + [[NSUserDefaults standardUserDefaults] synchronize]; } @end diff --git a/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.h b/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.h index 2fcda4bd8cc..c33bdfbc806 100644 --- a/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.h +++ b/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.h @@ -4,8 +4,25 @@ typedef void(^ARNotificationReadStatusAssigner)(RCTResponseSenderBlock block); +typedef void(^ARNotificationPermissionsPrompter)(); + +typedef void(^ARRelativeURLResolver)(NSString *path, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); + + +/// While metaphysics is read-only, we need to rely on Eigen's +/// v1 API access to get/set these bits of information. + @interface ARTemporaryAPIModule : NSObject + +// Just shows the apple dialog, used for explicitly asking permission in settings +@property (nonatomic, copy, readwrite) ARNotificationPermissionsPrompter directNotificationPermissionPrompter; + +// Uses some logic to pre-prompt, redirect to settings, and eventually prompt with apple dialog, used on login +@property (nonatomic, copy, readwrite) ARNotificationPermissionsPrompter prepromptNotificationPermissionPrompter; + @property (nonatomic, copy, readwrite) ARNotificationReadStatusAssigner notificationReadStatusAssigner; +@property (nonatomic, copy, readwrite) ARRelativeURLResolver urlResolver; + @end diff --git a/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.m b/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.m index 234ec35998b..1b5a4f17ec4 100644 --- a/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.m +++ b/ios/Artsy/Emission/TemporaryAPI/ARTemporaryAPIModule.m @@ -1,16 +1,27 @@ #import "ARTemporaryAPIModule.h" #import #import "AREmission.h" -#import @implementation ARTemporaryAPIModule RCT_EXPORT_MODULE(); -RCT_EXPORT_METHOD(markUserPermissionStatus:(BOOL)granted) +RCT_EXPORT_METHOD(requestDirectNotificationPermissions) { - [[Appboy sharedInstance] pushAuthorizationFromUserNotificationCenter:granted]; + /* Used in settings screen to directly ask user for push permissions */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.directNotificationPermissionPrompter(); + }); +} + + +RCT_EXPORT_METHOD(requestPrepromptNotificationPermissions) +{ + /* Used on login with some additional logic before requesting permissions */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.prepromptNotificationPermissionPrompter(); + }); } RCT_EXPORT_METHOD(fetchNotificationPermissions:(RCTResponseSenderBlock)callback) diff --git a/ios/Artsy/View_Controllers/Admin/ARAdminSettingsViewController.m b/ios/Artsy/View_Controllers/Admin/ARAdminSettingsViewController.m index 2577de0509b..3e03833be5e 100644 --- a/ios/Artsy/View_Controllers/Admin/ARAdminSettingsViewController.m +++ b/ios/Artsy/View_Controllers/Admin/ARAdminSettingsViewController.m @@ -124,6 +124,13 @@ - (ARCellData *)generateNotificationTokenPasteboardCopy; [[UIPasteboard generalPasteboard] setValue:deviceToken forPasteboardType:(NSString *)kUTTypePlainText]; }]; } + +- (ARCellData *)requestNotificationsAlert; +{ + return [self tappableCellDataWithTitle:@"Request Receiving Notifications" selection:^{ + [[[ARAppNotificationsDelegate alloc] init] registerForDeviceNotificationsWithApple]; + }]; +} #endif diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0c6d97bfe6c..f69b1448bd3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -536,7 +536,7 @@ PODS: - React-Core - react-native-view-shot (3.4.0): - React-Core - - react-native-webview (11.22.7): + - react-native-webview (11.3.1): - React-Core - React-perflogger (0.69.12) - React-RCTActionSheet (0.69.12): @@ -648,8 +648,6 @@ PODS: - React-Core - RNLocalize (2.0.1): - React-Core - - RNPermissions (3.8.1): - - React-Core - RNReactNativeHapticFeedback (1.13.0): - React-Core - RNReanimated (2.13.0): @@ -858,7 +856,6 @@ DEPENDENCIES: - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNKeychain (from `../node_modules/react-native-keychain`) - RNLocalize (from `../node_modules/react-native-localize`) - - RNPermissions (from `../node_modules/react-native-permissions`) - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) @@ -1111,8 +1108,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-keychain" RNLocalize: :path: "../node_modules/react-native-localize" - RNPermissions: - :path: "../node_modules/react-native-permissions" RNReactNativeHapticFeedback: :path: "../node_modules/react-native-haptic-feedback" RNReanimated: @@ -1259,7 +1254,7 @@ SPEC CHECKSUMS: react-native-shake: 62aa5681863203090a087842da70183c442b97f8 react-native-slider: 241935e3ea8e47599c317f512f96ee8de607d4cb react-native-view-shot: a60a98a18c72bcaaaf2138f9aab960ae9b0d96c7 - react-native-webview: 227ba9205abb8579116b69ea5774d9744267c65a + react-native-webview: 07fca3f4378bd6ea26254bf63119bfd70f4fabeb React-perflogger: 5ade0a1627352f1647d283e78331819bb46cceae React-RCTActionSheet: 8e94f1e46e09c7035b81fe56c0ed8d78f3ccd340 React-RCTAnimation: bf2af72f03cf16528db9a830be69fa04b341a1b7 @@ -1287,7 +1282,6 @@ SPEC CHECKSUMS: RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94 RNLocalize: 41026b7c14878f1a1b381bc79f668f1fbf841adb - RNPermissions: 124173e975e06dea451f5f6ce18314a404428749 RNReactNativeHapticFeedback: b83bfb4b537bdd78eb4f6ffe63c6884f7b049ead RNReanimated: f0d66bda3d074c43c72a18f4fd4f4c26687bc1fd RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f diff --git a/package.json b/package.json index 95b3a8b8833..324006b4f61 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,6 @@ "node": "16.x", "yarn": "1.x" }, - "reactNativePermissionsIOS": [ - "Notifications" - ], "main": "index-common.js", "scripts": { "android": "react-native run-android", @@ -55,7 +52,7 @@ "pod-install": "cd ios; bundle exec pod install; cd ..; ./scripts/post-pod-install.rb", "pod-install-repo-update": "cd ios; bundle exec pod install --repo-update; cd ..; ./scripts/post-pod-install.rb", "postinit-metaflags": "rimraf storybook.json", - "postinstall": "react-native setup-ios-permissions; yarn init-metaflags; prettier --write package.json; ./scripts/update-echo", + "postinstall": "yarn init-metaflags; prettier --write package.json; ./scripts/update-echo", "prepare": "patch-package && husky install", "prestart": "./scripts/set-storybook-environment.js", "prestart-storybook": "yarn sb-rn-get-stories --config-path ./src/app/storybook", @@ -191,7 +188,6 @@ "react-native-linear-gradient": "2.6.2", "react-native-localize": "2.0.1", "react-native-pager-view": "6.2.0", - "react-native-permissions": "3.8.1", "react-native-push-notification": "8.1.1", "react-native-reanimated": "2.13.0", "react-native-reanimated-zoom": "0.3.3", @@ -205,7 +201,7 @@ "react-native-url-polyfill": "1.3.0", "react-native-view-shot": "3.4.0", "react-native-vimeo-iframe": "1.2.1", - "react-native-webview": "11.22.7", + "react-native-webview": "11.3.1", "react-relay": "14.1.0", "react-spring": "8.0.23", "react-tracking": "9.2.0", diff --git a/scripts/changelog/generateChangelog.ts b/scripts/changelog/generateChangelog.ts index 36df2e8c5cf..d73c196bd1c 100644 --- a/scripts/changelog/generateChangelog.ts +++ b/scripts/changelog/generateChangelog.ts @@ -136,4 +136,5 @@ async function main() { } } + main().catch((err) => console.error(err)) diff --git a/src/app/Components/ArtsyWebView.tests.tsx b/src/app/Components/ArtsyWebView.tests.tsx index e93dc311d1b..eac04b9e834 100644 --- a/src/app/Components/ArtsyWebView.tests.tsx +++ b/src/app/Components/ArtsyWebView.tests.tsx @@ -9,7 +9,6 @@ import { stringify } from "query-string" import Share from "react-native-share" import WebView, { WebViewProps } from "react-native-webview" import { WebViewNavigation } from "react-native-webview/lib/WebViewTypes" - import { _test_expandGoogleAdLink as expandGoogleAdLink, ArtsyWebView, @@ -155,13 +154,16 @@ describe("ArtsyWebViewPage", () => { describe("mimicBrowserBackButton", () => { it("lets our native back button control the browser", () => { - const mockSystemBackAction = jest.fn() - const tree = render({ systemBackAction: mockSystemBackAction }) + const tree = render() + const browserGoBack = jest + .spyOn(screen.UNSAFE_getByType(WebView).instance, "goBack") + .mockImplementation(() => undefined) fireEvent.press(screen.getByTestId("fancy-modal-header-left-button")) expect(goBack).toHaveBeenCalled() + expect(browserGoBack).not.toHaveBeenCalled() ;(goBack as any).mockReset() - mockSystemBackAction.mockReset() + ;(browserGoBack as any).mockReset() webViewProps(tree).onNavigationStateChange?.({ ...mockOnNavigationStateChange, @@ -169,13 +171,15 @@ describe("ArtsyWebViewPage", () => { }) fireEvent.press(screen.getByTestId("fancy-modal-header-left-button")) - expect(mockSystemBackAction).toHaveBeenCalled() + expect(browserGoBack).toHaveBeenCalled() expect(goBack).not.toHaveBeenCalled() }) it("can be overridden", () => { - const mockSystemBackAction = jest.fn() - const tree = render({ mimicBrowserBackButton: false, systemBackAction: mockSystemBackAction }) + const tree = render({ mimicBrowserBackButton: false }) + const browserGoBack = jest + .spyOn(screen.UNSAFE_getByType(WebView).instance, "goBack") + .mockImplementation(() => undefined) webViewProps(tree).onNavigationStateChange?.({ ...mockOnNavigationStateChange, @@ -183,7 +187,7 @@ describe("ArtsyWebViewPage", () => { }) fireEvent.press(screen.getByTestId("fancy-modal-header-left-button")) - expect(mockSystemBackAction).not.toHaveBeenCalled() + expect(browserGoBack).not.toHaveBeenCalled() expect(goBack).toHaveBeenCalled() }) }) diff --git a/src/app/Components/ArtsyWebView.tsx b/src/app/Components/ArtsyWebView.tsx index 335fa705cec..6d124d7d250 100644 --- a/src/app/Components/ArtsyWebView.tsx +++ b/src/app/Components/ArtsyWebView.tsx @@ -55,7 +55,6 @@ export const ArtsyWebViewPage = ({ mimicBrowserBackButton = true, useRightCloseButton = false, showShareButton = false, - systemBackAction, backProps, backAction, safeAreaEdges, @@ -63,7 +62,6 @@ export const ArtsyWebViewPage = ({ url: string isPresentedModally?: boolean backProps?: GoBackProps - systemBackAction?: () => void backAction?: () => void } & ArtsyWebViewConfig) => { const saInsets = useSafeAreaInsets() @@ -122,11 +120,7 @@ export const ArtsyWebViewPage = ({ } else if (!canGoBack) { handleGoBack() } else { - if (systemBackAction) { - systemBackAction() - } else { - ref.current?.goBack() - } + ref.current?.goBack() } } } diff --git a/src/app/NativeModules/LegacyNativeModules.tsx b/src/app/NativeModules/LegacyNativeModules.tsx index 5ff3986f273..197901d53ec 100644 --- a/src/app/NativeModules/LegacyNativeModules.tsx +++ b/src/app/NativeModules/LegacyNativeModules.tsx @@ -22,10 +22,11 @@ const noop: any = (name: string) => () => interface LegacyNativeModules { ARTemporaryAPIModule: { + requestPrepromptNotificationPermissions(): void + requestDirectNotificationPermissions(): void fetchNotificationPermissions( callback: (error: any, result: PushAuthorizationStatus) => void ): void - markUserPermissionStatus(granted: boolean): void markNotificationsRead(callback: (error?: Error) => any): void setApplicationIconBadgeNumber(n: number): void getUserEmail(): string @@ -127,8 +128,9 @@ const LegacyNativeModulesAndroid = { }, ARTemporaryAPIModule: { + requestPrepromptNotificationPermissions: noop("requestPrepromptNotificationPermissions"), + requestDirectNotificationPermissions: noop("requestDirectNotificationPermissions"), fetchNotificationPermissions: noop("fetchNotificationPermissions"), - markUserPermissionStatus: noop("markUserPermissionStatus"), markNotificationsRead: noop("markNotificationsRead"), setApplicationIconBadgeNumber: () => { console.log("TODO: make app icon badge work on android") diff --git a/src/app/Scenes/Home/Home.tsx b/src/app/Scenes/Home/Home.tsx index a317ae9c6a9..bb5f66ce4f0 100644 --- a/src/app/Scenes/Home/Home.tsx +++ b/src/app/Scenes/Home/Home.tsx @@ -67,7 +67,6 @@ import { useMemoizedRandom, } from "app/utils/placeholders" import { usePrefetch } from "app/utils/queryPrefetching" -import { requestPushNotificationsPermission } from "app/utils/requestPushNotificationsPermission" import { ArtworkActionTrackingProps, extractArtworkActionTrackingProps, @@ -155,10 +154,6 @@ const Home = memo((props: HomeProps) => { prefetchUrl("sales") }, []) - useEffect(() => { - requestPushNotificationsPermission() - }, []) - // we cannot rely on mount events for screens in tab views for screen tracking // because they can be mounted before the screen is visible // do custom screen view instead diff --git a/src/app/Scenes/Home/HomeContainer.tsx b/src/app/Scenes/Home/HomeContainer.tsx index 591067c26a3..c2b45a28537 100644 --- a/src/app/Scenes/Home/HomeContainer.tsx +++ b/src/app/Scenes/Home/HomeContainer.tsx @@ -1,11 +1,17 @@ import { HomeQueryRenderer } from "app/Scenes/Home/Home" import { GlobalStore } from "app/store/GlobalStore" import { navigate } from "app/system/navigation/navigate" +import { requestPushNotificationsPermission } from "app/utils/PushNotification" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { useEffect } from "react" export const HomeContainer = () => { const artQuizState = GlobalStore.useAppState((state) => state.auth.onboardingArtQuizState) + const onboardingState = GlobalStore.useAppState((state) => state.auth.onboardingState) + const hasRequestedPermissionsThisSession = GlobalStore.useAppState( + (state) => state.auth.requestedPushPermissionsThisSession + ) + const isNavigationReady = GlobalStore.useAppState((state) => state.sessionState.isNavigationReady) const shouldShowArtQuiz = useFeatureFlag("ARShowArtQuizApp") @@ -19,6 +25,14 @@ export const HomeContainer = () => { navigateToArtQuiz() return } + + if ( + !hasRequestedPermissionsThisSession && + (!onboardingState || onboardingState === "complete" || onboardingState === "none") + ) { + requestPushNotificationsPermission() + GlobalStore.actions.auth.setState({ requestedPushPermissionsThisSession: true }) + } }, [shouldShowArtQuiz, artQuizState, navigateToArtQuiz, isNavigationReady]) if (shouldShowArtQuiz && artQuizState === "incomplete") { diff --git a/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx b/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx index 209f40d0460..ca8ba72e327 100644 --- a/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx +++ b/src/app/Scenes/MyProfile/MyProfilePushNotifications.tsx @@ -3,6 +3,7 @@ import { MyProfilePushNotificationsQuery } from "__generated__/MyProfilePushNoti import { MyProfilePushNotifications_me$data } from "__generated__/MyProfilePushNotifications_me.graphql" import { PageWithSimpleHeader } from "app/Components/PageWithSimpleHeader" import { SwitchMenu } from "app/Components/SwitchMenu" +import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import { updateMyUserProfile } from "app/Scenes/MyAccount/updateMyUserProfile" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { @@ -10,7 +11,6 @@ import { PushAuthorizationStatus, } from "app/utils/PushNotification" 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" @@ -83,7 +83,7 @@ export const AllowPushNotificationsBanner = () => (