Skip to content

Commit e23796a

Browse files
authored
Merge pull request #323 from Countly/web_sdk_support
feat: web sdk support to flutter
2 parents 9cf751e + a469dbc commit e23796a

File tree

13 files changed

+966
-34
lines changed

13 files changed

+966
-34
lines changed

CHANGELOG.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
1-
## XX.XX.XX
1+
## 25.1.0
2+
* Added experimental support for the web platform in the Countly Flutter SDK. Some functionalities are not yet fully supported. Below is the list of limitations for the web platform:
3+
* Hybrid sessions are the default; full manual sessions are not supported.
4+
* Features Not Supported: Push Notifications, APM, and Attribution.
5+
* Countly.setUserLocation and Countly.disableLocation are unavailable.
6+
* In Views, the following view-related functions are not supported:
7+
* startView
8+
* stopViewWithName
9+
* stopViewWithID
10+
* stopAllViews
11+
* pauseViewWithID
12+
* resumeViewWithID
13+
* addSegmentationToViewWithName
14+
* addSegmentationToViewWithID
15+
* updateGlobalViewSegmentation
16+
* setGlobalViewSegmentation
17+
* In Remote Config, the clearAll function is unavailable, and caching functionality is not provided.
18+
* In A/B Testing the following are not supported:
19+
* exitABTestsForKeys
20+
* Variant-level control
21+
* Experiment-level control
22+
* Star Rating and related configuration options are unavailable.
23+
* User properties during initialization are not supported.
24+
* Custom network headers are not supported.
25+
* Dropping old requests is not supported.
26+
* Content zone global callback is not supported.
27+
* Experimental configuration options are not supported.
28+
229
* Added 'event' interface for events methods which are
330
* recordEvent(String key, [Map<String, Object>? segmentation, int? count, int? sum, int? duration])
431
* startEvent(String key)
@@ -11,7 +38,6 @@
1138
* 'startEvent(key)', instead use 'events.startEvent(key)'
1239
* 'endEvent(options)', instead use 'events.endEvent(key, [segmentation, count, sum])'
1340

14-
## 25.1.0
1541
* Improved content size management for better adaptability across devices.
1642
* Resolved an issue where the action bar overlapped with the content display.
1743
* Added dynamic resizing functionality for the content zone for enhanced responsiveness.
@@ -23,6 +49,7 @@
2349

2450
* Updated underlying Android SDK version to 25.1.0
2551
* Updated underlying iOS SDK version to 25.1.0
52+
* Added underlying Web SDK version to 24.11.4
2653

2754
## 24.11.2
2855
* Improved view tracking capabilities in iOS.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'dart:convert';
2+
import 'dart:html';
3+
import 'package:countly_flutter/countly_flutter.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:integration_test/integration_test.dart';
6+
7+
import 'utils.dart';
8+
9+
/// Check if we can get stored queues from native side
10+
void main() {
11+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
12+
13+
group("Device ID change tests", () {
14+
tearDown(() async {
15+
await Countly.instance.halt();
16+
window.localStorage.clear();
17+
});
18+
test("Check init time temp mode with setID", () async {
19+
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY).setLoggingEnabled(true).enableTemporaryDeviceIDMode();
20+
await Countly.initWithConfig(config);
21+
22+
List<String> requestQueue = await getRequestQueue();
23+
List<String> eventQueue = await getEventQueue();
24+
25+
expect(1, requestQueue.length);
26+
expect(1, eventQueue.length);
27+
28+
dynamic request = jsonDecode(requestQueue[0]);
29+
expect("[CLY]_temp_id", request["device_id"]);
30+
31+
await Countly.recordEvent({"key": "1"});
32+
await Countly.instance.userProfile.setUserProperties({"name": "name"});
33+
34+
await Countly.instance.deviceId.setID("new ID");
35+
36+
await Countly.recordEvent({"key": "2"});
37+
38+
requestQueue = await getRequestQueue();
39+
eventQueue = await getEventQueue();
40+
41+
expect(4, requestQueue.length);
42+
expect(1, eventQueue.length);
43+
44+
// observe that all requests got new ID
45+
for (var request in requestQueue) {
46+
dynamic req = jsonDecode(request);
47+
expect("new ID", req["device_id"]);
48+
}
49+
});
50+
51+
test("Check init time temp mode with changeWithoutMerge", () async {
52+
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY).setLoggingEnabled(true).enableTemporaryDeviceIDMode();
53+
await Countly.initWithConfig(config);
54+
55+
List<String> requestQueue = await getRequestQueue();
56+
List<String> eventQueue = await getEventQueue();
57+
58+
expect(1, requestQueue.length);
59+
expect(0, eventQueue.length);
60+
61+
dynamic request = jsonDecode(requestQueue[0]);
62+
expect("[CLY]_temp_id", request["device_id"]);
63+
64+
await Countly.recordEvent({"key": "1"});
65+
await Countly.instance.userProfile.setUserProperties({"name": "name"});
66+
67+
await Countly.instance.deviceId.changeWithoutMerge("new ID");
68+
69+
await Countly.recordEvent({"key": "2"});
70+
71+
requestQueue = await getRequestQueue();
72+
eventQueue = await getEventQueue();
73+
74+
expect(4, requestQueue.length);
75+
expect(1, eventQueue.length);
76+
77+
// observe that all requests got new ID
78+
for (var request in requestQueue) {
79+
dynamic req = jsonDecode(request);
80+
expect("new ID", req["device_id"]);
81+
}
82+
});
83+
84+
test("Check init time temp mode with changeWithMerge", () async {
85+
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY).setLoggingEnabled(true).enableTemporaryDeviceIDMode();
86+
await Countly.initWithConfig(config);
87+
88+
List<String> requestQueue = await getRequestQueue();
89+
List<String> eventQueue = await getEventQueue();
90+
91+
expect(1, requestQueue.length);
92+
expect(0, eventQueue.length);
93+
94+
dynamic request = jsonDecode(requestQueue[0]);
95+
expect("[CLY]_temp_id", request["device_id"]);
96+
97+
await Countly.recordEvent({"key": "1"});
98+
await Countly.instance.userProfile.setUserProperties({"name": "name"});
99+
100+
await Countly.instance.deviceId.changeWithMerge("new ID");
101+
102+
await Countly.recordEvent({"key": "2"});
103+
104+
requestQueue = await getRequestQueue();
105+
eventQueue = await getEventQueue();
106+
107+
expect(4, requestQueue.length);
108+
expect(1, eventQueue.length);
109+
110+
// observe that all requests got new ID
111+
for (var request in requestQueue) {
112+
dynamic req = jsonDecode(request);
113+
expect("new ID", req["device_id"]);
114+
}
115+
});
116+
});
117+
}

example/integration_test/utils.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:convert';
22
import 'dart:io';
33
import 'package:countly_flutter/countly_flutter.dart';
4+
import 'package:flutter/foundation.dart';
45
import 'package:flutter/services.dart';
56
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
67
import 'package:flutter_test/flutter_test.dart';
@@ -28,14 +29,24 @@ Future<List<String>> getEventQueue() async {
2829
/// Verify the common request queue parameters
2930
void testCommonRequestParams(Map<String, List<String>> requestObject) {
3031
expect(requestObject['app_key']?[0], APP_KEY);
31-
expect(requestObject['sdk_name']?[0], "dart-flutterb-${Platform.isIOS ? "ios" : "android"}");
32+
expect(
33+
requestObject['sdk_name']?[0],
34+
"dart-flutterb-${kIsWeb ? 'web' : Platform.isIOS ? 'ios' : 'android'}");
3235
expect(requestObject['sdk_version']?[0], '25.1.0');
33-
expect(requestObject['av']?[0], Platform.isIOS ? '0.0.1' : '1.0.0');
36+
expect(
37+
requestObject['av']?[0],
38+
kIsWeb
39+
? '0.0'
40+
: Platform.isIOS
41+
? '0.0.1'
42+
: '1.0.0');
3443
assert(requestObject['timestamp']?[0] != null);
3544

3645
expect(requestObject['hour']?[0], DateTime.now().hour.toString());
3746
expect(requestObject['dow']?[0], DateTime.now().weekday.toString());
38-
expect(requestObject['tz']?[0], DateTime.now().timeZoneOffset.inMinutes.toString());
47+
if (!kIsWeb) {
48+
expect(requestObject['tz']?[0], DateTime.now().timeZoneOffset.inMinutes.toString());
49+
}
3950
}
4051

4152
/// Verify custom request queue parameters

example/lib/main.dart

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:countly_flutter_example/page_sessions.dart';
1414
import 'package:countly_flutter_example/page_user_profiles.dart';
1515
import 'package:countly_flutter_example/page_views.dart';
1616
import 'package:countly_flutter_example/style.dart';
17+
import 'package:flutter/foundation.dart' show kIsWeb;
1718
import 'package:flutter/material.dart';
1819

1920
void main() {
@@ -37,29 +38,28 @@ class _MyAppState extends State<MyApp> {
3738
@override
3839
void initState() {
3940
super.initState();
40-
Countly.isInitialized().then((bool isInitialized) {
41-
if (!isInitialized) {
42-
Countly.pushTokenType(Countly.messagingMode['TEST']!); // Set messaging mode for push notifications
4341

44-
CountlyConfig config = CountlyConfiguration.getConfig();
45-
Countly.initWithConfig(config).then((value) {
46-
Countly.appLoadingFinished(); // for APM feature
42+
if (!kIsWeb) {
43+
Countly.pushTokenType(Countly.messagingMode['TEST']!); // Set messaging mode for push notifications
44+
}
4745

48-
/// Push notifications settings. Should be call after init
49-
Countly.onNotification((String notification) {
50-
print('The notification:[$notification]');
51-
}); // Set callback to receive push notifications
46+
CountlyConfig config = CountlyConfiguration.getConfig();
47+
Countly.initWithConfig(config).then((value) {
48+
Countly.appLoadingFinished(); // for APM feature
5249

53-
Countly.askForNotificationPermission(); // This method will ask for permission, enables push notification and send push token to countly server.;
50+
if (!kIsWeb) {
51+
/// Push notifications settings. Should be call after init
52+
Countly.onNotification((String notification) {
53+
print('The notification:[$notification]');
54+
}); // Set callback to receive push notifications
5455

55-
Countly.instance.remoteConfig.registerDownloadCallback((rResult, error, fullValueUpdate, downloadedValues) {
56-
print('download callback after init 3');
57-
});
58-
}); // Initialize the countly SDK.
59-
} else {
60-
print('Countly: Already initialized!');
56+
Countly.askForNotificationPermission(); // This method will ask for permission, enables push notification and send push token to countly server.;
6157
}
62-
});
58+
59+
Countly.instance.remoteConfig.registerDownloadCallback((rResult, error, fullValueUpdate, downloadedValues) {
60+
print('download callback after init 3');
61+
});
62+
}); // Initialize the countly SDK.
6363
}
6464

6565
@override

example/lib/page_others.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:io';
22

33
import 'package:countly_flutter/countly_flutter.dart';
44
import 'package:countly_flutter_example/helpers.dart';
5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/material.dart';
67

78
class OthersPage extends StatelessWidget {
@@ -12,10 +13,12 @@ class OthersPage extends StatelessWidget {
1213

1314
void recordIndirectAttribution() {
1415
Map<String, String> attributionValues = {};
15-
if (Platform.isIOS) {
16-
attributionValues[AttributionKey.IDFA] = 'IDFA';
17-
} else {
18-
attributionValues[AttributionKey.AdvertisingID] = 'AdvertisingID';
16+
if (!kIsWeb) {
17+
if (Platform.isIOS) {
18+
attributionValues[AttributionKey.IDFA] = 'IDFA';
19+
} else {
20+
attributionValues[AttributionKey.AdvertisingID] = 'AdvertisingID';
21+
}
1922
}
2023
Countly.recordIndirectAttribution(attributionValues);
2124
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import 'package:integration_test/integration_test_driver.dart';
2+
3+
Future<void> main() => integrationDriver();

lib/src/countly_flutter.dart

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ class Countly {
120120
/// Flag to determine if manual session is enabled
121121
static bool _manualSessionControlEnabled = false;
122122

123-
static Map<String, String> messagingMode = Platform.isAndroid ? {'TEST': '2', 'PRODUCTION': '0'} : {'TEST': '1', 'PRODUCTION': '0', 'ADHOC': '2'};
123+
static Map<String, String> messagingMode = kIsWeb
124+
? {}
125+
: Platform.isAndroid
126+
? {'TEST': '2', 'PRODUCTION': '0'}
127+
: {'TEST': '1', 'PRODUCTION': '0', 'ADHOC': '2'};
124128

125129
static const temporaryDeviceID = 'CLYTemporaryDeviceID';
126130
@Deprecated('This variable is deprecated, please use "Countly.instance.deviceId.enableTemporaryDeviceID()" instead')
@@ -549,7 +553,7 @@ class Countly {
549553
log('disablePushNotifications, $_pushDisabledMsg', logLevel: LogLevel.ERROR);
550554
return _pushDisabledMsg;
551555
}
552-
if (!Platform.isIOS) {
556+
if (kIsWeb || !Platform.isIOS) {
553557
return 'disablePushNotifications : To be implemented';
554558
}
555559
final String? result = await _channel.invokeMethod('disablePushNotifications');
@@ -2000,10 +2004,12 @@ class Countly {
20002004
return 'Error : $error';
20012005
}
20022006
Map<String, String> attributionValues = {};
2003-
if (Platform.isIOS) {
2004-
attributionValues[AttributionKey.IDFA] = attributionID;
2005-
} else {
2006-
attributionValues[AttributionKey.AdvertisingID] = attributionID;
2007+
if (!kIsWeb) {
2008+
if (Platform.isIOS) {
2009+
attributionValues[AttributionKey.IDFA] = attributionID;
2010+
} else {
2011+
attributionValues[AttributionKey.AdvertisingID] = attributionID;
2012+
}
20072013
}
20082014
final String? result = await recordIndirectAttribution(attributionValues);
20092015
return result;
@@ -2285,6 +2291,13 @@ class Countly {
22852291
}
22862292
return countlyConfig;
22872293
}
2294+
2295+
/// This method is for testing purposes only
2296+
/// Do not use this method in production
2297+
Future<void> halt() {
2298+
_countlyState.isInitialized = false;
2299+
return _channel.invokeMethod('halt');
2300+
}
22882301
}
22892302

22902303
class CountlyPresentableFeedback {

lib/src/remote_config_internal.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,10 @@ class RemoteConfigInternal implements RemoteConfig {
412412
return {};
413413
}
414414

415-
final List<dynamic> experimentsInfo = await _countlyState.channel.invokeMethod('testingGetAllExperimentInfo');
415+
final List<dynamic>? experimentsInfo = await _countlyState.channel.invokeMethod('testingGetAllExperimentInfo');
416+
if (experimentsInfo == null) {
417+
return {};
418+
}
416419
final List<ExperimentInformation> experimentsInfoList = experimentsInfo.map((e) => ExperimentInformation.fromJson(e)).toList();
417420
final Map<String, ExperimentInformation> experimentsInfoMap = {for (final e in experimentsInfoList) e.experimentID: e};
418421
return experimentsInfoMap;

0 commit comments

Comments
 (0)