@kontextso/sdk-react-native 3.1.0-rc.4 → 3.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,4 +65,36 @@ public class KontextSDK: NSObject {
65
65
  resolve(ok)
66
66
  }
67
67
  }
68
+
69
+ // MARK: - SKStoreProductViewController
70
+
71
+ @objc
72
+ public static func presentSKStoreProduct(
73
+ _ appStoreId: String,
74
+ resolver resolve: @escaping RCTPromiseResolveBlock,
75
+ rejecter reject: @escaping RCTPromiseRejectBlock
76
+ ) {
77
+ DispatchQueue.main.async {
78
+ SKStoreProductManager.shared.present(appStoreId: appStoreId) { ok, error in
79
+ if ok {
80
+ resolve(true)
81
+ } else if let error = error {
82
+ reject("SK_STORE_PRODUCT_ERROR", error.localizedDescription, error)
83
+ } else {
84
+ resolve(false)
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ @objc
91
+ public static func dismissSKStoreProduct(
92
+ _ resolve: @escaping RCTPromiseResolveBlock,
93
+ rejecter reject: @escaping RCTPromiseRejectBlock
94
+ ) {
95
+ DispatchQueue.main.async {
96
+ let ok = SKStoreProductManager.shared.dismiss()
97
+ resolve(ok)
98
+ }
99
+ }
68
100
  }
package/ios/RNKontext.mm CHANGED
@@ -17,7 +17,6 @@ RCT_EXPORT_MODULE()
17
17
  }
18
18
  }
19
19
 
20
- // NEW: Present SKOverlay
21
20
  - (void)presentSKOverlay:(NSString *)appStoreId
22
21
  position:(NSString *)position
23
22
  dismissible:(BOOL)dismissible
@@ -30,12 +29,22 @@ RCT_EXPORT_MODULE()
30
29
  rejecter:reject];
31
30
  }
32
31
 
33
- // NEW: Dismiss SKOverlay
34
32
  - (void)dismissSKOverlay:(RCTPromiseResolveBlock)resolve
35
33
  reject:(RCTPromiseRejectBlock)reject {
36
34
  [KontextSDK dismissSKOverlay:resolve rejecter:reject];
37
35
  }
38
36
 
37
+ - (void)presentSKStoreProduct:(NSString *)appStoreId
38
+ resolve:(RCTPromiseResolveBlock)resolve
39
+ reject:(RCTPromiseRejectBlock)reject {
40
+ [KontextSDK presentSKStoreProduct:appStoreId resolver:resolve rejecter:reject];
41
+ }
42
+
43
+ - (void)dismissSKStoreProduct:(RCTPromiseResolveBlock)resolve
44
+ reject:(RCTPromiseRejectBlock)reject {
45
+ [KontextSDK dismissSKStoreProduct:resolve rejecter:reject];
46
+ }
47
+
39
48
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
40
49
  (const facebook::react::ObjCTurboModule::InitParams &)params {
41
50
  return std::make_shared<facebook::react::NativeRNKontextSpecJSI>(params);
@@ -54,7 +63,6 @@ RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)resolve
54
63
  }
55
64
  }
56
65
 
57
- // NEW: Present SKOverlay
58
66
  RCT_EXPORT_METHOD(presentSKOverlay:(NSString *)appStoreId
59
67
  position:(NSString *)position
60
68
  dismissible:(BOOL)dismissible
@@ -67,12 +75,22 @@ RCT_EXPORT_METHOD(presentSKOverlay:(NSString *)appStoreId
67
75
  rejecter:reject];
68
76
  }
69
77
 
70
- // NEW: Dismiss SKOverlay
71
78
  RCT_EXPORT_METHOD(dismissSKOverlay:(RCTPromiseResolveBlock)resolve
72
79
  rejecter:(RCTPromiseRejectBlock)reject) {
73
80
  [KontextSDK dismissSKOverlay:resolve rejecter:reject];
74
81
  }
75
82
 
83
+ RCT_EXPORT_METHOD(presentSKStoreProduct:(NSString *)appStoreId
84
+ resolver:(RCTPromiseResolveBlock)resolve
85
+ rejecter:(RCTPromiseRejectBlock)reject) {
86
+ [KontextSDK presentSKStoreProduct:appStoreId resolver:resolve rejecter:reject];
87
+ }
88
+
89
+ RCT_EXPORT_METHOD(dismissSKStoreProduct:(RCTPromiseResolveBlock)resolve
90
+ rejecter:(RCTPromiseRejectBlock)reject) {
91
+ [KontextSDK dismissSKStoreProduct:resolve rejecter:reject];
92
+ }
93
+
76
94
  #endif
77
95
 
78
96
  @end
@@ -0,0 +1,119 @@
1
+ import Foundation
2
+ import StoreKit
3
+ import UIKit
4
+
5
+ final class SKStoreProductManager: NSObject, SKStoreProductViewControllerDelegate {
6
+ static let shared = SKStoreProductManager()
7
+
8
+ private weak var presentedViewController: SKStoreProductViewController?
9
+
10
+ func present(appStoreId: String, completion: @escaping (Bool, NSError?) -> Void) {
11
+ guard let itemId = Int(appStoreId) else {
12
+ completion(false, NSError(
13
+ domain: "SKStoreProduct",
14
+ code: 1,
15
+ userInfo: [NSLocalizedDescriptionKey: "appStoreId must be a valid integer string"]
16
+ ))
17
+ return
18
+ }
19
+
20
+ let params: [String: Any] = [
21
+ SKStoreProductParameterITunesItemIdentifier: NSNumber(value: itemId)
22
+ ]
23
+
24
+ let viewController = SKStoreProductViewController()
25
+ viewController.delegate = self
26
+
27
+ viewController.loadProduct(withParameters: params) { [weak self] loaded, error in
28
+ guard let self = self else { return }
29
+
30
+ DispatchQueue.main.async {
31
+ guard loaded else {
32
+ completion(false, NSError(
33
+ domain: "SKStoreProduct",
34
+ code: 2,
35
+ userInfo: [NSLocalizedDescriptionKey: "Failed to load product"]
36
+ ))
37
+ return
38
+ }
39
+
40
+ guard let top = self.topViewController() else {
41
+ completion(false, NSError(
42
+ domain: "SKStoreProduct",
43
+ code: 3,
44
+ userInfo: [NSLocalizedDescriptionKey: "No top view controller found"]
45
+ ))
46
+ return
47
+ }
48
+
49
+ _ = self.dismiss()
50
+ top.present(viewController, animated: true)
51
+ self.presentedViewController = viewController
52
+ completion(true, nil)
53
+ }
54
+ }
55
+ }
56
+
57
+ @discardableResult
58
+ func dismiss() -> Bool {
59
+ var dismissed = false
60
+
61
+ let run: () -> Void = { [weak self] in
62
+ guard let self = self else { return }
63
+
64
+ if let viewController = self.presentedViewController {
65
+ viewController.dismiss(animated: true) { [weak self] in
66
+ self?.presentedViewController = nil
67
+ }
68
+ dismissed = true
69
+ return
70
+ }
71
+
72
+ if let top = self.topViewController(),
73
+ let storeViewController = top.presentedViewController as? SKStoreProductViewController {
74
+ storeViewController.dismiss(animated: true) { [weak self] in
75
+ self?.presentedViewController = nil
76
+ }
77
+ dismissed = true
78
+ }
79
+ }
80
+
81
+ if Thread.isMainThread {
82
+ run()
83
+ } else {
84
+ DispatchQueue.main.sync { run() }
85
+ }
86
+
87
+ return dismissed
88
+ }
89
+
90
+ func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
91
+ _ = dismiss()
92
+ }
93
+
94
+ private func topViewController(base: UIViewController? = nil) -> UIViewController? {
95
+ let seed: UIViewController? = base ?? {
96
+ if #available(iOS 13.0, *) {
97
+ let scene = UIApplication.shared.connectedScenes
98
+ .compactMap { $0 as? UIWindowScene }
99
+ .first { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }
100
+ return scene?.windows.first(where: { $0.isKeyWindow })?.rootViewController
101
+ } else {
102
+ return UIApplication.shared.keyWindow?.rootViewController
103
+ }
104
+ }()
105
+
106
+ guard let seed = seed else { return nil }
107
+
108
+ if let nav = seed as? UINavigationController {
109
+ return topViewController(base: nav.visibleViewController)
110
+ }
111
+ if let tab = seed as? UITabBarController {
112
+ return topViewController(base: tab.selectedViewController)
113
+ }
114
+ if let presented = seed.presentedViewController {
115
+ return topViewController(base: presented)
116
+ }
117
+ return seed
118
+ }
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "3.1.0-rc.4",
3
+ "version": "3.2.0-rc.0",
4
4
  "description": "Kontext SDK for React Native",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -5,6 +5,8 @@ export interface Spec extends TurboModule {
5
5
  isSoundOn(): Promise<boolean>
6
6
  presentSKOverlay(appStoreId: string, position: string, dismissible: boolean): Promise<boolean>
7
7
  dismissSKOverlay(): Promise<boolean>
8
+ presentSKStoreProduct(appStoreId: string): Promise<boolean>
9
+ dismissSKStoreProduct(): Promise<boolean>
8
10
  }
9
11
 
10
12
  export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
@@ -1,12 +1,9 @@
1
1
  import { Platform } from 'react-native'
2
2
  import NativeRNKontext from '../NativeRNKontext'
3
+ import { isValidAppStoreId } from './utils'
3
4
 
4
5
  export type SKOverlayPosition = 'bottom' | 'bottomRaised'
5
6
 
6
- const isValidAppStoreId = (id: unknown): id is string => {
7
- return typeof id === 'string' && /^\d+$/.test(id)
8
- }
9
-
10
7
  const isValidPosition = (p: unknown): p is SKOverlayPosition => {
11
8
  return p === 'bottom' || p === 'bottomRaised'
12
9
  }
@@ -0,0 +1,20 @@
1
+ import { Platform } from 'react-native'
2
+ import NativeRNKontext from '../NativeRNKontext'
3
+ import { isValidAppStoreId } from './utils'
4
+
5
+ export async function presentSKStoreProduct(appStoreId: string) {
6
+ if (Platform.OS !== 'ios') {
7
+ return false
8
+ }
9
+ if (!isValidAppStoreId(appStoreId)) {
10
+ return false
11
+ }
12
+ return NativeRNKontext.presentSKStoreProduct(appStoreId)
13
+ }
14
+
15
+ export async function dismissSKStoreProduct() {
16
+ if (Platform.OS !== 'ios') {
17
+ return false
18
+ }
19
+ return NativeRNKontext.dismissSKStoreProduct()
20
+ }
@@ -0,0 +1,4 @@
1
+
2
+ export const isValidAppStoreId = (id: unknown): id is string => {
3
+ return typeof id === 'string' && /^\d+$/.test(id)
4
+ }