jasnita_call_sdk 0.1.3
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
- Instalasi
- Setup Platform
- Quick Start
- Cara Kerja SDK
- JasnitaCallConfig
- CallController API
- CallState & CallStatus
- Theming
- Widgets
- Extra Headers
- Error Handling
- Contoh Lengkap
- FAQ
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 temaJasnitaIncomingCallOverlay— banner panggilan masuk di atas halaman manapunJasnitaDialpad— 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
JasnitaIncomingCallOverlaydanJasnitaCallScreenagar 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
onDeletetidak 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
extraHeadersyang 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.