Skip to content

Commit

Permalink
Merge pull request #301 from Countly/present_calls
Browse files Browse the repository at this point in the history
[Flutter] tweak present calls
  • Loading branch information
turtledreams authored Nov 29, 2024
2 parents 31981ee + 8f1e732 commit 6ad53b5
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## 24.11.1
* Added content configuration interface that has `setGlobalContentCallback` to get notified about content changes.
* Added support for localization of content blocks.
* Added the interface `feedback` and convenience methods that presents the first available widget to user:
* presentNPS([String? nameTagOrID, FeedbackCallback? callback])
* presentSurvey([String? nameTagOrID, FeedbackCallback? callback])
* presentRating([String? nameTagOrID, FeedbackCallback? callback])

* Mitigated an issue where visibility could have been wrongly assigned if a view was closed while going to background. (Experimental!)
* Mitigated issues where:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,65 @@ public void onClosed() {
}
});
}
} else if ("getFeedbackWidgetData".equals(call.method)) {
} else if("presentNPS".equals(call.method)){
if (activity == null) {
log("presentNPS failed : Activity is null", LogLevel.ERROR);
methodChannel.invokeMethod("feedbackCallback_onFinished", "Activity is null");
return;
}
String nameIDorTag = args.optString(0, "");

Countly.sharedInstance().feedback().presentNPS(activity, nameIDorTag, new FeedbackCallback() {
@Override
public void onFinished(String error) {
methodChannel.invokeMethod("feedbackCallback_onFinished", error);
}

@Override
public void onClosed() {
methodChannel.invokeMethod("feedbackCallback_onClosed", null);
}
});
} else if("presentSurvey".equals(call.method)){
if (activity == null) {
log("presentSurvey failed : Activity is null", LogLevel.ERROR);
methodChannel.invokeMethod("feedbackCallback_onFinished", "Activity is null");
return;
}
String nameIDorTag = args.optString(0, "");

Countly.sharedInstance().feedback().presentSurvey(activity, nameIDorTag, new FeedbackCallback() {
@Override
public void onFinished(String error) {
methodChannel.invokeMethod("feedbackCallback_onFinished", error);
}

@Override
public void onClosed() {
methodChannel.invokeMethod("feedbackCallback_onClosed", null);
}
});
} else if("presentRating".equals(call.method)){
if (activity == null) {
log("presentRating failed : Activity is null", LogLevel.ERROR);
methodChannel.invokeMethod("feedbackCallback_onFinished", "Activity is null");
return;
}
String nameIDorTag = args.optString(0, "");

Countly.sharedInstance().feedback().presentRating(activity, nameIDorTag, new FeedbackCallback() {
@Override
public void onFinished(String error) {
methodChannel.invokeMethod("feedbackCallback_onFinished", error);
}

@Override
public void onClosed() {
methodChannel.invokeMethod("feedbackCallback_onClosed", null);
}
});
}
else if ("getFeedbackWidgetData".equals(call.method)) {
String widgetId = args.getString(0);
CountlyFeedbackWidget feedbackWidget = getFeedbackWidget(widgetId);
if (feedbackWidget == null) {
Expand Down
39 changes: 38 additions & 1 deletion example/lib/page_feedback_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class _FeedbackWidgetsPageState extends State<FeedbackWidgetsPage> {
super.dispose();
}

// for Countly Lite users only
void askForStarRating() {
Countly.askForStarRating();
}
Expand Down Expand Up @@ -267,8 +268,38 @@ class _FeedbackWidgetsPageState extends State<FeedbackWidgetsPage> {
});
}

void demoNPS(nameTagOrID, callback) {
if (ratingIdController.text.isNotEmpty) {
nameTagOrID = ratingIdController.text;
}
Countly.instance.feedback.presentNPS(nameTagOrID, callback);
}

void demoSurvey(nameTagOrID, callback) {
if (ratingIdController.text.isNotEmpty) {
nameTagOrID = ratingIdController.text;
}
Countly.instance.feedback.presentSurvey(nameTagOrID, callback);
}

void demoRating(nameTagOrID, callback) {
if (ratingIdController.text.isNotEmpty) {
nameTagOrID = ratingIdController.text;
}
Countly.instance.feedback.presentRating(nameTagOrID, callback);
}

@override
Widget build(BuildContext context) {
FeedbackCallback widgetCB = FeedbackCallback(onClosed: () {
showCountlyToast(context, 'Widget Closed', Colors.green);
}, onFinished: (error) {
if (error != null) {
showCountlyToast(context, 'Error: $error', Colors.red);
} else {
showCountlyToast(context, 'Widget Finished', Colors.green);
}
});
return Scaffold(
appBar: AppBar(
title: Text('FeedbackWidgets'),
Expand All @@ -278,13 +309,19 @@ class _FeedbackWidgetsPageState extends State<FeedbackWidgetsPage> {
child: Center(
child: Column(
children: [
MyButton(text: 'Present NPS', color: 'green', onPressed: () => demoNPS(null, null)),
MyButton(text: 'Present Survey', color: 'green', onPressed: () => demoSurvey(null, null)),
MyButton(text: 'Present Rating', color: 'green', onPressed: () => demoRating(null, null)),
MyButton(text: 'Present NPS wCallback', color: 'green', onPressed: () => demoNPS(null, widgetCB)),
MyButton(text: 'Present Survey wCallback', color: 'green', onPressed: () => demoSurvey(null, widgetCB)),
MyButton(text: 'Present Rating wCallback', color: 'green', onPressed: () => demoRating(null, widgetCB)),
MyButton(text: 'Open rating modal', color: 'orange', onPressed: askForStarRating),
MyButton(text: 'Open feedback modal', color: 'orange', onPressed: presentRatingWidget),
TextField(
controller: ratingIdController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter a Rating ID',
hintText: 'Rating ID, Tag or Name',
),
),
MyButton(text: 'Show Rating using EditBox', color: 'orange', onPressed: ratingIdController.text.isNotEmpty ? presentRatingWidgetUsingEditBox : null),
Expand Down
60 changes: 60 additions & 0 deletions ios/Classes/CountlyFlutterPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,66 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
}];
}
});
} else if ([@"presentNPS" isEqualToString:call.method]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *nameIDorTag = @"";

if (command.count != 0) {
nameIDorTag = [command objectAtIndex:0];
}

[Countly.sharedInstance.feedback presentNPS:nameIDorTag widgetCallback:^(WidgetState state) {
if (state == WIDGET_CLOSED) {
[_channel invokeMethod:@"feedbackCallback_onClosed" arguments:nil];
result(@"[CountlyFlutterPlugin] presentNPS, dismissed");
} else if (state = WIDGET_APPEARED) {
[_channel invokeMethod:@"feedbackCallback_Finished" arguments:nil];
result(@"[CountlyFlutterPlugin] presentNPS, appeared");
}
}];

result(@"[CountlyFlutterPlugin] presentNPS, success");
});
} else if ([@"presentRating" isEqualToString:call.method]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *nameIDorTag = @"";

if (command.count != 0) {
nameIDorTag = [command objectAtIndex:0];
}

[Countly.sharedInstance.feedback presentRating:nameIDorTag widgetCallback:^(WidgetState state) {
if (state == WIDGET_CLOSED) {
[_channel invokeMethod:@"feedbackCallback_onClosed" arguments:nil];
result(@"[CountlyFlutterPlugin] presentRating, dismissed");
} else if (state = WIDGET_APPEARED) {
[_channel invokeMethod:@"feedbackCallback_Finished" arguments:nil];
result(@"[CountlyFlutterPlugin] presentRating, appeared");
}
}];

result(@"[CountlyFlutterPlugin] presentRating, success");
});
} else if ([@"presentSurvey" isEqualToString:call.method]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *nameIDorTag = @"";

if (command.count != 0) {
nameIDorTag = [command objectAtIndex:0];
}

[Countly.sharedInstance.feedback presentSurvey:nameIDorTag widgetCallback:^(WidgetState state) {
if (state == WIDGET_CLOSED) {
[_channel invokeMethod:@"feedbackCallback_onClosed" arguments:nil];
result(@"CountlyFlutterPlugin] presentSurvey, dismissed");
} else if (state = WIDGET_APPEARED) {
[_channel invokeMethod:@"feedbackCallback_Finished" arguments:nil];
result(@"CountlyFlutterPlugin] presentSurvey, appeared");
}
}];

result(@"[CountlyFlutterPlugin] presentSurvey, success");
});
} else if ([@"getFeedbackWidgetData" isEqualToString:call.method]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *widgetId = [command objectAtIndex:0];
Expand Down
1 change: 1 addition & 0 deletions lib/countly_flutter.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export 'src/content_builder.dart';
export 'src/countly_config.dart';
export 'src/countly_flutter.dart';
export 'src/feedback.dart';
export 'src/remote_config.dart';
export 'src/sessions.dart';
export 'src/user_profile.dart';
Expand Down
19 changes: 19 additions & 0 deletions lib/src/countly_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:pedantic/pedantic.dart';

import 'content_builder.dart';
import 'content_builder_internal.dart';
import 'countly_config.dart';
import 'countly_state.dart';
import 'device_id.dart';
import 'device_id_internal.dart';
import 'feedback.dart';
import 'feedback_internal.dart';
import 'remote_config.dart';
import 'remote_config_internal.dart';
import 'sessions.dart';
Expand Down Expand Up @@ -64,6 +67,7 @@ class Countly {
_viewsInternal = ViewsInternal(_countlyState);
_sessionsInternal = SessionsInternal(_countlyState);
_contentBuilderInternal = ContentBuilderInternal(_countlyState);
_feedbackInternal = FeedbackInternal(_countlyState);
}
static final instance = _instance;
static final _instance = Countly._();
Expand All @@ -88,6 +92,9 @@ class Countly {
late final ContentBuilderInternal _contentBuilderInternal;
ContentBuilderInternal get content => _contentBuilderInternal;

late final FeedbackInternal _feedbackInternal;
Feedback get feedback => _feedbackInternal;

/// ignore: constant_identifier_names
static const bool BUILDING_WITH_PUSH_DISABLED = false;
static const String _pushDisabledMsg = 'In this plugin Push notification is disabled, Countly has separate plugin with push notification enabled';
Expand Down Expand Up @@ -212,6 +219,18 @@ class Countly {
log('[FMethodCallH] $e', logLevel: LogLevel.ERROR);
}
break;
case 'feedbackCallback_onClosed':
if (_instance._feedbackInternal.feedbackCallback != null) {
_instance._feedbackInternal.feedbackCallback!.onClosed();
_instance._feedbackInternal.feedbackCallback = null;
}
break;
case 'feedbackCallback_onFinished':
if (_instance._feedbackInternal.feedbackCallback != null) {
_instance._feedbackInternal.feedbackCallback!.onFinished(call.arguments);
_instance._feedbackInternal.feedbackCallback = null;
}
break;
case 'contentCallback':
Map<String, dynamic> argumentsMap = Map<String, dynamic>.from(call.arguments);
final int contentResult = argumentsMap['contentResult'];
Expand Down
17 changes: 17 additions & 0 deletions lib/src/feedback.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
typedef OnClosedCallback = void Function();
typedef OnFinishedCallback = void Function(String error);

class FeedbackCallback {
final OnClosedCallback onClosed;
final OnFinishedCallback onFinished;

FeedbackCallback({required this.onClosed, required this.onFinished});
}

abstract class Feedback {
Future<void> presentNPS([String? nameIDorTag, FeedbackCallback? feedbackCallback]);

Future<void> presentRating([String? nameIDorTag, FeedbackCallback? feedbackCallback]);

Future<void> presentSurvey([String? nameIDorTag, FeedbackCallback? feedbackCallback]);
}
63 changes: 63 additions & 0 deletions lib/src/feedback_internal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:convert';

import 'countly_flutter.dart';
import 'countly_state.dart';
import 'feedback.dart';

class FeedbackInternal implements Feedback {
FeedbackInternal(this._countlyState);
final CountlyState _countlyState;
FeedbackCallback? _feedbackCallback;
FeedbackCallback? get feedbackCallback => _feedbackCallback;
set feedbackCallback(FeedbackCallback? callback) {
// Add any validation or custom logic here if needed
_feedbackCallback = callback;
}

@override
Future<void> presentNPS([String? nameIDorTag, FeedbackCallback? feedbackCallback]) async {
if (!_countlyState.isInitialized) {
Countly.log('presentNPS, "initWithConfig" must be called before "presentNPS"', logLevel: LogLevel.ERROR);
feedbackCallback?.onFinished('init must be called before presentNPS');
return;
}

_feedbackCallback = feedbackCallback;
Countly.log('Calling "presentNPS" with nameIDorTag: [$nameIDorTag]');
await invokeFeedbackMethod('presentNPS', nameIDorTag);
}

@override
Future<void> presentRating([String? nameIDorTag, FeedbackCallback? feedbackCallback]) async {
if (!_countlyState.isInitialized) {
Countly.log('presentRating, "initWithConfig" must be called before "presentRating"', logLevel: LogLevel.ERROR);
feedbackCallback?.onFinished('init must be called before presentRating');
return;
}

_feedbackCallback = feedbackCallback;
Countly.log('Calling "presentRating" with nameIDorTag: [$nameIDorTag]');
await invokeFeedbackMethod('presentRating', nameIDorTag);
}

@override
Future<void> presentSurvey([String? nameIDorTag, FeedbackCallback? feedbackCallback]) async {
if (!_countlyState.isInitialized) {
Countly.log('presentSurvey, "initWithConfig" must be called before "presentSurvey"', logLevel: LogLevel.ERROR);
feedbackCallback?.onFinished('init must be called before presentSurvey');
return;
}

_feedbackCallback = feedbackCallback;
Countly.log('Calling "presentSurvey" with nameIDorTag: [$nameIDorTag]');
await invokeFeedbackMethod('presentSurvey', nameIDorTag);
}

Future<void> invokeFeedbackMethod(String methodName, String? nameIDorTag){
final args = [];
nameIDorTag ??= '';
args.add(nameIDorTag);

return _countlyState.channel.invokeMethod(methodName, <String, dynamic>{'data': json.encode(args)});
}
}

0 comments on commit 6ad53b5

Please sign in to comment.