From 9551ea6fa87eb7a365c2cef705bf8509c680c91b Mon Sep 17 00:00:00 2001 From: Hugo EXTRAT Date: Wed, 12 Mar 2025 16:32:39 +0100 Subject: [PATCH] feat: use `RequestReviewAction` API on iOS 16+ (#158) * feat: use `RequestReviewAction` API on iOS 16+ * update readme --- README.md | 5 +++- RateApp.podspec | 2 +- example/ios/Podfile.lock | 4 +-- example/src/App.tsx | 1 + ios/RateApp-Bridging-Header.h | 1 + ios/RateApp.mm | 52 ++++------------------------------- ios/RateApp.swift | 50 +++++++++++++++++++++++++++++++++ 7 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 ios/RateApp-Bridging-Header.h create mode 100644 ios/RateApp.swift diff --git a/README.md b/README.md index 08ffce0..ef40859 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,12 @@ To enable in-app reviews for the Samsung Galaxy Store, ensure that your app is c - **Development Mode**: The in-app review dialog is always displayed in development mode, regardless of rate limits. This is useful for testing purposes. - **Best Practices**: According to operating system guidelines, it is recommended to request a review during a natural flow in your app, rather than from a button. For example, you might request a review after a user has completed a task or achieved a milestone within your app. -[Apple Documentation](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview#discussion)
+[Apple Documentation iOS 14+](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview#discussion)
+[Apple Documentation iOS 16+](https://developer.apple.com/documentation/storekit/requestreviewaction)
[Google Documentation](https://developer.android.com/guide/playcore/in-app-review) +Below iOS 16 library is using the deprecated SKStoreReviewController and on iOS 16+ it uses the new RequestReviewAction API. Both implementations provide the same functionality and user experience. + ### `RateApp.openStoreForReview(options)` Opens the app store page for the app, allowing the user to leave a review. diff --git a/RateApp.podspec b/RateApp.podspec index 68bea15..4f55ced 100644 --- a/RateApp.podspec +++ b/RateApp.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/huextrat/react-native-rate-app.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,cpp}" + s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" s.private_header_files = "ios/generated/**/*.h" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3b8d2ae..53f25eb 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.78.0): - hermes-engine/Pre-built (= 0.78.0) - hermes-engine/Pre-built (0.78.0) - - RateApp (1.2.2): + - RateApp (1.2.3): - DoubleConversion - glog - hermes-engine @@ -1769,7 +1769,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 hermes-engine: b417d2b2aee3b89b58e63e23a51e02be91dc876d - RateApp: 4ae46fe22bc16dfa26d931ce1cb4968185eaa9d3 + RateApp: d3cbe9933fd0ea0ce2b23522293bd99a85acf92b RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 RCTDeprecation: b2eecf2d60216df56bc5e6be5f063826d3c1ee35 RCTRequired: 78522de7dc73b81f3ed7890d145fa341f5bb32ea diff --git a/example/src/App.tsx b/example/src/App.tsx index 7597175..003983b 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -33,5 +33,6 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", gap: 8, + backgroundColor: "white", }, }); diff --git a/ios/RateApp-Bridging-Header.h b/ios/RateApp-Bridging-Header.h new file mode 100644 index 0000000..5a4bae0 --- /dev/null +++ b/ios/RateApp-Bridging-Header.h @@ -0,0 +1 @@ +#import \ No newline at end of file diff --git a/ios/RateApp.mm b/ios/RateApp.mm index 2ea1dd4..f941d0b 100644 --- a/ios/RateApp.mm +++ b/ios/RateApp.mm @@ -1,55 +1,15 @@ -#import "RateApp.h" -#import +#import -static NSString *const kNoActiveSceneError = @"no_active_scene"; -static NSString *const kUnsupportedPlatformError = @"unsupported_platform"; +@interface RCT_EXTERN_MODULE(RateApp, NSObject) -@implementation RateApp -RCT_EXPORT_MODULE() - -RCT_EXPORT_METHOD(requestReview:(RCTPromiseResolveBlock)resolve +RCT_EXTERN_METHOD(requestReview:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -{ - UIWindowScene *scene = [self findActiveScene]; - if (scene) { - [SKStoreReviewController requestReviewInScene:scene]; - resolve(@(YES)); - } else { - reject(kNoActiveSceneError, @"No active scene found", nil); - } -} -RCT_EXPORT_METHOD(requestReviewAppGallery:(RCTPromiseResolveBlock)resolve +RCT_EXTERN_METHOD(requestReviewAppGallery:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -{ - // Since this is iOS, we should reject with an appropriate message - reject(kUnsupportedPlatformError, @"App Gallery reviews are not supported on iOS", nil); -} -RCT_EXPORT_METHOD(requestReviewGalaxyStore:(NSString *)packageName +RCT_EXTERN_METHOD(requestReviewGalaxyStore:(NSString *)packageName resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -{ - // Since this is iOS, we should reject with an appropriate message - reject(kUnsupportedPlatformError, @"Galaxy Store reviews are not supported on iOS", nil); -} -- (UIWindowScene *) findActiveScene { - for (UIWindowScene *scene in UIApplication.sharedApplication.connectedScenes) { - if (scene.activationState == UISceneActivationStateForegroundActive) { - return scene; - } - } - return nil; -} - -// Don't compile this code when we build for the old architecture. -#ifdef RCT_NEW_ARCH_ENABLED -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); -} -#endif - -@end +@end \ No newline at end of file diff --git a/ios/RateApp.swift b/ios/RateApp.swift new file mode 100644 index 0000000..cb9363f --- /dev/null +++ b/ios/RateApp.swift @@ -0,0 +1,50 @@ +import Foundation +import StoreKit + +@objc(RateApp) +class RateApp: NSObject { + + private let noActiveSceneError = "no_active_scene" + private let unsupportedPlatformError = "unsupported_platform" + + @objc + static func requiresMainQueueSetup() -> Bool { + return false + } + + @objc + func requestReview(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + if let scene = findActiveScene() { + if #available(iOS 16.0, *) { + Task { @MainActor in + AppStore.requestReview(in: scene) + } + } else { + SKStoreReviewController.requestReview(in: scene) + } + resolve(true) + } else { + reject(noActiveSceneError, "No active scene found", nil) + } + } + + @objc + func requestReviewAppGallery(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + reject(unsupportedPlatformError, "App Gallery reviews are not supported on iOS", nil) + } + + @objc + func requestReviewGalaxyStore(_ packageName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + reject(unsupportedPlatformError, "Galaxy Store reviews are not supported on iOS", nil) + } + + private func findActiveScene() -> UIWindowScene? { + for scene in UIApplication.shared.connectedScenes { + if let windowScene = scene as? UIWindowScene, + windowScene.activationState == .foregroundActive { + return windowScene + } + } + return nil + } +} \ No newline at end of file