Skip to content

Commit bee7168

Browse files
authored
Merge pull request #423 from qonversion/kamo/dev-352-no-codes-in-analytics-mode
2 parents 28988cc + a52af46 commit bee7168

12 files changed

Lines changed: 555 additions & 8 deletions

File tree

android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/NoCodesPlugin.kt

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import io.flutter.plugin.common.BinaryMessenger
55
import io.flutter.plugin.common.MethodChannel.Result
66
import io.qonversion.sandwich.BridgeData
77
import io.qonversion.sandwich.NoCodesEventListener
8+
import io.qonversion.sandwich.NoCodesPurchaseDelegateBridge
89
import io.qonversion.sandwich.NoCodesSandwich
910
import com.google.gson.Gson
1011

11-
class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: Context) : NoCodesEventListener {
12+
class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: Context) : NoCodesEventListener, NoCodesPurchaseDelegateBridge {
1213
private var noCodesSandwich: NoCodesSandwich? = null
1314
private val gson = Gson()
1415

@@ -19,6 +20,10 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context:
1920
private var actionFailedEventStreamHandler: BaseEventStreamHandler? = null
2021
private var actionFinishedEventStreamHandler: BaseEventStreamHandler? = null
2122
private var screenFailedToLoadEventStreamHandler: BaseEventStreamHandler? = null
23+
24+
// Purchase delegate event stream handlers
25+
private var purchaseEventStreamHandler: BaseEventStreamHandler? = null
26+
private var restoreEventStreamHandler: BaseEventStreamHandler? = null
2227

2328
companion object {
2429
private const val SCREEN_SHOWN_EVENT_CHANNEL = "nocodes_screen_shown"
@@ -27,6 +32,8 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context:
2732
private const val ACTION_FAILED_EVENT_CHANNEL = "nocodes_action_failed"
2833
private const val ACTION_FINISHED_EVENT_CHANNEL = "nocodes_action_finished"
2934
private const val SCREEN_FAILED_TO_LOAD_EVENT_CHANNEL = "nocodes_screen_failed_to_load"
35+
private const val PURCHASE_EVENT_CHANNEL = "nocodes_purchase"
36+
private const val RESTORE_EVENT_CHANNEL = "nocodes_restore"
3037
}
3138

3239
init {
@@ -58,6 +65,15 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context:
5865
val screenFailedToLoadListener = BaseListenerWrapper(messenger, SCREEN_FAILED_TO_LOAD_EVENT_CHANNEL)
5966
screenFailedToLoadListener.register()
6067
this.screenFailedToLoadEventStreamHandler = screenFailedToLoadListener.eventStreamHandler
68+
69+
// Register purchase delegate event channels
70+
val purchaseListener = BaseListenerWrapper(messenger, PURCHASE_EVENT_CHANNEL)
71+
purchaseListener.register()
72+
this.purchaseEventStreamHandler = purchaseListener.eventStreamHandler
73+
74+
val restoreListener = BaseListenerWrapper(messenger, RESTORE_EVENT_CHANNEL)
75+
restoreListener.register()
76+
this.restoreEventStreamHandler = restoreListener.eventStreamHandler
6177
}
6278

6379
fun initializeNoCodes(args: Map<String, Any>, result: Result) {
@@ -108,6 +124,33 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context:
108124
result.success(null)
109125
}
110126

127+
// MARK: - Purchase Delegate Methods
128+
129+
fun setPurchaseDelegate(result: Result) {
130+
noCodesSandwich?.setPurchaseDelegate(this)
131+
result.success(null)
132+
}
133+
134+
fun delegatedPurchaseCompleted(result: Result) {
135+
noCodesSandwich?.delegatedPurchaseCompleted()
136+
result.success(null)
137+
}
138+
139+
fun delegatedPurchaseFailed(errorMessage: String?, result: Result) {
140+
noCodesSandwich?.delegatedPurchaseFailed(errorMessage ?: "Unknown error")
141+
result.success(null)
142+
}
143+
144+
fun delegatedRestoreCompleted(result: Result) {
145+
noCodesSandwich?.delegatedRestoreCompleted()
146+
result.success(null)
147+
}
148+
149+
fun delegatedRestoreFailed(errorMessage: String?, result: Result) {
150+
noCodesSandwich?.delegatedRestoreFailed(errorMessage ?: "Unknown error")
151+
result.success(null)
152+
}
153+
111154
// NoCodesEventListener implementation
112155
override fun onNoCodesEvent(event: NoCodesEventListener.Event, payload: BridgeData?) {
113156
val eventData = mapOf("payload" to (payload ?: emptyMap<String, Any>()))
@@ -136,4 +179,14 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context:
136179
}
137180
}
138181
}
139-
}
182+
183+
// NoCodesPurchaseDelegateBridge implementation
184+
override fun purchase(product: BridgeData) {
185+
val jsonString = gson.toJson(product)
186+
purchaseEventStreamHandler?.eventSink?.success(jsonString)
187+
}
188+
189+
override fun restore() {
190+
restoreEventStreamHandler?.eventSink?.success("restore")
191+
}
192+
}

android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,23 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware {
111111
"remoteConfigList" -> {
112112
return remoteConfigList(result)
113113
}
114-
"closeNoCodes" -> noCodesPlugin?.closeNoCodes(result)
114+
"closeNoCodes" -> {
115+
noCodesPlugin?.closeNoCodes(result)
116+
return
117+
}
118+
// NoCodes Purchase Delegate methods without args
119+
"setNoCodesPurchaseDelegate" -> {
120+
noCodesPlugin?.setPurchaseDelegate(result)
121+
return
122+
}
123+
"delegatedPurchaseCompleted" -> {
124+
noCodesPlugin?.delegatedPurchaseCompleted(result)
125+
return
126+
}
127+
"delegatedRestoreCompleted" -> {
128+
noCodesPlugin?.delegatedRestoreCompleted(result)
129+
return
130+
}
115131
}
116132

117133
// Methods with args
@@ -138,6 +154,9 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware {
138154
"setScreenPresentationConfig" -> noCodesPlugin?.setScreenPresentationConfig(args["config"] as? Map<String, Any>, args["contextKey"] as? String, result)
139155
"showNoCodesScreen" -> noCodesPlugin?.showNoCodesScreen(args["contextKey"] as? String, result)
140156
"setNoCodesLocale" -> noCodesPlugin?.setLocale(args["locale"] as? String, result)
157+
// NoCodes Purchase Delegate methods
158+
"delegatedPurchaseFailed" -> noCodesPlugin?.delegatedPurchaseFailed(args["errorMessage"] as? String, result)
159+
"delegatedRestoreFailed" -> noCodesPlugin?.delegatedRestoreFailed(args["errorMessage"] as? String, result)
141160
else -> result.notImplemented()
142161
}
143162
}

example/lib/screens/no_codes_screen.dart

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,76 @@ import 'package:qonversion_flutter/qonversion_flutter.dart';
77
import '../app_state.dart';
88
import '../theme.dart';
99

10+
/// Sample PurchaseDelegate implementation for testing purposes.
11+
///
12+
/// In a real app, you would implement your own purchase system here
13+
/// (e.g., RevenueCat, custom backend, StoreKit directly, etc.).
14+
///
15+
/// For this sample, we use Qonversion SDK to perform purchases,
16+
/// just to demonstrate how the delegate works.
17+
class SamplePurchaseDelegate implements NoCodesPurchaseDelegate {
18+
final void Function(String message) onEvent;
19+
20+
SamplePurchaseDelegate({required this.onEvent});
21+
22+
@override
23+
Future<void> purchase(QProduct product) async {
24+
onEvent('🛒 [PurchaseDelegate] purchase() called for: ${product.qonversionId}');
25+
debugPrint('🛒 [PurchaseDelegate] purchase() called for product: ${product.qonversionId}');
26+
debugPrint(' Store ID: ${product.storeId}');
27+
debugPrint(' Type: ${product.type}');
28+
debugPrint(' Price: ${product.prettyPrice}');
29+
30+
try {
31+
// For testing purposes, we use Qonversion SDK here.
32+
// In a real app, you would use your own purchase system.
33+
onEvent('🔄 [PurchaseDelegate] Performing purchase...');
34+
final result = await Qonversion.getSharedInstance().purchaseWithResult(product);
35+
36+
if (result.status == QPurchaseResultStatus.success) {
37+
onEvent('✅ [PurchaseDelegate] Purchase successful!');
38+
debugPrint('✅ [PurchaseDelegate] Purchase successful');
39+
} else if (result.status == QPurchaseResultStatus.userCanceled) {
40+
onEvent('⚠️ [PurchaseDelegate] Purchase canceled by user');
41+
debugPrint('⚠️ [PurchaseDelegate] Purchase canceled');
42+
throw Exception('Purchase was canceled by user');
43+
} else if (result.status == QPurchaseResultStatus.pending) {
44+
onEvent('⏳ [PurchaseDelegate] Purchase pending');
45+
debugPrint('⏳ [PurchaseDelegate] Purchase pending');
46+
} else {
47+
final errorMessage = result.error?.message ?? 'Unknown error';
48+
onEvent('❌ [PurchaseDelegate] Purchase failed: $errorMessage');
49+
debugPrint('❌ [PurchaseDelegate] Purchase failed: $errorMessage');
50+
throw Exception(errorMessage);
51+
}
52+
} catch (e) {
53+
onEvent('❌ [PurchaseDelegate] Purchase error: $e');
54+
debugPrint('❌ [PurchaseDelegate] Purchase error: $e');
55+
rethrow;
56+
}
57+
}
58+
59+
@override
60+
Future<void> restore() async {
61+
onEvent('🔄 [PurchaseDelegate] restore() called');
62+
debugPrint('🔄 [PurchaseDelegate] restore() called');
63+
64+
try {
65+
// For testing purposes, we use Qonversion SDK here.
66+
// In a real app, you would use your own restore logic.
67+
onEvent('🔄 [PurchaseDelegate] Performing restore...');
68+
final entitlements = await Qonversion.getSharedInstance().restore();
69+
70+
onEvent('✅ [PurchaseDelegate] Restore successful! Entitlements: ${entitlements.length}');
71+
debugPrint('✅ [PurchaseDelegate] Restore successful. Entitlements count: ${entitlements.length}');
72+
} catch (e) {
73+
onEvent('❌ [PurchaseDelegate] Restore error: $e');
74+
debugPrint('❌ [PurchaseDelegate] Restore error: $e');
75+
rethrow;
76+
}
77+
}
78+
}
79+
1080
class NoCodesScreen extends StatefulWidget {
1181
const NoCodesScreen({super.key});
1282

@@ -20,6 +90,8 @@ class _NoCodesScreenState extends State<NoCodesScreen> {
2090

2191
NoCodesPresentationStyle _presentationStyle = NoCodesPresentationStyle.fullScreen;
2292
bool _animated = true;
93+
bool _purchaseDelegateEnabled = false;
94+
SamplePurchaseDelegate? _purchaseDelegate;
2395

2496
@override
2597
void dispose() {
@@ -53,6 +125,8 @@ class _NoCodesScreenState extends State<NoCodesScreen> {
53125
children: [
54126
_buildShowScreenSection(),
55127
const SizedBox(height: 16),
128+
_buildPurchaseDelegateSection(appState),
129+
const SizedBox(height: 16),
56130
_buildPresentationConfigSection(),
57131
const SizedBox(height: 16),
58132
_buildLocaleSection(),
@@ -94,6 +168,95 @@ class _NoCodesScreenState extends State<NoCodesScreen> {
94168
);
95169
}
96170

171+
Widget _buildPurchaseDelegateSection(AppState appState) {
172+
return SectionCard(
173+
title: 'Purchase Delegate',
174+
child: Column(
175+
crossAxisAlignment: CrossAxisAlignment.stretch,
176+
children: [
177+
Container(
178+
padding: const EdgeInsets.all(12),
179+
decoration: BoxDecoration(
180+
color: _purchaseDelegateEnabled
181+
? Colors.green.shade50
182+
: Colors.grey.shade100,
183+
borderRadius: BorderRadius.circular(8),
184+
border: Border.all(
185+
color: _purchaseDelegateEnabled
186+
? Colors.green.shade200
187+
: Colors.grey.shade300,
188+
),
189+
),
190+
child: Row(
191+
children: [
192+
Icon(
193+
_purchaseDelegateEnabled
194+
? Icons.check_circle
195+
: Icons.circle_outlined,
196+
color: _purchaseDelegateEnabled
197+
? Colors.green
198+
: Colors.grey,
199+
),
200+
const SizedBox(width: 12),
201+
Expanded(
202+
child: Column(
203+
crossAxisAlignment: CrossAxisAlignment.start,
204+
children: [
205+
Text(
206+
_purchaseDelegateEnabled
207+
? 'Custom Purchase Handling ENABLED'
208+
: 'Custom Purchase Handling DISABLED',
209+
style: TextStyle(
210+
fontWeight: FontWeight.w600,
211+
color: _purchaseDelegateEnabled
212+
? Colors.green.shade700
213+
: Colors.grey.shade700,
214+
),
215+
),
216+
const SizedBox(height: 4),
217+
Text(
218+
_purchaseDelegateEnabled
219+
? 'Purchases from No-Code screens will be handled by the custom delegate'
220+
: 'Purchases from No-Code screens will use default SDK flow',
221+
style: TextStyle(
222+
fontSize: 12,
223+
color: Colors.grey.shade600,
224+
),
225+
),
226+
],
227+
),
228+
),
229+
],
230+
),
231+
),
232+
const SizedBox(height: 12),
233+
SizedBox(
234+
width: double.infinity,
235+
child: ElevatedButton.icon(
236+
onPressed: _purchaseDelegateEnabled ? null : () => _togglePurchaseDelegate(appState),
237+
icon: const Icon(Icons.play_arrow),
238+
label: const Text('Enable Custom Purchase Delegate'),
239+
style: ElevatedButton.styleFrom(
240+
backgroundColor: Colors.green,
241+
foregroundColor: Colors.white,
242+
),
243+
),
244+
),
245+
const SizedBox(height: 8),
246+
Text(
247+
'When enabled, purchase() and restore() calls from No-Code screens '
248+
'will be logged in the Events section below. Once enabled, delegate stays active.',
249+
style: TextStyle(
250+
fontSize: 11,
251+
fontStyle: FontStyle.italic,
252+
color: Colors.grey.shade500,
253+
),
254+
),
255+
],
256+
),
257+
);
258+
}
259+
97260
Widget _buildPresentationConfigSection() {
98261
return SectionCard(
99262
title: 'Presentation Config',
@@ -217,14 +380,27 @@ class _NoCodesScreenState extends State<NoCodesScreen> {
217380
itemCount: appState.noCodesEvents.length,
218381
itemBuilder: (context, index) {
219382
final event = appState.noCodesEvents[index];
383+
// Color-code different event types
384+
Color textColor = Colors.grey.shade800;
385+
if (event.contains('[PurchaseDelegate]')) {
386+
if (event.contains('✅')) {
387+
textColor = Colors.green.shade700;
388+
} else if (event.contains('❌')) {
389+
textColor = Colors.red.shade700;
390+
} else if (event.contains('🛒') || event.contains('🔄')) {
391+
textColor = Colors.blue.shade700;
392+
} else if (event.contains('⚠️')) {
393+
textColor = Colors.orange.shade700;
394+
}
395+
}
220396
return Padding(
221397
padding: const EdgeInsets.symmetric(vertical: 2),
222398
child: Text(
223399
event,
224400
style: TextStyle(
225401
fontSize: 12,
226402
fontFamily: 'monospace',
227-
color: Colors.grey.shade800,
403+
color: textColor,
228404
),
229405
),
230406
);
@@ -234,6 +410,29 @@ class _NoCodesScreenState extends State<NoCodesScreen> {
234410
);
235411
}
236412

413+
void _togglePurchaseDelegate(AppState appState) async {
414+
// Create and set the purchase delegate
415+
_purchaseDelegate = SamplePurchaseDelegate(
416+
onEvent: (message) {
417+
appState.addNoCodesEvent(message);
418+
},
419+
);
420+
421+
try {
422+
debugPrint('🔄 [NoCodes] Enabling custom purchase delegate...');
423+
await NoCodes.getSharedInstance().setPurchaseDelegate(_purchaseDelegate!);
424+
setState(() {
425+
_purchaseDelegateEnabled = true;
426+
});
427+
debugPrint('✅ [NoCodes] Custom purchase delegate enabled');
428+
appState.addNoCodesEvent('✅ Custom Purchase Delegate ENABLED');
429+
_showSuccess('Custom purchase handling enabled');
430+
} catch (e) {
431+
debugPrint('❌ [NoCodes] Failed to set purchase delegate: $e');
432+
_showError('Failed to enable purchase delegate: $e');
433+
}
434+
}
435+
237436
void _showScreen() {
238437
final contextKey = _contextKeyController.text.trim();
239438

0 commit comments

Comments
 (0)