flutter_zoom_sdk_advanced 0.2.2
flutter_zoom_sdk_advanced: ^0.2.2 copied to clipboard
A Flutter plugin for Zoom Meeting SDK with support for joining meetings, email login, meeting controls, and real-time meeting events.
example/lib/main.dart
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_zoom_sdk_advanced/flutter_zoom_sdk_advanced.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Zoom SDK Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF2D8CFF),
useMaterial3: true,
brightness: Brightness.light,
),
home: const ZoomExamplePage(
// JWT token from server
jwtToken: '',
zakToken: null,
// USER_ZAK_TOKEN_FROM_BACKEND (For Zoom org users)
joinToken: "",
// USER_JOIN_TOKEN_FROM_BACKEND (For external registered users)
),
);
}
}
class ZoomExamplePage extends StatefulWidget {
final String jwtToken;
final String? zakToken;
final String? joinToken;
const ZoomExamplePage({
super.key,
required this.jwtToken,
this.zakToken,
this.joinToken,
});
@override
State<ZoomExamplePage> createState() => _ZoomExamplePageState();
}
class _ZoomExamplePageState extends State<ZoomExamplePage> {
final _zoomPlugin = FlutterZoomSdkAdvanced();
String _status = 'Initializing...';
bool _sdkInitialized = false;
bool _sdkInitFailed = false;
bool _joining = false;
StreamSubscription? _eventSubscription;
final _meetingIdController = TextEditingController(text: "");
final _meetingPasswordController = TextEditingController(text: "");
final _displayNameController = TextEditingController(text: 'Flutter User');
@override
void initState() {
super.initState();
_initSdk();
}
Future<void> _initSdk() async {
setState(() {
_status = 'Initializing...';
_sdkInitFailed = false;
});
final result = await _zoomPlugin.initZoom(ZoomOptions(
jwtToken: widget.jwtToken,
));
final success = result['status'] == true;
if (!mounted) return;
setState(() {
_sdkInitialized = success;
_sdkInitFailed = !success;
_status = success ? 'Ready' : result['message']?.toString() ?? 'Init failed';
});
if (success) {
_eventSubscription = _zoomPlugin.onMeetingStateChanged().listen((event) {
if (!mounted) return;
final status = event['status']?.toString() ?? '';
debugPrint('[ZoomSDK] Dart event: $status');
if (status.contains('ENDED') || status.contains('IDLE')) {
setState(() => _joining = false);
}
});
// These are global MobileRTCMeetingSettings toggles, not scoped to a
// live meeting — set them right after init, before joinMeeting(), so
// the meeting UI never builds the invite/passcode/share elements in
// the first place. iOS only; Android uses ZoomMeetingOptions fields
// (noInvite, hideMeetingId, hidePassword, disableShare) at join time
// instead.
if (Platform.isIOS) {
_zoomPlugin
.setInviteVisibility(hideInvite: true, hideInviteUrl: true)
.then((r) => debugPrint('[ZoomSDK] setInviteVisibility -> $r'));
_zoomPlugin
.setMeetingInfoVisibility(hidePassword: true)
.then((r) => debugPrint('[ZoomSDK] setMeetingInfoVisibility -> $r'));
_zoomPlugin
.setShareVisibility(true)
.then((r) => debugPrint('[ZoomSDK] setShareVisibility -> $r'));
_zoomPlugin
.setParticipantsVisibility(true)
.then((r) => debugPrint('[ZoomSDK] setParticipantsVisibility -> $r'));
}
}
}
Future<void> _joinMeeting() async {
final meetingId = _meetingIdController.text.trim();
final password = _meetingPasswordController.text.trim();
final name = _displayNameController.text.trim();
if (meetingId.isEmpty || name.isEmpty) {
_showSnackBar('Meeting ID and Display Name are required');
return;
}
final cameraStatus = await Permission.camera.request();
final micStatus = await Permission.microphone.request();
if (!cameraStatus.isGranted || !micStatus.isGranted) {
if (!mounted) return;
_showSnackBar('Camera and microphone permissions are required');
return;
}
setState(() => _joining = true);
debugPrint('[ZoomSDK] Dart joinMeeting - id: $meetingId, hasZak: ${widget.zakToken != null}, hasJoinToken: ${widget.joinToken != null}');
if (widget.joinToken != null) {
debugPrint('[ZoomSDK] joinToken length: ${widget.joinToken!.length}, starts with: ${widget.joinToken!.substring(0, widget.joinToken!.length > 20 ? 20 : widget.joinToken!.length)}...');
}
final result = await _zoomPlugin.joinMeeting(ZoomMeetingOptions(
meetingId: meetingId,
meetingPassword: password,
displayName: name,
zakToken: widget.zakToken,
joinToken: widget.joinToken,
noInvite: true,
hideMeetingId: true,
hidePassword: true,
disableShare: true,
hideParticipants: true,
));
debugPrint('[ZoomSDK] Dart join result: $result');
if (!mounted) return;
final success = result['status'] == true;
if (!success) {
setState(() => _joining = false);
_showSnackBar(result['message']?.toString() ?? 'Failed to join');
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
));
}
@override
void dispose() {
_eventSubscription?.cancel();
_meetingIdController.dispose();
_meetingPasswordController.dispose();
_displayNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo / Header
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.videocam_rounded,
size: 40,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
Text(
'Join Meeting',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_sdkInitialized
? 'Enter your meeting details below'
: _status,
style: theme.textTheme.bodyMedium?.copyWith(
color: _sdkInitFailed
? theme.colorScheme.error
: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
if (_sdkInitFailed) ...[
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: _initSdk,
icon: const Icon(Icons.refresh_rounded),
label: const Text('Retry'),
),
],
const SizedBox(height: 32),
// Form Card
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: theme.colorScheme.outlineVariant,
),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_buildTextField(
controller: _meetingIdController,
label: 'Meeting ID',
icon: Icons.tag_rounded,
keyboardType: TextInputType.number,
),
const SizedBox(height: 12),
_buildTextField(
controller: _meetingPasswordController,
label: 'Password',
icon: Icons.lock_outline_rounded,
obscureText: true,
),
const SizedBox(height: 12),
_buildTextField(
controller: _displayNameController,
label: 'Display Name',
icon: Icons.person_outline_rounded,
),
],
),
),
),
const SizedBox(height: 20),
// Join Button
SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
onPressed: _sdkInitialized && !_joining
? _joinMeeting
: null,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: _joining
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Join Meeting',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 32),
],
),
),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
TextInputType? keyboardType,
bool obscureText = false,
}) {
return TextField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerLowest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.5,
),
),
),
);
}
}