connect_x_mobile_sdk 1.1.12 copy "connect_x_mobile_sdk: ^1.1.12" to clipboard
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()),
        ),
      );
}