connect_x_mobile_sdk 1.1.12
connect_x_mobile_sdk: ^1.1.12 copied to clipboard
ConnectX Mobile SDK for Mobile.
example/lib/main.dart
import 'package:connect_x_mobile_sdk/connect_x_mobile_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter_branch_sdk/flutter_branch_sdk.dart';
// นำทางจากนอก widget tree (Branch listener อยู่ใน main())
final navigatorKey = GlobalKey<NavigatorState>();
/// ยิง tracking ให้ทุก action ในตัวอย่าง — ชื่อ route ที่อยู่บนสุด SDK เติมให้เองใน cx_link
/// ponytail: เรียกตรงจุด action ไม่ทำ NavigatorObserver เพราะปุ่มที่ไม่ได้เปลี่ยนหน้า
/// (Clear Cookie, Submit) observer จับไม่ได้อยู่ดี
Future<void> track(String title, String event) =>
ConnectXMobileSdk.cxTracking({'cx_title': title, 'cx_event': event});
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ConnectXMobileSdk.initialize(
'-',
'-',
);
// --- Branch.io: capture attribution (deferred deep link) ทั้ง iOS + Android ---
// Branch เป็นเจ้าของ click → ส่ง UTM/custom param ของ ad จริงเข้ามาได้
// (ต่างจาก native Play referrer ที่ Google ตีเป็น organic บนเครื่องเทสต์)
await FlutterBranchSdk.init();
FlutterBranchSdk.listSession().listen((data) async {
debugPrint('Branch session: $data');
// ยิงเฉพาะ first install หรือคลิกมาจาก Branch link ที่มี attribution
final firstInstall = data['+is_first_session'] == true;
final clicked = data['+clicked_branch_link'] == true;
if (!firstInstall && !clicked) return;
try {
await ConnectXMobileSdk.cxTracking({
'cx_title': 'App Install',
'cx_event': 'install_referrer',
// UTM/custom param ที่ตั้งบน Branch link ผ่านมาตรง ๆ
if (data['utm_source'] != null) 'cx_utmSource': data['utm_source'],
if (data['utm_medium'] != null) 'cx_utmMedium': data['utm_medium'],
if (data['utm_campaign'] != null) 'cx_utmName': data['utm_campaign'],
if (data['utm_term'] != null) 'cx_utmTerm': data['utm_term'],
if (data['utm_content'] != null) 'cx_utmContent': data['utm_content'],
if (data['cx_trackid'] != null) 'cx_trackid': data['cx_trackid'],
// Branch attribution ของมันเอง
if (data['~campaign'] != null) 'cx_branch_campaign': data['~campaign'],
if (data['~channel'] != null) 'cx_branch_channel': data['~channel'],
if (data['~feature'] != null) 'cx_branch_feature': data['~feature'],
if (data['~referring_link'] != null)
'cx_referrer': data['~referring_link'],
'cx_referrer_url':
data.entries.map((e) => '${e.key}=${e.value}').join('&'),
});
debugPrint('Branch install stamped to ConnectX');
} catch (e) {
debugPrint('Branch cxTracking error: $e');
}
// Deep link routing: ลิ้ง Branch ใส่ $deeplink_path=/open-ticket → เปิดหน้านั้น
// ponytail: cold-start ที่ listener ยิงก่อน navigator พร้อม จะไม่ route (currentState null)
final path = (data[r'$deeplink_path'] ?? data['route'])?.toString();
if (path != null && path.isNotEmpty) {
final route = path.startsWith('/') ? path : '/$path';
navigatorKey.currentState?.pushNamed(route);
debugPrint('Branch deep link → $route');
}
}, onError: (e) => debugPrint('Branch listSession error: $e'));
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'ConnectX SDK Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
// --- กำหนด Routes เพื่อให้ SDK ดึงชื่อไปใช้ใน cx_link ---
initialRoute: '/',
routes: {
'/': (context) => const MainMenuPage(),
'/open-ticket': (context) => const OpenTicketPage(),
'/drop-form': (context) => const DropFormPage(),
},
);
}
}
// --- FIRST PAGE: MENU ---
class MainMenuPage extends StatefulWidget {
const MainMenuPage({super.key});
@override
State<MainMenuPage> createState() => _MainMenuPageState();
}
class _MainMenuPageState extends State<MainMenuPage> {
@override
void initState() {
super.initState();
track('Main Menu', 'pageview');
}
Future<void> _go(String title, String event, String route) async {
await track(title, event); // cx_link ตอนนี้ยังเป็น "/" (หน้าที่กดออกมา)
if (mounted)
Navigator.pushNamed(context, route).then((_) => setState(() {}));
}
Future<void> _clearCookie() async {
await track('Press Clear Cookie Menu', 'clear_cookie');
await ConnectXMobileSdk.cxClearCookie();
if (!mounted) return;
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Cookie ใหม่: ${ConnectXMobileSdk.instance.cookie}'),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
title: const Text('ConnectX SDK Menu'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading:
const Icon(Icons.confirmation_number, color: Colors.blue),
title: const Text('Open Ticket'),
subtitle: const Text('Submit a new ticket to CX system'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => _go('Press Open Ticket Menu',
'go to open ticket menu', '/open-ticket'),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.person_add, color: Colors.blue),
title: const Text('Drop Form'),
subtitle: const Text('Identify a customer via cxIdentify'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => _go(
'Press Drop Form Menu', 'go to drop form menu', '/drop-form'),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.cookie_outlined, color: Colors.orange),
title: const Text('Clear Cookie'),
// เห็นค่า cookie เปลี่ยนได้ทันทีหลังกด = จุดสำคัญของเมนูนี้
subtitle: Text('now: ${ConnectXMobileSdk.instance.cookie}'),
trailing: const Icon(Icons.refresh, size: 20),
onTap: _clearCookie,
),
),
],
),
);
}
}
// --- SECOND PAGE: FORM ---
class OpenTicketPage extends StatefulWidget {
const OpenTicketPage({super.key});
@override
State<OpenTicketPage> createState() => _OpenTicketPageState();
}
class _OpenTicketPageState extends State<OpenTicketPage> {
final _nameController = TextEditingController();
final _email1Controller =
TextEditingController(text: 'tester.cnw001@gmail.com');
final _email2Controller = TextEditingController();
final _contentController = TextEditingController();
@override
void initState() {
super.initState();
track('Open Ticket Page', 'pageview');
}
void _submitTicket() async {
// เมื่อเรียกตัวนี้ cx_link จะได้ค่าเป็น "/open-ticket" โดยอัตโนมัติ
await track('Open Ticket from Mobile SDK', 'submit_ticket');
await ConnectXMobileSdk.cxOpenTicket({
'key': 'cx_Name',
'customers': {
'cx_Name': _nameController.text,
'cx_email': _email2Controller.text,
},
'ticket': {
'cx_subject': 'test email',
'email': {
'text': 'from mobile app',
'html': '<b>${_contentController.text}</b>'
},
},
'lead': {
'cx_email': 'xxxx@hotmail.com',
'cx_channel': 'test_connect_email',
},
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ticket submitted successfully!')),
);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Create Ticket")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
_buildTextField(_nameController, "Customer Name"),
_buildTextField(_email1Controller, "Email (Agent)"),
_buildTextField(_email2Controller, "Email (Customer)"),
_buildTextField(_contentController, "Message Content", maxLines: 4),
const SizedBox(height: 30),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(55),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
onPressed: _submitTicket,
child: const Text("SUBMIT TICKET"),
),
],
),
),
);
}
Widget _buildTextField(TextEditingController controller, String label,
{int maxLines = 1}) {
return Padding(
padding: const EdgeInsets.only(bottom: 15),
child: TextField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
labelText: label, border: const OutlineInputBorder()),
),
);
}
}
// --- THIRD PAGE: DROP FORM (cxIdentify) ---
class DropFormPage extends StatefulWidget {
const DropFormPage({super.key});
@override
State<DropFormPage> createState() => _DropFormPageState();
}
class _DropFormPageState extends State<DropFormPage> {
// key = "ชื่อ field" ที่ใช้ match ไม่ใช่ค่า — แก้ได้ในหน้าจอเพื่อลองเคส key ผิด
final _keyController = TextEditingController(text: 'cx_Name');
final _nameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
@override
void initState() {
super.initState();
track('Drop Form Page', 'pageview');
}
@override
void dispose() {
for (final c in [
_keyController,
_nameController,
_lastNameController,
_emailController,
_phoneController
]) {
c.dispose();
}
super.dispose();
}
Future<void> _submit() async {
final key = _keyController.text.trim();
final customers = {
'cx_Name': _nameController.text.trim(),
'cx_lastName': _lastNameController.text.trim(),
'cx_email': _emailController.text.trim(),
'cx_mobilePhone': _phoneController.text.trim(),
}..removeWhere((_, v) => v.isEmpty);
if (key.isEmpty || (customers[key] ?? '').isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('ต้องกรอกค่าของ field "$key" ที่ใช้เป็น key'),
backgroundColor: Colors.orange,
));
return;
}
try {
await track('Submit Drop Form', 'submit_drop_form');
await ConnectXMobileSdk.cxIdentify({'key': key, 'customers': customers});
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Identified ด้วย $key = ${customers[key]}'),
backgroundColor: Colors.green,
));
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed: $e'), backgroundColor: Colors.red),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Drop Form')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_field(_keyController, 'key (ชื่อ field ที่ใช้ match)'),
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(bottom: 15),
child: Text(
'ค่าของ field นี้ต้องเป็นค่าเดียวกันทุกครั้ง ไม่งั้นได้ customer ซ้ำ',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
),
),
_field(_nameController, 'cx_Name'),
_field(_lastNameController, 'cx_lastName'),
_field(_emailController, 'cx_email'),
_field(_phoneController, 'cx_mobilePhone'),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(55),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
onPressed: _submit,
child: const Text('CX IDENTIFY'),
),
],
),
),
);
}
Widget _field(TextEditingController controller, String label) => Padding(
padding: const EdgeInsets.only(bottom: 15),
child: TextField(
controller: controller,
decoration: InputDecoration(
labelText: label, border: const OutlineInputBorder()),
),
);
}