jasnita_call_sdk 0.1.3 copy "jasnita_call_sdk: ^0.1.3" to clipboard
jasnita_call_sdk: ^0.1.3 copied to clipboard

Flutter SDK for SIP-based voice calling with license key validation. Built on top of sip_ua for WebRTC-powered calls.

jasnita_call_sdk #

Flutter SDK untuk panggilan suara berbasis SIP/WebRTC, didukung oleh platform Jasnita.
Cukup sediakan license key — SDK menangani provisioning, koneksi, dan UI secara otomatis.


Daftar Isi #


Fitur #

  • License key — validasi otomatis via Jasnita API
  • Provisioning otomatis — SIP extension dibuat/diperbarui tanpa konfigurasi manual
  • Cache lokal — extension yang masih valid di-reuse tanpa re-provisioning
  • Panggilan keluar & masuk lewat WebRTC (SIP over WebSocket)
  • Mute, Hold, DTMF — kontrol panggilan, dapat dikonfigurasi per app
  • Theming — 4 preset warna bawaan + dukungan warna custom sepenuhnya
  • JasnitaCallScreen — layar telepon siap pakai, warna mengikuti tema
  • JasnitaIncomingCallOverlay — banner panggilan masuk di atas halaman manapun
  • JasnitaDialpad — dialpad dengan animasi tekan, sub-huruf, dan tombol hapus
  • Stream-based — tidak ada BLoC dependency, fleksibel untuk state management apapun

Instalasi #

Tambahkan ke pubspec.yaml project Anda:

dependencies:
  jasnita_call_sdk: ^0.1.1

Kemudian jalankan:

flutter pub get

Setup Platform #

Android #

Tambahkan permission berikut ke android/app/src/main/AndroidManifest.xml di dalam tag <manifest>:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

Pastikan minSdkVersion minimal 21 di android/app/build.gradle:

android {
    defaultConfig {
        minSdkVersion 21
    }
}

iOS #

Tambahkan permission mikrofon di ios/Runner/Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Aplikasi ini membutuhkan akses mikrofon untuk melakukan panggilan suara.</string>
<key>NSCameraUsageDescription</key>
<string>Aplikasi ini membutuhkan akses kamera untuk panggilan video.</string>

Quick Start #

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jasnita_call_sdk/jasnita_call_sdk.dart';

class MyPage extends StatefulWidget { ... }

class _MyPageState extends State<MyPage> {
  final _controller = CallController();

  @override
  void initState() {
    super.initState();
    // Auto-initialize saat layar pertama terbuka
    WidgetsBinding.instance.addPostFrameCallback((_) => _init());

    _controller.stateStream.listen((state) {
      if (state.status == CallStatus.incoming) {
        // ada panggilan masuk
      }
    });
  }

  Future<void> _init() async {
    await _controller.initialize(JasnitaCallConfig(
      licenseKey: 'YOUR_LICENSE_KEY',
      baseUrl:    'https://robocall.omnicode.id',
      phone:      '08123456789',
      name:       'Nama Pengguna',
      showMute:   true,
      showHold:   true,
    ));
  }

  Future<void> _makeCall(String number) async {
    // Buka layar SEBELUM call() agar event dialing tidak terlewat
    unawaited(Navigator.push(context, MaterialPageRoute(
      builder: (_) => JasnitaCallScreen(
        controller: _controller,
        theme: JasnitaCallTheme.lightBlue,
      ),
    )));
    await _controller.call(number);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Cara Kerja SDK #

initialize()
    │
    ├─► Minta permission mikrofon (runtime)
    ├─► ApiService.createExtension()   ← validasi license key ke server
    │       └─► cache ke SharedPreferences
    │
    └─► SipManager.connect()
            │
            ├─► WebSocket transport terhubung
            ├─► countdown (default 5 detik)
            └─► SIP REGISTER
                    └─► status: registered ✓

Setelah registered, SDK siap menerima/melakukan panggilan.
Jika koneksi terputus, panggil reconnect() untuk daftar ulang tanpa re-provisioning.


JasnitaCallConfig #

Semua opsi konfigurasi saat memanggil initialize():

await controller.initialize(JasnitaCallConfig(
  // ── Wajib ──────────────────────────────────────────────────────
  licenseKey:    'YOUR_LICENSE_KEY',      // license key dari Jasnita
  baseUrl:       'https://...',           // URL API Jasnita
  phone:         '08123456789',           // nomor telepon pengguna
  name:          'John Doe',              // nama tampilan ke penerima

  // ── Opsional ───────────────────────────────────────────────────
  registerDelay: 5,                       // detik tunggu sebelum register (default: 5)
  showMute:      false,                   // tampilkan tombol Mute di layar aktif (default: false)
  showHold:      false,                   // tampilkan tombol Hold di layar aktif (default: false)
  customerId:    'CUST-001',              // customer ID — dikirim otomatis sebagai SIP header
));

Tabel Parameter #

Parameter Tipe Default Keterangan
licenseKey String License key dari Jasnita (wajib)
baseUrl String URL API Jasnita (wajib)
phone String Nomor telepon pengguna (wajib)
name String Nama tampilan ke penerima (wajib)
registerDelay int 5 Detik tunggu setelah WebSocket connect sebelum REGISTER
showMute bool false Tampilkan tombol Mute pada layar panggilan aktif
showHold bool false Tampilkan tombol Hold pada layar panggilan aktif
customerId String? null Customer identifier — otomatis dikirim sebagai X-Customer-ID SIP header di setiap panggilan keluar

Catatan: Tombol Speaker dan Dialpad (DTMF) selalu tampil dan tidak dapat disembunyikan.


CallController API #

Metode Panggilan #

Metode Keterangan
call(destination, {extraHeaders}) Panggil nomor/extension. Bisa berupa '1000' atau 'sip:1000@domain.com'
answer() Angkat panggilan masuk
hangup() Tutup panggilan / tolak panggilan masuk
setMuted(bool) true = bisukan mikrofon, false = aktifkan kembali
setHold(bool) true = tahan panggilan, false = lanjutkan
setSpeakerOn(bool) true = speaker, false = earpiece
sendDtmf(String) Kirim nada DTMF: '0''9', '*', '#'

Manajemen Koneksi #

Metode Keterangan
reconnect() Daftar ulang SIP setelah koneksi terputus
dispose() Putuskan koneksi dan bebaskan semua resource

Getter #

Getter Tipe Keterangan
stateStream Stream<CallState> Stream state panggilan secara real-time
currentState CallState State panggilan saat ini (tanpa subscribe)
showMute bool Apakah tombol Mute dikonfigurasi untuk tampil
showHold bool Apakah tombol Hold dikonfigurasi untuk tampil

Stream State #

StreamBuilder<CallState>(
  stream: controller.stateStream,
  initialData: controller.currentState,
  builder: (context, snapshot) {
    final state = snapshot.data!;
    return Text(state.status.name);
  },
)

CallState & CallStatus #

CallStatus #

idle            → SDK belum diinisialisasi / sudah di-dispose
connecting      → Menghubungkan ke server / sedang registrasi
readyToRegister → Transport terhubung, menunggu countdown sebelum register
registered      → Siap melakukan/menerima panggilan ✓
dialing         → Panggilan keluar sedang dikirim
ringing         → Pihak lawan sedang berdering
incoming        → Ada panggilan masuk yang menunggu
active          → Panggilan aktif (audio dua arah)
ended           → Panggilan selesai
error           → Terjadi kesalahan (lihat errorMessage)

CallState #

class CallState {
  final CallStatus status;

  // Tersedia saat dialing/ringing/incoming/active
  final String? remoteParty;      // nama atau nomor lawan bicara

  // Tersedia saat status == active
  final Duration callDuration;    // durasi panggilan berjalan
  final bool isMuted;             // apakah mikrofon dimatikan
  final bool isOnHold;            // apakah panggilan ditahan

  // Tersedia saat status == readyToRegister
  final int registerCountdown;    // detik tersisa sebelum auto-register

  // Tersedia saat status == error
  final String? errorMessage;

  // Tersedia saat status == ended
  final String? endCause;
}

Theming #

SDK menyediakan class JasnitaCallTheme untuk mengubah tampilan seluruh layar panggilan dan banner notifikasi. Theme diterapkan ke JasnitaCallScreen dan JasnitaIncomingCallOverlay.

Preset Bawaan #

Preset Tampilan
JasnitaCallTheme.dark Navy gelap — default Jasnita
JasnitaCallTheme.lightBlue Biru muda — airy & modern
JasnitaCallTheme.light Putih bersih — minimal
JasnitaCallTheme.teal Teal gelap — profesional
// Dark (default)
JasnitaCallScreen(controller: _controller)

// Light blue
JasnitaCallScreen(
  controller: _controller,
  theme: JasnitaCallTheme.lightBlue,
)

// Putih bersih
JasnitaCallScreen(
  controller: _controller,
  theme: JasnitaCallTheme.light,
)

// Teal gelap
JasnitaCallScreen(
  controller: _controller,
  theme: JasnitaCallTheme.teal,
)

Warna Custom #

Buat theme sendiri dengan mengisi semua parameter warna:

const myTheme = JasnitaCallTheme(
  backgroundColor:    Color(0xFF2D1B4E),   // latar belakang layar
  textPrimary:        Colors.white,         // teks utama (nama caller)
  textSecondary:      Color(0xB3FFFFFF),    // teks sekunder (subtitle, label tombol)
  controlButtonColor: Color(0x3DA855F7),    // tombol kontrol saat tidak aktif
  controlActiveColor: Color(0xFFA855F7),    // tombol kontrol saat aktif (mute/hold/speaker)
  answerColor:        Color(0xFF22C55E),    // tombol angkat
  hangupColor:        Color(0xFFEF4444),    // tombol tutup/tolak
);

JasnitaCallScreen(
  controller: _controller,
  theme: myTheme,
)

Referensi JasnitaCallTheme #

Field Keterangan
backgroundColor Warna latar layar dan bottom sheet DTMF
textPrimary Warna teks utama — nama caller, teks input
textSecondary Warna teks sekunder — subtitle, label tombol, hint
controlButtonColor Warna tombol Mute/Hold/Speaker/Dialpad saat tidak aktif
controlActiveColor Warna tombol saat aktif/toggle (muted, on-hold, speaker on)
answerColor Warna tombol Angkat
hangupColor Warna tombol Tutup / Tolak / Batalkan

Widgets #

JasnitaCallScreen #

Layar panggilan penuh yang menangani semua status: incoming, dialing, ringing, active, ended.
Secara otomatis pop ketika panggilan selesai dan SDK kembali ke registered.

// Buka layar SEBELUM memanggil call() agar tidak melewatkan event dialing
unawaited(Navigator.push(context, MaterialPageRoute(
  builder: (_) => JasnitaCallScreen(
    controller: _controller,
    theme: JasnitaCallTheme.lightBlue,  // opsional, default: JasnitaCallTheme.dark
  ),
)));
await _controller.call('1000');

Tampilan per status:

Status Kontrol yang muncul
incoming Tombol Angkat + Tolak
dialing / ringing Tombol Batalkan
active Mute*, Hold*, Speaker, Dialpad (DTMF), Tutup
ended Layar menutup otomatis

* Hanya muncul jika showMute: true / showHold: true di JasnitaCallConfig.


JasnitaIncomingCallOverlay #

Banner di atas layar yang muncul saat ada panggilan masuk. Bungkus widget utama Anda agar notifikasi muncul di halaman manapun.

@override
Widget build(BuildContext context) {
  return JasnitaIncomingCallOverlay(
    controller: _controller,
    theme: JasnitaCallTheme.lightBlue,          // opsional, default: dark
    onCallScreenBuilder: (context) => JasnitaCallScreen(
      controller: _controller,
      theme: JasnitaCallTheme.lightBlue,
    ),
    child: Scaffold(
      body: MyHomePage(),
    ),
  );
}

Gunakan theme yang sama pada JasnitaIncomingCallOverlay dan JasnitaCallScreen agar tampilan konsisten.


JasnitaDialpad #

Dialpad telepon dengan animasi tekan, sub-huruf (ABC, DEF, …), dan tombol hapus opsional.

JasnitaDialpad(
  onDigit: (digit) => setState(() => _number += digit),
  onDelete: () => setState(() {
    if (_number.isNotEmpty) {
      _number = _number.substring(0, _number.length - 1);
    }
  }),
  keySize:         72,                     // opsional, default 72
  keyColor:        Color(0xFF2A2A3E),      // opsional
  keyPressedColor: Color(0xFF4A4A6E),      // opsional — warna saat ditekan
  textColor:       Colors.white,           // opsional
)

Jika onDelete tidak disediakan, tombol hapus tidak akan ditampilkan.


Extra Headers #

Kirim SIP header tambahan pada setiap panggilan keluar menggunakan parameter extraHeaders di call().
Header ini akan di-merge dengan header otomatis dari config (X-Customer-ID, X-Transaction-ID).

await _controller.call(
  '1000',
  extraHeaders: {
    'X-Agent-ID':    'AGT-999',
    'X-Campaign-ID': 'CAMP-2025',
  },
);

Header Otomatis (saat customerId diset) #

Jika customerId diisi di JasnitaCallConfig, dua header berikut otomatis dikirim di setiap panggilan keluar — tanpa perlu menulis manual:

Header Contoh Nilai Keterangan
X-Customer-ID CUST-001 Nilai dari customerId di config
X-Transaction-ID TRX-1746123456789 Di-generate otomatis tiap panggilan (timestamp ms)

Header dari extraHeaders yang Anda kirim akan menimpa header otomatis jika namanya sama.


Error Handling #

try {
  await controller.initialize(config);
} on JasnitaPermissionException catch (e) {
  // Izin mikrofon ditolak pengguna
  print(e.message);
} on JasnitaLicenseException catch (e) {
  // License key tidak valid atau sudah kedaluwarsa
  print(e.message);
} on JasnitaApiException catch (e) {
  // Kesalahan API lainnya (network, server error)
  print(e.message);
}

Pantau juga error koneksi SIP lewat stream:

controller.stateStream.listen((state) {
  if (state.status == CallStatus.error) {
    print('SIP error: ${state.errorMessage}');
    controller.reconnect();
  }
});

Contoh Lengkap #

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jasnita_call_sdk/jasnita_call_sdk.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Jasnita Call SDK Demo',
      theme: ThemeData.dark(useMaterial3: true),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // ── Konfigurasi SDK — ubah di sini ─────────────────────────────────────────
  static const _config = JasnitaCallConfig(
    licenseKey: 'YOUR_LICENSE_KEY',
    baseUrl:    'https://robocall.omnicode.id',
    phone:      '08123456789',
    name:       'Demo User',
    showMute:   true,
    showHold:   true,
  );

  // ── Tema tampilan — pilih preset atau buat custom ───────────────────────────
  // Preset: dark | lightBlue | light | teal
  // Custom: JasnitaCallTheme(backgroundColor: ..., textPrimary: ..., ...)
  static const _theme = JasnitaCallTheme.dark;

  // ───────────────────────────────────────────────────────────────────────────

  final _controller = CallController();
  final _numberController = TextEditingController();

  CallState _callState = CallState.idle;
  bool _initializing = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _controller.stateStream.listen((s) {
      if (mounted) setState(() => _callState = s);
    });
    _numberController.addListener(() => setState(() {}));
    // Auto-initialize saat layar pertama terbuka.
    WidgetsBinding.instance.addPostFrameCallback((_) => _initialize());
  }

  Future<void> _initialize() async {
    setState(() {
      _initializing = true;
      _error = null;
    });
    try {
      await _controller.initialize(_config);
    } on JasnitaPermissionException catch (e) {
      setState(() => _error = 'Permission: ${e.message}');
    } on JasnitaLicenseException catch (e) {
      setState(() => _error = 'License error: ${e.message}');
    } on JasnitaApiException catch (e) {
      setState(() => _error = 'API error: ${e.message}');
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _initializing = false);
    }
  }

  Future<void> _call() async {
    final number = _numberController.text;
    if (number.isEmpty || !mounted) return;
    unawaited(Navigator.of(context).push(MaterialPageRoute(
      builder: (_) => JasnitaCallScreen(controller: _controller, theme: _theme),
    )));
    await _controller.call(number);
  }

  void _appendDigit(String d) {
    final newText = _numberController.text + d;
    _numberController.value = TextEditingValue(
      text: newText,
      selection: TextSelection.collapsed(offset: newText.length),
    );
  }

  void _deleteDigit() {
    final t = _numberController.text;
    if (t.isEmpty) return;
    final newText = t.substring(0, t.length - 1);
    _numberController.value = TextEditingValue(
      text: newText,
      selection: TextSelection.collapsed(offset: newText.length),
    );
  }

  @override
  void dispose() {
    _numberController.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final status = _callState.status;
    final canCall =
        status == CallStatus.registered && _numberController.text.isNotEmpty;

    return JasnitaIncomingCallOverlay(
      controller: _controller,
      theme: _theme,
      onCallScreenBuilder: (_) =>
          JasnitaCallScreen(controller: _controller, theme: _theme),
      child: Scaffold(
        backgroundColor: const Color(0xFF1A1A2E),
        appBar: AppBar(
          backgroundColor: const Color(0xFF1A1A2E),
          title: const Text('Jasnita Call SDK'),
          centerTitle: true,
        ),
        body: SafeArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 24),
            child: Column(
              children: [
                const SizedBox(height: 12),
                _StatusBadge(status: status),
                const SizedBox(height: 12),
                if (_error != null) ...[
                  Container(
                    padding: const EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: Colors.red.withValues(alpha: 0.15),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(_error!,
                        style: const TextStyle(color: Colors.red, fontSize: 13)),
                  ),
                  const SizedBox(height: 8),
                ],
                ElevatedButton(
                  onPressed: _initializing ? null : _initialize,
                  style: ElevatedButton.styleFrom(
                    minimumSize: const Size.fromHeight(44),
                  ),
                  child: _initializing
                      ? const SizedBox(
                          height: 18,
                          width: 18,
                          child: CircularProgressIndicator(strokeWidth: 2))
                      : const Text('Initialize SDK'),
                ),
                const SizedBox(height: 24),
                // Input nomor
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 20, vertical: 4),
                  decoration: BoxDecoration(
                    color: Colors.white.withValues(alpha: 0.06),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: TextField(
                    controller: _numberController,
                    keyboardType: TextInputType.phone,
                    textAlign: TextAlign.center,
                    cursorColor: Colors.white54,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                      fontWeight: FontWeight.w300,
                      letterSpacing: 2,
                    ),
                    decoration: const InputDecoration(
                      border: InputBorder.none,
                      hintText: 'Masukkan nomor...',
                      hintStyle: TextStyle(
                        color: Colors.white30,
                        fontSize: 24,
                        fontWeight: FontWeight.w300,
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 20),
                JasnitaDialpad(
                  onDigit: _appendDigit,
                  onDelete: _deleteDigit,
                  keySize: 72,
                ),
                const SizedBox(height: 20),
                // Tombol call
                SizedBox(
                  width: 72,
                  height: 72,
                  child: Material(
                    color: canCall ? Colors.green : Colors.grey.shade800,
                    shape: const CircleBorder(),
                    child: InkWell(
                      customBorder: const CircleBorder(),
                      onTap: canCall ? _call : null,
                      child: const Icon(Icons.call,
                          color: Colors.white, size: 32),
                    ),
                  ),
                ),
                const SizedBox(height: 16),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Lihat juga folder example/ untuk kode demo lengkap yang siap dijalankan.


FAQ #

Q: Apakah perlu konfigurasi SIP server sendiri?
A: Tidak. License key Anda sudah terhubung ke server Jasnita. SDK mengurus provisioning extension SIP secara otomatis.

Q: Kenapa tombol Mute dan Hold tidak muncul secara default?
A: Fitur tersebut disembunyikan secara default agar tampilan layar panggilan lebih bersih. Aktifkan dengan showMute: true dan/atau showHold: true di JasnitaCallConfig.

Q: Bagaimana cara mengganti tema warna?
A: Gunakan parameter theme di JasnitaCallScreen dan JasnitaIncomingCallOverlay. Tersedia 4 preset (dark, lightBlue, light, teal) dan dukungan warna custom penuh via konstruktor JasnitaCallTheme(...).

Q: Bagaimana jika koneksi internet terputus saat panggilan?
A: Panggilan akan berakhir dengan status: ended. Panggil reconnect() untuk mendaftar ulang setelah koneksi pulih.

Q: Apakah extension SIP di-cache?
A: SDK saat ini selalu re-provision setiap initialize() untuk memastikan credentials terbaru. Cache lokal disimpan sebagai fallback.

Q: Bisakah dipakai tanpa widget bawaan?
A: Ya. Cukup gunakan CallController dan stateStream tanpa menggunakan JasnitaCallScreen atau JasnitaIncomingCallOverlay.

Q: Berapa banyak CallController yang bisa dibuat?
A: Satu instance per sesi pengguna. Pastikan dispose() dipanggil di State.dispose() untuk mencegah memory leak.


License #

Proprietary — Jasnita © 2025.
Memerlukan license key yang valid. Hubungi jasnita.com untuk mendapatkan akses.

0
likes
160
points
22
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for SIP-based voice calling with license key validation. Built on top of sip_ua for WebRTC-powered calls.

Homepage

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_webrtc, http, permission_handler, shared_preferences, sip_ua

More

Packages that depend on jasnita_call_sdk