forked from eatvio/eatvio_app
Compare commits
3 Commits
fix/testfl
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 194b8d5b16 | |||
|
|
508c321913 | ||
|
|
4a5c67632e |
@@ -437,73 +437,21 @@ import flutter_foreground_task
|
||||
return payload
|
||||
}
|
||||
|
||||
private func bringAppToForeground() {
|
||||
DispatchQueue.main.async {
|
||||
for scene in UIApplication.shared.connectedScenes {
|
||||
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||
for window in windowScene.windows {
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
self.window?.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
private func openAppForAcceptedCall(_ call: Call) {
|
||||
guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else {
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_missing_conversation_uuid",
|
||||
data: ["callKitId": call.data.uuid]
|
||||
)
|
||||
/// The answer callback runs before the scene is visible. A custom-scheme
|
||||
/// open from here is an untrusted user action and SpringBoard denies it.
|
||||
/// CallKit foregrounds a video call itself after `fulfill`. Flutter reads
|
||||
/// the stored payload once that scene is actually active.
|
||||
func deliverStoredAcceptedCall() {
|
||||
guard voipDiagnosticsChannel != nil else { return }
|
||||
guard let payload = UserDefaults.standard.dictionary(forKey: pendingAcceptedVoipCallKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
var components = URLComponents()
|
||||
components.scheme = appUrlScheme
|
||||
components.host = "video-call"
|
||||
var queryItems = [
|
||||
URLQueryItem(name: "callKitId", value: call.data.uuid),
|
||||
URLQueryItem(name: "conversationUuid", value: conversationUuid)
|
||||
]
|
||||
let serverCallId = extraServerCallId(call.data.extra)
|
||||
if !serverCallId.isEmpty {
|
||||
queryItems.append(URLQueryItem(name: "call_id", value: serverCallId))
|
||||
}
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_url_build_failed",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
]
|
||||
)
|
||||
let acceptedAt = (payload[pendingAcceptedAtKey] as? NSNumber)?.doubleValue
|
||||
if let acceptedAt, Date().timeIntervalSince1970 - acceptedAt > pendingAcceptedCallTtl {
|
||||
UserDefaults.standard.removeObject(forKey: pendingAcceptedVoipCallKey)
|
||||
return
|
||||
}
|
||||
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_open_app_requested",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
"appState": UIApplication.shared.applicationState.rawValue,
|
||||
]
|
||||
)
|
||||
|
||||
bringAppToForeground()
|
||||
|
||||
UIApplication.shared.open(url, options: [:]) { [weak self] success in
|
||||
self?.recordVoipDiagnostic(
|
||||
success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
]
|
||||
)
|
||||
}
|
||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: payload)
|
||||
}
|
||||
|
||||
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
|
||||
@@ -1177,6 +1125,8 @@ import flutter_foreground_task
|
||||
// activation and can start I/O before didActivate.
|
||||
prepareAvAudioSessionCategory()
|
||||
storePendingAcceptedCall(call)
|
||||
let extra = call.data.extra as? [String: Any] ?? [:]
|
||||
VideoCallDeclineClient.sendAccept(extra: extra, handle: call.data.handle)
|
||||
recordVoipDiagnostic(
|
||||
"callkit_accept_native",
|
||||
data: [
|
||||
@@ -1185,11 +1135,7 @@ import flutter_foreground_task
|
||||
"data": call.data.toJSON(),
|
||||
]
|
||||
)
|
||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON())
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.bringAppToForeground()
|
||||
self?.openAppForAcceptedCall(call)
|
||||
}
|
||||
deliverStoredAcceptedCall()
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ class SceneDelegate: FlutterSceneDelegate {
|
||||
override func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
super.sceneDidBecomeActive(scene)
|
||||
registerCallChannels()
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
|
||||
return
|
||||
}
|
||||
appDelegate.deliverStoredAcceptedCall()
|
||||
}
|
||||
|
||||
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||
|
||||
@@ -8,25 +8,37 @@ enum VideoCallDeclineClient {
|
||||
/// `POST /video-call/decline` from the CallKit callback. The process is
|
||||
/// awake here even when the user never opens the app afterwards.
|
||||
static func send(extra: [String: Any], handle: String) {
|
||||
post(action: "decline", extra: extra, handle: handle)
|
||||
}
|
||||
|
||||
/// `POST /video-call/accept` from the CallKit answer callback. Flutter may
|
||||
/// still be frozen on the splash snapshot, so the other device must be
|
||||
/// told from here. A later Dart retry with the same hw_id is idempotent.
|
||||
static func sendAccept(extra: [String: Any], handle: String) {
|
||||
post(action: "accept", extra: extra, handle: handle)
|
||||
}
|
||||
|
||||
private static func post(action: String, extra: [String: Any], handle: String) {
|
||||
let fields = declineFields(extra: extra, handle: handle)
|
||||
guard let request = makeRequest(
|
||||
baseUrl: prefsString("apiBaseUrl") ?? prefsString("customBaseUrl"),
|
||||
token: prefsString("token"),
|
||||
hwId: UIDevice.current.identifierForVendor?.uuidString,
|
||||
fields: fields
|
||||
fields: fields,
|
||||
action: action
|
||||
) else {
|
||||
NSLog(
|
||||
"[EatvioVoIP] decline POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
||||
"[EatvioVoIP] \(action) POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let task = EatvioBackgroundTask()
|
||||
task.begin(name: "video-call-decline")
|
||||
task.begin(name: "video-call-\(action)")
|
||||
let urlTask = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
NSLog(
|
||||
"[EatvioVoIP] decline POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
||||
"[EatvioVoIP] \(action) POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
||||
)
|
||||
task.end()
|
||||
}
|
||||
@@ -37,7 +49,8 @@ enum VideoCallDeclineClient {
|
||||
baseUrl: String?,
|
||||
token: String?,
|
||||
hwId: String?,
|
||||
fields: VideoCallDeclineFields
|
||||
fields: VideoCallDeclineFields,
|
||||
action: String = "decline"
|
||||
) -> URLRequest? {
|
||||
if fields.endedPush { return nil }
|
||||
let conversation = fields.conversationUuid.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -50,8 +63,9 @@ enum VideoCallDeclineClient {
|
||||
guard !conversation.isEmpty, !authToken.isEmpty else { return nil }
|
||||
guard deviceId.count >= 8, deviceId.count <= 64 else { return nil }
|
||||
guard root.hasPrefix("http://") || root.hasPrefix("https://") else { return nil }
|
||||
guard action == "accept" || action == "decline" else { return nil }
|
||||
guard let encoded = conversation.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
||||
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/decline")
|
||||
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/\(action)")
|
||||
else { return nil }
|
||||
|
||||
var body: [String: String] = ["hw_id": deviceId]
|
||||
|
||||
@@ -43,60 +43,37 @@ platform :ios do
|
||||
end
|
||||
|
||||
UI.message "Using auth_key_path: #{auth_key_path}"
|
||||
|
||||
# Authenticate to App Store Connect early:
|
||||
# needed here by latest_testflight_build_number, not only by upload_to_testflight.
|
||||
app_store_connect_api_key(
|
||||
key_id: "325JS9JCZH",
|
||||
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
|
||||
key_filepath: auth_key_path,
|
||||
duration: 1200,
|
||||
in_house: false
|
||||
)
|
||||
|
||||
|
||||
# Configure Flutter for release mode
|
||||
UI.message "Configuring Flutter for release mode..."
|
||||
sh("cd .. && flutter build ios --config-only --release")
|
||||
|
||||
# Read version and build number from Flutter Generated.xcconfig.
|
||||
# They are only a floor: the actual build number is derived from TestFlight below,
|
||||
# so repeated CI runs on the same commit can never hit
|
||||
# ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE ("bundle version must be higher").
|
||||
# Read build number from Flutter Generated.xcconfig
|
||||
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
|
||||
flutter_build_name = nil
|
||||
flutter_build_number = nil
|
||||
|
||||
|
||||
if File.exist?(generated_xcconfig_path)
|
||||
File.foreach(generated_xcconfig_path) do |line|
|
||||
if (m = line.match(/FLUTTER_BUILD_NAME=(.*)/))
|
||||
flutter_build_name = m[1].strip
|
||||
elsif (m = line.match(/FLUTTER_BUILD_NUMBER=(.*)/))
|
||||
flutter_build_number = m[1].strip
|
||||
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
|
||||
if matches
|
||||
flutter_build_number = matches[1].strip
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if flutter_build_name.nil? || flutter_build_number.nil?
|
||||
UI.user_error!("Could not find FLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
|
||||
|
||||
if flutter_build_number.nil?
|
||||
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
|
||||
end
|
||||
|
||||
UI.message "Flutter version from pubspec: #{flutter_build_name} (#{flutter_build_number})"
|
||||
|
||||
asc_latest_build_number = latest_testflight_build_number(
|
||||
app_identifier: "com.eatvioapp.app",
|
||||
version: flutter_build_name,
|
||||
initial_build_number: flutter_build_number.to_i
|
||||
)
|
||||
|
||||
new_build_number = [flutter_build_number.to_i, asc_latest_build_number].max + 1
|
||||
UI.message "Latest TestFlight build number: #{asc_latest_build_number} -> uploading build #{new_build_number}"
|
||||
|
||||
|
||||
UI.message "Flutter build number: #{flutter_build_number}"
|
||||
|
||||
match(type: "appstore", readonly: true)
|
||||
|
||||
|
||||
# Set build number explicitly instead of incrementing
|
||||
increment_build_number(
|
||||
xcodeproj: "Runner.xcodeproj",
|
||||
build_number: new_build_number
|
||||
build_number: flutter_build_number
|
||||
)
|
||||
|
||||
update_code_signing_settings(
|
||||
@@ -116,13 +93,22 @@ platform :ios do
|
||||
export_team_id: "VL68C36HYS"
|
||||
)
|
||||
|
||||
app_store_connect_api_key(
|
||||
key_id: "325JS9JCZH",
|
||||
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
|
||||
key_filepath: auth_key_path,
|
||||
duration: 1200,
|
||||
in_house: false
|
||||
)
|
||||
|
||||
|
||||
begin
|
||||
upload_to_testflight(
|
||||
skip_waiting_for_build_processing: true,
|
||||
skip_submission: true,
|
||||
distribute_external: false,
|
||||
notify_external_testers: false,
|
||||
changelog: "TestFlight build #{new_build_number}",
|
||||
changelog: "TestFlight build #{flutter_build_number}",
|
||||
verbose: true
|
||||
)
|
||||
rescue => ex
|
||||
|
||||
@@ -52,6 +52,7 @@ class VideoCallCallKitCoordinator {
|
||||
|
||||
_PendingIncomingAccept? _pendingIncomingAccept;
|
||||
_AndroidAcceptResumeObserver? _androidResumeObserver;
|
||||
_AppResumeObserver? _iosResumeObserver;
|
||||
bool _pendingAcceptDrainScheduled = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
@@ -86,6 +87,12 @@ class VideoCallCallKitCoordinator {
|
||||
}
|
||||
});
|
||||
await _firebaseApi.registerPushToken();
|
||||
_iosResumeObserver = _AppResumeObserver(
|
||||
onResumed: () {
|
||||
unawaited(_drainPendingNativeAcceptedVoipCall());
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_iosResumeObserver!);
|
||||
await _drainPendingNativeAcceptedVoipCall();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scheduleNativeVoipDiagnosticsDrain();
|
||||
@@ -257,6 +264,11 @@ class VideoCallCallKitCoordinator {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
_androidResumeObserver = null;
|
||||
}
|
||||
final iosObserver = _iosResumeObserver;
|
||||
if (iosObserver != null) {
|
||||
WidgetsBinding.instance.removeObserver(iosObserver);
|
||||
_iosResumeObserver = null;
|
||||
}
|
||||
unawaited(_eventSubscription?.cancel());
|
||||
_eventSubscription = null;
|
||||
_callKitParamsById.clear();
|
||||
@@ -1228,6 +1240,19 @@ class _PendingIncomingAccept {
|
||||
final bool? counterpartIsConsultant;
|
||||
}
|
||||
|
||||
class _AppResumeObserver with WidgetsBindingObserver {
|
||||
_AppResumeObserver({required this.onResumed});
|
||||
|
||||
final VoidCallback onResumed;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
onResumed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidAcceptResumeObserver with WidgetsBindingObserver {
|
||||
_AndroidAcceptResumeObserver({required this.onResumed});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.68+157
|
||||
version: 1.3.69+158
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user