Skip to content

Commit a821cc9

Browse files
committed
feat: add web support with CustomerIOWebPlugin and jsKey configuration
(cherry picked from commit 7273c33)
1 parent 6f8cceb commit a821cc9

File tree

5 files changed

+240
-2
lines changed

5 files changed

+240
-2
lines changed

customerio-flutter.api

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class CustomerIO {
4444
public final class CustomerIOConfig {
4545
public fun new(
4646
required String cdpApiKey,
47+
String? jsKey,
4748
String? migrationSiteId,
4849
Region? region,
4950
CioLogLevel? logLevel,
@@ -63,6 +64,7 @@ public final class CustomerIOConfig {
6364
public val cdnHost: String?;
6465
public val cdpApiKey: String;
6566
public val flushAt: int?;
67+
public val jsKey: String?;
6668
public val flushInterval: int?;
6769
public val inAppConfig: InAppConfig?;
6870
public val logLevel: CioLogLevel?;
@@ -234,4 +236,3 @@ public final class ScreenView {
234236
static public val values: List<ScreenView>;
235237
}
236238

237-

lib/config/customer_io_config.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CustomerIOConfig {
88
final String version = plugin_info.version;
99

1010
final String cdpApiKey;
11+
final String? jsKey;
1112
final String? migrationSiteId;
1213
final Region? region;
1314
final CioLogLevel? logLevel;
@@ -23,6 +24,7 @@ class CustomerIOConfig {
2324

2425
CustomerIOConfig({
2526
required this.cdpApiKey,
27+
this.jsKey,
2628
this.migrationSiteId,
2729
this.region,
2830
this.logLevel,
@@ -38,7 +40,7 @@ class CustomerIOConfig {
3840
}) : pushConfig = pushConfig ?? PushConfig();
3941

4042
Map<String, dynamic> toMap() {
41-
return {
43+
final map = {
4244
'cdpApiKey': cdpApiKey,
4345
'migrationSiteId': migrationSiteId,
4446
'region': region?.name,
@@ -55,5 +57,11 @@ class CustomerIOConfig {
5557
'version': version,
5658
'source': source
5759
};
60+
61+
if (jsKey != null) {
62+
map['jsKey'] = jsKey;
63+
}
64+
65+
return map;
5866
}
5967
}

lib/customer_io_web.dart

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import 'dart:js_interop';
2+
import 'dart:js_interop_unsafe';
3+
import 'package:js/js.dart';
4+
5+
import 'package:customer_io/customer_io_config.dart';
6+
import 'package:customer_io/customer_io_enums.dart';
7+
import 'package:customer_io/data_pipelines/customer_io_platform_interface.dart';
8+
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
9+
10+
@JS('window')
11+
external JSObject get _window;
12+
13+
@JS('Function')
14+
external Object _jsFunctionConstructor(String code);
15+
16+
class CustomerIOWebPlugin extends CustomerIOPlatform {
17+
String? _currentUserId;
18+
late final CustomerIOConfig _config;
19+
20+
static void registerWith(Registrar registrar) {
21+
CustomerIOPlatform.instance = CustomerIOWebPlugin();
22+
}
23+
24+
void _injectAnalyticsSnippet(String key) {
25+
try {
26+
final has = _window.has('cioanalytics');
27+
if (has) return;
28+
} catch (_) {}
29+
30+
final snippet = '''
31+
!(function () {
32+
var i = "cioanalytics",
33+
analytics = (window[i] = window[i] || []);
34+
if (!analytics.initialize)
35+
if (analytics.invoked)
36+
window.console &&
37+
console.error &&
38+
console.error("Snippet included twice.");
39+
else {
40+
analytics.invoked = !0;
41+
analytics.methods = [
42+
"trackSubmit",
43+
"trackClick",
44+
"trackLink",
45+
"trackForm",
46+
"pageview",
47+
"identify",
48+
"reset",
49+
"group",
50+
"track",
51+
"ready",
52+
"alias",
53+
"debug",
54+
"page",
55+
"once",
56+
"off",
57+
"on",
58+
"addSourceMiddleware",
59+
"addIntegrationMiddleware",
60+
"setAnonymousId",
61+
"addDestinationMiddleware",
62+
];
63+
analytics.factory = function (e) {
64+
return function () {
65+
var t = Array.prototype.slice.call(arguments);
66+
t.unshift(e);
67+
analytics.push(t);
68+
return analytics;
69+
};
70+
};
71+
for (var e = 0; e < analytics.methods.length; e++) {
72+
var key = analytics.methods[e];
73+
analytics[key] = analytics.factory(key);
74+
}
75+
analytics.load = function (key, e) {
76+
var t = document.createElement("script");
77+
t.type = "text/javascript";
78+
t.async = !0;
79+
t.setAttribute("data-global-customerio-analytics-key", i);
80+
t.src =
81+
"https://cdp.customer.io/v1/analytics-js/snippet/" +
82+
key +
83+
"/analytics.min.js";
84+
var n = document.getElementsByTagName("script")[0];
85+
n.parentNode.insertBefore(t, n);
86+
analytics._writeKey = key;
87+
analytics._loadOptions = e;
88+
};
89+
analytics.SNIPPET_VERSION = "4.15.3";
90+
analytics.load("$key");
91+
}
92+
})();
93+
''';
94+
95+
try {
96+
final fn = _jsFunctionConstructor(snippet) as Function;
97+
fn();
98+
} catch (_) {
99+
print('CustomerIO web: Failed to inject analytics snippet.');
100+
}
101+
}
102+
103+
JSObject get _cio {
104+
if (!_window.has('cioanalytics')) {
105+
_window['cioanalytics'] = <JSAny?>[].toJS;
106+
}
107+
return _window['cioanalytics'] as JSObject;
108+
}
109+
110+
JSAny? _toJS(dynamic value) {
111+
if (value == null) return null;
112+
if (value is String) return value.toJS;
113+
if (value is num) return value.toJS;
114+
if (value is bool) return value.toJS;
115+
if (value is List) {
116+
return value.map(_toJS).toList().toJS;
117+
}
118+
if (value is Map<String, dynamic>) {
119+
final jsObject = JSObject();
120+
value.forEach((key, val) {
121+
jsObject[key] = _toJS(val);
122+
});
123+
return jsObject;
124+
}
125+
return value.toString().toJS;
126+
}
127+
128+
void _callCio(String method, [List<dynamic>? args]) {
129+
final List<JSAny?> payload = [method.toJS];
130+
if (args != null && args.isNotEmpty) {
131+
payload.addAll(args.map(_toJS));
132+
}
133+
134+
_cio.callMethod('push'.toJS, payload.toJS);
135+
}
136+
137+
@override
138+
Future<void> initialize({required CustomerIOConfig config}) async {
139+
_config = config;
140+
final key = _config.jsKey ?? '';
141+
if (key.isEmpty) {
142+
print(
143+
'CustomerIO web: JSKey is empty. Do NOT use cdpApiKey in client-side code.');
144+
}
145+
_injectAnalyticsSnippet(key);
146+
}
147+
148+
@override
149+
void identify(
150+
{required String userId, Map<String, dynamic> traits = const {}}) {
151+
_currentUserId = userId;
152+
153+
if (traits.isEmpty) {
154+
_callCio('identify', [userId]);
155+
} else {
156+
_callCio('identify', [userId, traits]);
157+
}
158+
}
159+
160+
@override
161+
void clearIdentify() {
162+
_currentUserId = null;
163+
_callCio('reset', const []);
164+
}
165+
166+
@override
167+
void track(
168+
{required String name, Map<String, dynamic> properties = const {}}) {
169+
if (properties.isEmpty) {
170+
_callCio('track', [name]);
171+
} else {
172+
_callCio('track', [name, properties]);
173+
}
174+
}
175+
176+
@override
177+
void screen(
178+
{required String title, Map<String, dynamic> properties = const {}}) {
179+
if (properties.isEmpty) {
180+
_callCio('page', [title]);
181+
} else {
182+
_callCio('page', [title, properties]);
183+
}
184+
}
185+
186+
@override
187+
void trackMetric(
188+
{required String deliveryID,
189+
required String deviceToken,
190+
required MetricEvent event}) {}
191+
192+
@override
193+
void deleteDeviceToken() {}
194+
195+
@override
196+
void registerDeviceToken({required String deviceToken}) {}
197+
198+
@override
199+
void setDeviceAttributes({required Map<String, dynamic> attributes}) {
200+
if (_currentUserId != null) {
201+
identify(userId: _currentUserId!, traits: attributes);
202+
}
203+
}
204+
205+
@override
206+
void setProfileAttributes({required Map<String, dynamic> attributes}) {
207+
if (_currentUserId != null) {
208+
identify(userId: _currentUserId!, traits: attributes);
209+
}
210+
}
211+
}

pubspec.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ dependencies:
1212
flutter:
1313
sdk: flutter
1414
plugin_platform_interface: ^2.0.2
15+
flutter_web_plugins:
16+
sdk: flutter
17+
js: ^0.6.5
1518

1619
dev_dependencies:
1720
flutter_test:
@@ -45,3 +48,6 @@ flutter:
4548
pluginClass: CustomerIOPlugin
4649
native_sdk_version: 4.0.0
4750
firebase_wrapper_version: 1.0.0
51+
web:
52+
pluginClass: CustomerIOWebPlugin
53+
fileName: customer_io_web.dart

test/customer_io_config_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ void main() {
1111
final config = CustomerIOConfig(cdpApiKey: 'testApiKey');
1212

1313
expect(config.cdpApiKey, 'testApiKey');
14+
expect(config.jsKey, isNull);
1415
expect(config.migrationSiteId, isNull);
1516
expect(config.region, isNull);
1617
expect(config.logLevel, isNull);
@@ -43,6 +44,7 @@ void main() {
4344

4445
final config = CustomerIOConfig(
4546
cdpApiKey: 'testApiKey',
47+
jsKey: 'webKey',
4648
migrationSiteId: 'testMigrationSiteId',
4749
region: Region.us,
4850
logLevel: CioLogLevel.debug,
@@ -57,6 +59,7 @@ void main() {
5759
);
5860

5961
expect(config.cdpApiKey, 'testApiKey');
62+
expect(config.jsKey, 'webKey');
6063
expect(config.migrationSiteId, 'testMigrationSiteId');
6164
expect(config.region, Region.us);
6265
expect(config.logLevel, CioLogLevel.debug);
@@ -81,6 +84,7 @@ void main() {
8184

8285
final config = CustomerIOConfig(
8386
cdpApiKey: 'testApiKey',
87+
jsKey: 'webKey',
8488
migrationSiteId: 'testMigrationSiteId',
8589
region: Region.eu,
8690
logLevel: CioLogLevel.info,
@@ -98,6 +102,7 @@ void main() {
98102
final expectedMap = {
99103
'cdpApiKey': 'testApiKey',
100104
'migrationSiteId': 'testMigrationSiteId',
105+
'jsKey': 'webKey',
101106
'region': 'eu',
102107
'logLevel': 'info',
103108
'autoTrackDeviceAttributes': false,
@@ -122,6 +127,13 @@ void main() {
122127
expect(config.pushConfig.pushConfigAndroid.pushClickBehavior,
123128
PushClickBehaviorAndroid.activityPreventRestart);
124129
});
130+
131+
test('toMap() omits jsKey when not provided', () {
132+
final config = CustomerIOConfig(cdpApiKey: 'testApiKey');
133+
134+
final map = config.toMap();
135+
expect(map.containsKey('jsKey'), isFalse);
136+
});
125137
});
126138

127139
group('CustomerIOConfig with Region', () {

0 commit comments

Comments
 (0)