Skip to content

Commit adf4f63

Browse files
authored
Merge pull request #395 from qonversion/kamo/dev-275-noCodesInAnalytics
2 parents 9b6d149 + e7b4bf4 commit adf4f63

12 files changed

Lines changed: 259 additions & 9 deletions

File tree

android/src/main/java/com/qonversion/reactnativesdk/NoCodesModule.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.facebook.react.bridge.WritableNativeMap
1010
import io.qonversion.sandwich.BridgeData
1111
import io.qonversion.sandwich.NoCodesSandwich
1212
import io.qonversion.sandwich.NoCodesEventListener
13+
import io.qonversion.sandwich.NoCodesPurchaseDelegateBridge
1314

1415
@ReactModule(name = NoCodesModule.NAME)
1516
class NoCodesModule(private val reactContext: ReactApplicationContext) : NativeNoCodesModuleSpec(reactContext) {
@@ -26,6 +27,16 @@ class NoCodesModule(private val reactContext: ReactApplicationContext) : NativeN
2627
emitOnNoCodeEvent(eventMap)
2728
}
2829
}
30+
private val purchaseDelegate: NoCodesPurchaseDelegateBridge = object : NoCodesPurchaseDelegateBridge {
31+
override fun purchase(product: BridgeData) {
32+
val productMap = EntitiesConverter.convertMapToWritableMap(product)
33+
emitOnNoCodePurchase(productMap)
34+
}
35+
36+
override fun restore() {
37+
emitOnNoCodeRestore()
38+
}
39+
}
2940

3041
override fun getName(): String {
3142
return NAME
@@ -70,6 +81,31 @@ class NoCodesModule(private val reactContext: ReactApplicationContext) : NativeN
7081
}
7182
}
7283

84+
@ReactMethod
85+
override fun setPurchaseDelegate() {
86+
noCodesSandwich.setPurchaseDelegate(purchaseDelegate)
87+
}
88+
89+
@ReactMethod
90+
override fun delegatedPurchaseCompleted() {
91+
noCodesSandwich.delegatedPurchaseCompleted()
92+
}
93+
94+
@ReactMethod
95+
override fun delegatedPurchaseFailed(errorMessage: String) {
96+
noCodesSandwich.delegatedPurchaseFailed(errorMessage)
97+
}
98+
99+
@ReactMethod
100+
override fun delegatedRestoreCompleted() {
101+
noCodesSandwich.delegatedRestoreCompleted()
102+
}
103+
104+
@ReactMethod
105+
override fun delegatedRestoreFailed(errorMessage: String) {
106+
noCodesSandwich.delegatedRestoreFailed(errorMessage)
107+
}
108+
73109
companion object {
74110
const val NAME = "RNNoCodes"
75111
}

example/ios/Podfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ if linkage != nil
1616
use_frameworks! :linkage => linkage.to_sym
1717
end
1818

19+
# For testing Sandwich SDK locally, uncomment the following line
20+
# pod 'QonversionSandwich', :path => '../../../sandwich-sdk'
21+
1922
target 'QonversionExample' do
2023
config = use_native_modules!
2124

example/ios/Podfile.lock

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,6 +2200,7 @@ DEPENDENCIES:
22002200
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
22012201
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
22022202
- qonversion-react-native-sdk (from `../..`)
2203+
- QonversionSandwich (from `../../../sandwich-sdk`)
22032204
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
22042205
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
22052206
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
@@ -2273,7 +2274,6 @@ DEPENDENCIES:
22732274
SPEC REPOS:
22742275
trunk:
22752276
- Qonversion
2276-
- QonversionSandwich
22772277
- SocketRocket
22782278

22792279
EXTERNAL SOURCES:
@@ -2294,6 +2294,8 @@ EXTERNAL SOURCES:
22942294
:tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca
22952295
qonversion-react-native-sdk:
22962296
:path: "../.."
2297+
QonversionSandwich:
2298+
:path: "../../../sandwich-sdk"
22972299
RCT-Folly:
22982300
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
22992301
RCTDeprecation:
@@ -2509,6 +2511,6 @@ SPEC CHECKSUMS:
25092511
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
25102512
Yoga: 703055a9f39562521cdb8657162dfd80f8c174c3
25112513

2512-
PODFILE CHECKSUM: 05782888d88e3a4a4bbc945c42df54704269e6ea
2514+
PODFILE CHECKSUM: f56bbcfb3ee0fbed79b9ce0f06983ea8ea7533f8
25132515

25142516
COCOAPODS: 1.16.2

example/src/screens/NoCodesScreen/index.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import {
88
TextInput,
99
Platform,
1010
} from 'react-native';
11-
import {
11+
import Qonversion, {
1212
NoCodesAction,
1313
NoCodesConfigBuilder,
1414
ScreenPresentationStyle,
1515
ScreenPresentationConfig,
1616
NoCodes,
17+
NoCodesError,
18+
type PurchaseDelegate,
19+
Product
1720
} from '@qonversion/react-native-sdk';
18-
import type NoCodesError from '../../../../src/dto/NoCodesError';
1921
import { AppContext } from '../../store/AppStore';
2022
import styles from './styles';
2123
import Snackbar from 'react-native-snackbar';
@@ -37,6 +39,42 @@ const NoCodesScreen: React.FC = () => {
3739
// Initialize No-Codes SDK once
3840
const initializeNoCodes = () => {
3941
console.log('🔄 [NoCodes] Starting SDK initialization...');
42+
// @ts-ignore - PurchaseDelegate is not used until the comment below is uncommented
43+
const purchaseDelegate: PurchaseDelegate = {
44+
purchase: async (product: Product) => {
45+
console.log('🔄 [PurchaseDelegate] Starting purchase for product:', product.qonversionId);
46+
try {
47+
const entitlements = await Qonversion.getSharedInstance().purchaseProduct(product);
48+
console.log('✅ [PurchaseDelegate] Purchase successful:', Object.fromEntries(entitlements));
49+
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase completed: ${product.qonversionId}` });
50+
Snackbar.show({
51+
text: `Purchase completed: ${product.qonversionId}`,
52+
duration: Snackbar.LENGTH_SHORT,
53+
});
54+
} catch (error: any) {
55+
console.error('❌ [PurchaseDelegate] Purchase failed:', error);
56+
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase failed: ${error.message}` });
57+
throw error; // Re-throw to let NoCodes SDK handle the error
58+
}
59+
},
60+
restore: async () => {
61+
console.log('🔄 [PurchaseDelegate] Starting restore...');
62+
try {
63+
const entitlements = await Qonversion.getSharedInstance().restore();
64+
console.log('✅ [PurchaseDelegate] Restore successful:', Object.fromEntries(entitlements));
65+
dispatch({ type: 'ADD_NOCODES_EVENT', payload: 'Restore completed' });
66+
Snackbar.show({
67+
text: 'Restore completed successfully!',
68+
duration: Snackbar.LENGTH_SHORT,
69+
});
70+
} catch (error: any) {
71+
console.error('❌ [PurchaseDelegate] Restore failed:', error);
72+
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Restore failed: ${error.message}` });
73+
throw error; // Re-throw to let NoCodes SDK handle the error
74+
}
75+
},
76+
};
77+
4078
const noCodesConfig = new NoCodesConfigBuilder(ProjectKey)
4179
.setNoCodesListener({
4280
onScreenShown: (id: string) => {
@@ -71,6 +109,7 @@ const NoCodesScreen: React.FC = () => {
71109
NoCodes.getSharedInstance().close();
72110
},
73111
})
112+
// .setPurchaseDelegate(purchaseDelegate) // Uncomment this to use the purchase delegate
74113
.build();
75114
console.log('✅ [NoCodes] Config built successfully:', noCodesConfig);
76115

ios/RNNoCodes.mm

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#import "qonversion_react_native_sdk-Swift.h"
66
#endif
77

8-
@interface RNNoCodes () <NoCodesEventDelegate>
8+
@interface RNNoCodes () <NoCodesEventDelegate, NoCodesPurchaseDelegateProxy>
99

1010
@property (nonatomic, strong) RNNoCodesImpl *impl;
1111

@@ -51,8 +51,40 @@ - (void)close:(RCTPromiseResolveBlock)resolve
5151
});
5252
}
5353

54+
- (void)setPurchaseDelegate {
55+
[self.impl setPurchaseDelegate:self];
56+
}
57+
58+
- (void)delegatedPurchaseCompleted {
59+
[self.impl delegatedPurchaseCompleted];
60+
}
61+
62+
- (void)delegatedPurchaseFailed:(NSString *)errorMessage {
63+
[self.impl delegatedPurchaseFailed:errorMessage];
64+
}
65+
66+
- (void)delegatedRestoreCompleted {
67+
[self.impl delegatedRestoreCompleted];
68+
}
69+
70+
- (void)delegatedRestoreFailed:(NSString *)errorMessage {
71+
[self.impl delegatedRestoreFailed:errorMessage];
72+
}
73+
74+
#pragma mark - NoCodesEventDelegate
75+
5476
- (void)noCodesDidTriggerWithEvent:(NSString * _Nonnull)event payload:(NSDictionary<NSString *,id> * _Nullable)payload {
55-
[self emitOnNoCodeEvent:@{@"name": event, @"payload": payload ?: [NSNull null]}];
77+
[self emitOnNoCodeEvent:@{@"name": event, @"payload": payload ?: [NSNull null]}];
78+
}
79+
80+
#pragma mark - NoCodesPurchaseDelegateProxy
81+
82+
- (void)purchase:(NSDictionary *)product {
83+
[self emitOnNoCodePurchase:product];
84+
}
85+
86+
- (void)restore {
87+
[self emitOnNoCodeRestore];
5688
}
5789

5890
#pragma mark - TurboModule

ios/RNNoCodesImpl.swift

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,34 @@ class NoCodesEventHandler: NoCodesEventListener {
2626
}
2727
}
2828

29+
@objc
30+
public protocol NoCodesPurchaseDelegateProxy {
31+
func purchase(_ product: [String: Any])
32+
func restore()
33+
}
34+
35+
class NoCodesPurchaseDelegateHandler: NoCodesPurchaseDelegateBridge {
36+
private let delegate: NoCodesPurchaseDelegateProxy
37+
38+
init(delegate: NoCodesPurchaseDelegateProxy) {
39+
self.delegate = delegate
40+
}
41+
42+
func purchase(_ product: [String: Any]) {
43+
delegate.purchase(product)
44+
}
45+
46+
func restore() {
47+
delegate.restore()
48+
}
49+
}
50+
2951
@objc
3052
public class RNNoCodesImpl: NSObject {
3153

3254
var noCodesSandwich: NoCodesSandwich?
3355
var eventHandler: NoCodesEventHandler
56+
var purchaseDelegateHandler: NoCodesPurchaseDelegateHandler?
3457

3558
@objc
3659
public override init() {
@@ -63,7 +86,34 @@ public class RNNoCodesImpl: NSObject {
6386
}
6487

6588
@objc
66-
public func setDelegate(_ delegate: NoCodesEventDelegate?) {
89+
public func setDelegate(_ delegate: NoCodesEventDelegate) {
6790
eventHandler.delegate = delegate
6891
}
92+
93+
@objc
94+
public func setPurchaseDelegate(_ delegate: NoCodesPurchaseDelegateProxy) {
95+
let delegateHandler = NoCodesPurchaseDelegateHandler(delegate: delegate)
96+
purchaseDelegateHandler = delegateHandler
97+
noCodesSandwich?.setPurchaseDelegate(delegateHandler)
98+
}
99+
100+
@objc
101+
public func delegatedPurchaseCompleted() {
102+
noCodesSandwich?.delegatedPurchaseCompleted()
103+
}
104+
105+
@objc
106+
public func delegatedPurchaseFailed(_ errorMessage: String) {
107+
noCodesSandwich?.delegatedPurchaseFailed(errorMessage)
108+
}
109+
110+
@objc
111+
public func delegatedRestoreCompleted() {
112+
noCodesSandwich?.delegatedRestoreCompleted()
113+
}
114+
115+
@objc
116+
public func delegatedRestoreFailed(_ errorMessage: String) {
117+
noCodesSandwich?.delegatedRestoreFailed(errorMessage)
118+
}
69119
}

src/NoCodesConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import type { NoCodesListener } from './dto/NoCodesListener';
2+
import type { PurchaseDelegate } from './dto/PurchaseDelegate';
23

34
class NoCodesConfig {
45
readonly projectKey: string;
56
readonly noCodesListener: NoCodesListener | undefined;
7+
readonly purchaseDelegate: PurchaseDelegate | undefined;
68
readonly proxyUrl: string | undefined;
79

810
constructor(
911
projectKey: string,
1012
noCodesListener: NoCodesListener | undefined = undefined,
13+
purchaseDelegate: PurchaseDelegate | undefined = undefined,
1114
proxyUrl: string | undefined = undefined
1215
) {
1316
this.projectKey = projectKey;
1417
this.noCodesListener = noCodesListener;
18+
this.purchaseDelegate = purchaseDelegate;
1519
this.proxyUrl = proxyUrl;
1620
}
1721
}

src/NoCodesConfigBuilder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type {NoCodesListener} from './dto/NoCodesListener';
2+
import type {PurchaseDelegate} from './dto/PurchaseDelegate';
23
import NoCodesConfig from './NoCodesConfig';
34

45
class NoCodesConfigBuilder {
56
private readonly projectKey: string;
67
private noCodesListener: NoCodesListener | undefined = undefined;
8+
private purchaseDelegate: PurchaseDelegate | undefined = undefined;
79
private proxyUrl: string | undefined = undefined;
810

911
constructor(projectKey: string) {
@@ -25,6 +27,18 @@ class NoCodesConfigBuilder {
2527
return this;
2628
}
2729

30+
/**
31+
* Provide a delegate for custom purchase and restore handling.
32+
* When this delegate is provided, it replaces the default Qonversion SDK purchase flow.
33+
*
34+
* @param purchaseDelegate delegate to handle purchase and restore operations.
35+
* @return builder instance for chain calls.
36+
*/
37+
setPurchaseDelegate(purchaseDelegate: PurchaseDelegate): NoCodesConfigBuilder {
38+
this.purchaseDelegate = purchaseDelegate;
39+
return this;
40+
}
41+
2842
/**
2943
* Set proxy URL for NoCodes SDK.
3044
*
@@ -45,6 +59,7 @@ class NoCodesConfigBuilder {
4559
return new NoCodesConfig(
4660
this.projectKey,
4761
this.noCodesListener,
62+
this.purchaseDelegate,
4863
this.proxyUrl
4964
);
5065
}

src/dto/PurchaseDelegate.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Product from './Product';
2+
3+
/**
4+
* Delegate responsible for custom purchase and restore handling.
5+
* When this delegate is provided, it replaces the default Qonversion SDK purchase flow.
6+
*/
7+
export interface PurchaseDelegate {
8+
/**
9+
* Handle purchase for the given product.
10+
* @param product Product to purchase.
11+
* @returns Promise that completes successfully when purchase finishes, otherwise throws an error.
12+
*/
13+
purchase(product: Product): Promise<void>;
14+
15+
/**
16+
* Handle restore flow.
17+
* @returns Promise that completes successfully when restore finishes, otherwise throws an error.
18+
*/
19+
restore(): Promise<void>;
20+
}
21+

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ export type { default as NoCodesApi } from './NoCodesApi';
3939
export { default as NoCodesConfig } from './NoCodesConfig';
4040
export { default as NoCodesConfigBuilder } from './NoCodesConfigBuilder';
4141
export type { NoCodesListener } from './dto/NoCodesListener';
42+
export type { PurchaseDelegate } from './dto/PurchaseDelegate';
4243
export { default as NoCodesAction } from './dto/NoCodesAction';
44+
export { default as NoCodesError } from './dto/NoCodesError';

0 commit comments

Comments
 (0)