Add FCM push notifications, queue alerts, caller ID fixes, and auto-revert agent status
All checks were successful
Create Release / build (push) Successful in 6s

Server-side:
- Add push credential auto-creation for FCM incoming call notifications
- Add queue alert FCM notifications (data-only for background delivery)
- Add queue alert cancellation on call accept/disconnect
- Fix caller ID to show caller's number instead of Twilio number
- Fix FCM token storage when refresh_token is null
- Add pre_call_status tracking to revert agent status 30s after call ends
- Add SSE fallback polling for mobile app connectivity

Mobile app:
- Add Android telecom permissions and phone account registration
- Add VoiceFirebaseMessagingService for incoming call push handling
- Add insistent queue alert notifications with custom sound
- Fix caller number display on active call screen
- Add caller ID selection dropdown on dashboard
- Add phone numbers endpoint and provider support
- Add unit tests for CallInfo, QueueState, and CallProvider
- Remove local.properties from tracking, add .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-07 17:11:02 -08:00
parent 78e6c5a4ee
commit 4af4be94a4
26 changed files with 1829 additions and 189 deletions

View File

@@ -0,0 +1,129 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:twp_softphone/models/call_info.dart';
void main() {
group('CallInfo', () {
test('default state is idle', () {
const info = CallInfo();
expect(info.state, CallState.idle);
expect(info.callSid, isNull);
expect(info.callerNumber, isNull);
expect(info.duration, Duration.zero);
expect(info.isMuted, false);
expect(info.isSpeakerOn, false);
expect(info.isOnHold, false);
});
test('isActive returns true for ringing, connecting, connected', () {
expect(const CallInfo(state: CallState.ringing).isActive, true);
expect(const CallInfo(state: CallState.connecting).isActive, true);
expect(const CallInfo(state: CallState.connected).isActive, true);
});
test('isActive returns false for idle and disconnected', () {
expect(const CallInfo(state: CallState.idle).isActive, false);
expect(const CallInfo(state: CallState.disconnected).isActive, false);
});
test('copyWith preserves unmodified fields', () {
const original = CallInfo(
state: CallState.connected,
callSid: 'CA123',
callerNumber: '+1234567890',
isMuted: true,
);
final modified = original.copyWith(isSpeakerOn: true);
expect(modified.state, CallState.connected);
expect(modified.callSid, 'CA123');
expect(modified.callerNumber, '+1234567890');
expect(modified.isMuted, true);
expect(modified.isSpeakerOn, true);
});
test('copyWith can change state', () {
const info = CallInfo(state: CallState.connecting);
final updated = info.copyWith(state: CallState.connected);
expect(updated.state, CallState.connected);
});
test('copyWith with callerNumber preserves it', () {
const info = CallInfo(callerNumber: '+19095737372');
final updated = info.copyWith(state: CallState.connected);
expect(updated.callerNumber, '+19095737372');
});
test('reset to idle clears all fields', () {
// Verify a complex state exists
const connected = CallInfo(
state: CallState.connected,
callSid: 'CA123',
callerNumber: '+1234567890',
isMuted: true,
isSpeakerOn: true,
isOnHold: true,
duration: Duration(seconds: 30),
);
expect(connected.isActive, true);
// Simulating what callEnded does
const reset = CallInfo();
expect(reset.state, CallState.idle);
expect(reset.callSid, isNull);
expect(reset.callerNumber, isNull);
expect(reset.isActive, false);
});
});
group('CallState transitions', () {
test('outbound call flow: idle -> connecting -> connected -> idle', () {
var info = const CallInfo();
expect(info.state, CallState.idle);
// makeCall sets connecting + callerNumber
info = info.copyWith(state: CallState.connecting, callerNumber: '+19095737372');
expect(info.state, CallState.connecting);
expect(info.callerNumber, '+19095737372');
expect(info.isActive, true);
// SDK fires connected
info = info.copyWith(state: CallState.connected);
expect(info.state, CallState.connected);
expect(info.callerNumber, '+19095737372'); // preserved
expect(info.isActive, true);
// callEnded resets
info = const CallInfo();
expect(info.state, CallState.idle);
expect(info.isActive, false);
});
test('inbound call flow: idle -> ringing -> connected -> idle', () {
var info = const CallInfo();
info = info.copyWith(state: CallState.ringing);
expect(info.isActive, true);
// callerNumber set from active.from
info = info.copyWith(callerNumber: '+18005551234');
expect(info.callerNumber, '+18005551234');
info = info.copyWith(state: CallState.connected);
expect(info.state, CallState.connected);
info = const CallInfo();
expect(info.state, CallState.idle);
});
test('outbound callerNumber not overwritten by null copyWith', () {
var info = const CallInfo(
state: CallState.connecting,
callerNumber: '+19095737372',
);
// copyWith without callerNumber should preserve it
info = info.copyWith(state: CallState.connected);
expect(info.callerNumber, '+19095737372');
});
});
}