diff --git a/packages/firebase_crashlytics_tvos/CHANGELOG.md b/packages/firebase_crashlytics_tvos/CHANGELOG.md index 74be135..fda491b 100644 --- a/packages/firebase_crashlytics_tvos/CHANGELOG.md +++ b/packages/firebase_crashlytics_tvos/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.0.2 + +* Re-ported from `firebase_crashlytics` 5.3.0 (Swift + Pigeon). With 0.0.1, + every call failed with `channel-error` on 5.3.0. +* `pluginClass` is now `FirebaseCrashlyticsPlugin`. +* Requires `firebase_crashlytics` `^5.3.0` and `firebase_core_tvos` `^0.0.3`. +* Registers with Firebase as upstream's `flutter-fire-cls` instead of + `flutter-fire-cls-tvos`, like the other Swift ports. + ## 0.0.1 * Initial tvOS (Apple TV) implementation of `firebase_crashlytics`, ported from diff --git a/packages/firebase_crashlytics_tvos/PORTING_REPORT.md b/packages/firebase_crashlytics_tvos/PORTING_REPORT.md index 15ef1db..d94fbc2 100644 --- a/packages/firebase_crashlytics_tvos/PORTING_REPORT.md +++ b/packages/firebase_crashlytics_tvos/PORTING_REPORT.md @@ -127,3 +127,19 @@ produced. **Crash-capture limits.** Per `FIRCLSFeatures.h`, tvOS captures ordinary crashes (signal + `NSException` handlers) but has no alternate signal stack (stack-overflow may be missed) and no Mach exception server. + +## 0.0.2: re-port to firebase_crashlytics 5.3.0 + +5.3.0 moved the Apple plugin to Swift + Pigeon, so 0.0.1's MethodChannel +handler was never reached (`channel-error` on every call). `Classes/` is now +upstream 5.3.0's `ios/firebase_crashlytics/Sources/`, verbatim except: + +- `FirebaseCrashlyticsPlugin.swift`: imports `firebase_core_tvos`; the + `registrar.messenger()` branch also covers `os(tvOS)`. +- `FirebaseCrashlyticsMessages.g.swift`: `import Flutter` also covers `os(tvOS)`. +- Upstream's Swift and ObjC SPM targets are one pod (`DEFINES_MODULE`). +- Podspec: `Firebase/Crashlytics ~> 12.15.0` to match `firebase_core_tvos` + (upstream uses 12.18.0). + +**Verified** on the tvOS 27.0 simulator: every API call returns, and a forced +`crash()` is detected on relaunch. Not yet checked on a physical Apple TV. diff --git a/packages/firebase_crashlytics_tvos/README.md b/packages/firebase_crashlytics_tvos/README.md index e19bdea..46044bd 100644 --- a/packages/firebase_crashlytics_tvos/README.md +++ b/packages/firebase_crashlytics_tvos/README.md @@ -3,7 +3,7 @@ The tvOS implementation of [`firebase_crashlytics`](https://pub.dev/packages/firebase_crashlytics). > Ported with [`flutter-tvos plugin port`](https://github.com/fluttertv/flutter-tvos) -> from `firebase_crashlytics` 5.2.4, then finished + verified by hand. See +> from `firebase_crashlytics` 5.3.0 (Swift + Pigeon), then finished + verified by hand. See > `PORTING_REPORT.md`. ## Usage @@ -13,14 +13,15 @@ automatically. Apps that already use `firebase_crashlytics` and target tvOS add: ```yaml dependencies: - firebase_crashlytics: ^5.2.4 - firebase_crashlytics_tvos: ^0.0.1 - firebase_core: ^4.11.0 - firebase_core_tvos: ^0.0.1 # tvOS core (this package depends on it) + firebase_crashlytics: ^5.3.0 + firebase_crashlytics_tvos: ^0.0.2 + firebase_core: ^4.14.0 + firebase_core_tvos: ^0.0.3 # tvOS core (this package depends on it) ``` > **Version alignment matters.** This package's native code matches the -> `firebase_core_platform_interface` **7.1.0** train (`firebase_core 4.11.x`). +> `firebase_core_platform_interface` **8.1.1** train (`firebase_core 4.14.x`). +> Use 0.0.2 with `firebase_crashlytics` 5.3.0+, 0.0.1 with 5.2.x. > Keep your whole Firebase stack on one consistent FlutterFire release, or Dart > `initializeApp(options:)` can crash in `CoreFirebaseOptions.fromList` at launch > (a `FirebaseOptions` field-count mismatch). @@ -29,8 +30,8 @@ dependencies: | Platform | Implemented | Verified | | --------------------------------------- | ----------- | ----------------------------------------------------------- | -| Apple TV (`appletvos`) | yes | ✅ **runtime-verified on a physical Apple TV 4K (release/AOT)** — a forced `crash()` was captured and reported to the Crashlytics console | -| Apple TV simulator (`appletvsimulator`) | yes | ✅ runtime-verified — crash reported to the console | +| Apple TV (`appletvos`) | yes | 0.0.1: crash reported to the console. 0.0.2: not yet run | +| Apple TV simulator (`appletvsimulator`) | yes | ✅ 0.0.2: all calls work, crash detected on relaunch | All `firebase_crashlytics` Dart APIs work on tvOS. Ordinary crashes are captured (signal + `NSException` handlers), but tvOS has no alternate signal stack, so diff --git a/packages/firebase_crashlytics_tvos/example/pubspec.yaml b/packages/firebase_crashlytics_tvos/example/pubspec.yaml index e4a9bf2..5f6a6cf 100755 --- a/packages/firebase_crashlytics_tvos/example/pubspec.yaml +++ b/packages/firebase_crashlytics_tvos/example/pubspec.yaml @@ -8,12 +8,12 @@ environment: dependencies: # firebase_core is pinned exact; its train picks the matching firebase_core_tvos - # (4.11.x -> pi 7.1.0 -> 0.0.2); floating it breaks the native Pigeon ABI. - firebase_core: 4.11.0 - firebase_crashlytics: 5.2.4 + # (4.14.x -> pi 8.1.1 -> 0.0.3); floating it breaks the native Pigeon ABI. + firebase_core: 4.14.0 + firebase_crashlytics: 5.3.0 firebase_crashlytics_tvos: path: ../ - firebase_core_tvos: ^0.0.1 + firebase_core_tvos: ^0.0.3 flutter: sdk: flutter diff --git a/packages/firebase_crashlytics_tvos/pubspec.yaml b/packages/firebase_crashlytics_tvos/pubspec.yaml index ef6f8bc..809586d 100644 --- a/packages/firebase_crashlytics_tvos/pubspec.yaml +++ b/packages/firebase_crashlytics_tvos/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_crashlytics_tvos description: >- tvOS (Apple TV) implementation of the firebase_crashlytics Flutter plugin, providing Firebase Crashlytics crash reporting on Apple TV. -version: 0.0.1 +version: 0.0.2 homepage: https://fluttertv.dev repository: https://github.com/fluttertv/plugins/tree/main/packages/firebase_crashlytics_tvos issue_tracker: https://github.com/fluttertv/plugins/issues @@ -16,13 +16,17 @@ environment: dependencies: flutter: sdk: flutter - firebase_crashlytics: ^5.2.4 + # tvos/Classes/*.g.swift is generated against 5.3.0. 5.4.0 ships the same + # native sources apart from versionNumber. 6.0.0 needs firebase_core 5.x, + # which firebase_core_tvos doesn't support yet. + firebase_crashlytics: ^5.3.0 # Transitive Dart dependency so the app's dependency graph includes # firebase_core_tvos — plugin discovery (tvos_plugins.dart) only adds a # package's native pod to the generated Podfile if it's reachable here, # which the podspec's `s.dependency 'firebase_core_tvos'` then resolves # instead of failing over to upstream's iOS-only firebase_core pod. - firebase_core_tvos: ^0.0.1 + # 0.0.3+ matches the platform interface 8.1.1 that 5.3.0 needs. + firebase_core_tvos: ^0.0.3 dev_dependencies: flutter_lints: ^4.0.0 @@ -33,4 +37,4 @@ flutter: plugin: platforms: tvos: - pluginClass: FLTFirebaseCrashlyticsPlugin + pluginClass: FirebaseCrashlyticsPlugin diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/Constants.swift b/packages/firebase_crashlytics_tvos/tvos/Classes/Constants.swift new file mode 100644 index 0000000..5b202a9 --- /dev/null +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/Constants.swift @@ -0,0 +1,6 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Auto-generated file. Do not edit. +public let versionNumber = "5.3.0" diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/CrashlyticsPlatformHelpers.m b/packages/firebase_crashlytics_tvos/tvos/Classes/CrashlyticsPlatformHelpers.m new file mode 100644 index 0000000..c7e45ec --- /dev/null +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/CrashlyticsPlatformHelpers.m @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "CrashlyticsPlatformHelpers.h" +#import "Crashlytics_Platform.h" +#import "ExceptionModel_Platform.h" + +@implementation CrashlyticsPlatformHelpers + ++ (void)setDevelopmentPlatformName:(NSString *)name version:(NSString *)version { + FIRCrashlytics *crashlytics = [FIRCrashlytics crashlytics]; + crashlytics.developmentPlatformName = name; + crashlytics.developmentPlatformVersion = version; +} + ++ (void)recordExceptionModel:(FIRExceptionModel *)exception fatal:(BOOL)fatal { + exception.onDemand = YES; + exception.isFatal = fatal; + if (fatal) { + [[FIRCrashlytics crashlytics] recordOnDemandExceptionModel:exception]; + } else { + [[FIRCrashlytics crashlytics] recordExceptionModel:exception]; + } +} + +@end diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/FLTFirebaseCrashlyticsPlugin.m b/packages/firebase_crashlytics_tvos/tvos/Classes/FLTFirebaseCrashlyticsPlugin.m deleted file mode 100644 index 25f4af4..0000000 --- a/packages/firebase_crashlytics_tvos/tvos/Classes/FLTFirebaseCrashlyticsPlugin.m +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "include/FLTFirebaseCrashlyticsPlugin.h" -#import "include/Crashlytics_Platform.h" -#import "include/ExceptionModel_Platform.h" - -@import FirebaseCrashlytics; - -#if __has_include() -#import -#else -#import "FLTFirebasePluginRegistry.h" -#endif - -NSString *const kFLTFirebaseCrashlyticsChannelName = @"plugins.flutter.io/firebase_crashlytics"; -NSString *const kFLTFirebaseCrashlyticsTestChannelName = - @"plugins.flutter.io/firebase_crashlytics_test_stream"; - -// Argument Keys -NSString *const kCrashlyticsArgumentException = @"exception"; -NSString *const kCrashlyticsArgumentInformation = @"information"; -NSString *const kCrashlyticsArgumentStackTraceElements = @"stackTraceElements"; -NSString *const kCrashlyticsArgumentReason = @"reason"; -NSString *const kCrashlyticsArgumentIdentifier = @"identifier"; -NSString *const kCrashlyticsArgumentKey = @"key"; -NSString *const kCrashlyticsArgumentValue = @"value"; -NSString *const kCrashlyticsArgumentFatal = @"fatal"; - -NSString *const kCrashlyticsArgumentFile = @"file"; -NSString *const kCrashlyticsArgumentLine = @"line"; -NSString *const kCrashlyticsArgumentMethod = @"method"; - -NSString *const kCrashlyticsArgumentEnabled = @"enabled"; -NSString *const kCrashlyticsArgumentUnsentReports = @"unsentReports"; -NSString *const kCrashlyticsArgumentDidCrashOnPreviousExecution = @"didCrashOnPreviousExecution"; - -@interface FLTFirebaseCrashlyticsPlugin () -@property(nonatomic, strong) FlutterEventChannel *testEventChannel; -@property(nonatomic, strong) FlutterEventSink testEventSink; -@end - -@implementation FLTFirebaseCrashlyticsPlugin - -#pragma mark - FlutterPlugin - -// Returns a singleton instance of the Firebase Crashlytics plugin. -+ (instancetype)sharedInstance { - static dispatch_once_t onceToken; - static FLTFirebaseCrashlyticsPlugin *instance; - - dispatch_once(&onceToken, ^{ - instance = [[FLTFirebaseCrashlyticsPlugin alloc] init]; - // Register with the Flutter Firebase plugin registry. - [[FLTFirebasePluginRegistry sharedInstance] registerFirebasePlugin:instance]; - [[FIRCrashlytics crashlytics] setDevelopmentPlatformName:@"Flutter"]; - // We can't currently get the Flutter plugin version number, so use -1. - [[FIRCrashlytics crashlytics] setDevelopmentPlatformVersion:@"-1"]; - }); - - return instance; -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:kFLTFirebaseCrashlyticsChannelName - binaryMessenger:[registrar messenger]]; - FLTFirebaseCrashlyticsPlugin *instance = [FLTFirebaseCrashlyticsPlugin sharedInstance]; - [registrar addMethodCallDelegate:instance channel:channel]; - instance.testEventChannel = - [FlutterEventChannel eventChannelWithName:kFLTFirebaseCrashlyticsTestChannelName - binaryMessenger:[registrar messenger]]; - [instance.testEventChannel setStreamHandler:instance]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutterResult { - FLTFirebaseMethodCallErrorBlock errorBlock = - ^(NSString *_Nullable code, NSString *_Nullable message, NSDictionary *_Nullable details, - NSError *_Nullable error) { - // `result.error` is not called in this plugin so this block does nothing. - flutterResult(nil); - }; - - FLTFirebaseMethodCallResult *methodCallResult = - [FLTFirebaseMethodCallResult createWithSuccess:flutterResult andErrorBlock:errorBlock]; - - if ([@"Crashlytics#recordError" isEqualToString:call.method]) { - [self recordError:call.arguments withMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#setUserIdentifier" isEqualToString:call.method]) { - [self setUserIdentifier:call.arguments withMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#setCustomKey" isEqualToString:call.method]) { - [self setCustomKey:call.arguments withMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#log" isEqualToString:call.method]) { - [self log:call.arguments withMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#crash" isEqualToString:call.method]) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wall" - @throw - [NSException exceptionWithName:@"FirebaseCrashlyticsTestCrash" - reason:@"This is a test crash caused by calling .crash() in Dart." - userInfo:nil]; -#pragma clang diagnostic pop - } else if ([@"Crashlytics#setCrashlyticsCollectionEnabled" isEqualToString:call.method]) { - [self setCrashlyticsCollectionEnabled:call.arguments withMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#checkForUnsentReports" isEqualToString:call.method]) { - [self checkForUnsentReportsWithMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#sendUnsentReports" isEqualToString:call.method]) { - [self sendUnsentReportsWithMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#deleteUnsentReports" isEqualToString:call.method]) { - [self deleteUnsentReportsWithMethodCallResult:methodCallResult]; - } else if ([@"Crashlytics#didCrashOnPreviousExecution" isEqualToString:call.method]) { - [self didCrashOnPreviousExecutionWithMethodCallResult:methodCallResult]; - } else { - methodCallResult.success(FlutterMethodNotImplemented); - } -} - -#pragma mark - Firebase Crashlytics API - -- (void)recordError:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { - NSString *reason = arguments[kCrashlyticsArgumentReason]; - NSString *information = arguments[kCrashlyticsArgumentInformation]; - NSString *dartExceptionMessage = arguments[kCrashlyticsArgumentException]; - NSArray *errorElements = arguments[kCrashlyticsArgumentStackTraceElements]; - BOOL fatal = [arguments[kCrashlyticsArgumentFatal] boolValue]; - - // Log additional information so it's captured on the Firebase Crashlytics dashboard. - if ([information length] != 0) { - [[FIRCrashlytics crashlytics] logWithFormat:@"%@", information]; - } - - // Report crash. - NSMutableArray *frames = [NSMutableArray array]; - for (NSDictionary *errorElement in errorElements) { - [frames addObject:[self generateFrame:errorElement]]; - } - - if (![reason isEqual:[NSNull null]]) { - NSString *crashlyticsErrorReason = [NSString stringWithFormat:@"thrown %@", reason]; - - if (self.testEventSink) { - self.testEventSink(crashlyticsErrorReason); - } - // Log additional custom value to match Android. - [[FIRCrashlytics crashlytics] setCustomValue:crashlyticsErrorReason - forKey:@"flutter_error_reason"]; - reason = [NSString stringWithFormat:@"%@. Error thrown %@.", dartExceptionMessage, reason]; - } else { - reason = dartExceptionMessage; - } - - if (fatal) { - NSTimeInterval timeInterval = [NSDate date].timeIntervalSince1970; - [[FIRCrashlytics crashlytics] setCustomValue:@(llrint(timeInterval)) - forKey:@"com.firebase.crashlytics.flutter.fatal"]; - } - - // Log additional custom value to match Android. - [[FIRCrashlytics crashlytics] setCustomValue:dartExceptionMessage - forKey:@"flutter_error_exception"]; - - FIRExceptionModel *exception = [FIRExceptionModel exceptionModelWithName:@"FlutterError" - reason:reason]; - - exception.stackTrace = frames; - exception.onDemand = YES; - exception.isFatal = fatal; - if (fatal) { - [[FIRCrashlytics crashlytics] recordOnDemandExceptionModel:exception]; - } else { - [[FIRCrashlytics crashlytics] recordExceptionModel:exception]; - } - result.success(nil); -} - -- (void)setUserIdentifier:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { - [[FIRCrashlytics crashlytics] setUserID:arguments[kCrashlyticsArgumentIdentifier]]; - result.success(nil); -} - -- (void)setCustomKey:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { - NSString *key = arguments[kCrashlyticsArgumentKey]; - NSString *value = arguments[kCrashlyticsArgumentValue]; - [[FIRCrashlytics crashlytics] setCustomValue:value forKey:key]; - result.success(nil); -} - -- (void)log:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { - NSString *msg = arguments[@"message"]; - [[FIRCrashlytics crashlytics] logWithFormat:@"%@", msg]; - result.success(nil); -} - -- (void)setCrashlyticsCollectionEnabled:(id)arguments - withMethodCallResult:(FLTFirebaseMethodCallResult *)result { - BOOL enabled = [arguments[kCrashlyticsArgumentEnabled] boolValue]; - [[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:enabled]; - result.success(@{ - @"isCrashlyticsCollectionEnabled" : - @([FIRCrashlytics crashlytics].isCrashlyticsCollectionEnabled) - }); -} - -- (void)checkForUnsentReportsWithMethodCallResult:(FLTFirebaseMethodCallResult *)result { - [[FIRCrashlytics crashlytics] checkForUnsentReportsWithCompletion:^(BOOL unsentReports) { - result.success(@{kCrashlyticsArgumentUnsentReports : @(unsentReports)}); - }]; -} - -- (void)sendUnsentReportsWithMethodCallResult:(FLTFirebaseMethodCallResult *)result { - [[FIRCrashlytics crashlytics] sendUnsentReports]; - result.success(nil); -} - -- (void)deleteUnsentReportsWithMethodCallResult:(FLTFirebaseMethodCallResult *)result { - [[FIRCrashlytics crashlytics] deleteUnsentReports]; - result.success(nil); -} - -- (void)didCrashOnPreviousExecutionWithMethodCallResult:(FLTFirebaseMethodCallResult *)result { - BOOL didCrash = [[FIRCrashlytics crashlytics] didCrashDuringPreviousExecution]; - result.success(@{kCrashlyticsArgumentDidCrashOnPreviousExecution : @(didCrash)}); -} - -#pragma mark - Utilities - -- (FIRStackFrame *)generateFrame:(NSDictionary *)errorElement { - NSString *methodName = [errorElement valueForKey:kCrashlyticsArgumentMethod]; - NSString *className = [errorElement valueForKey:@"class"]; - NSString *symbol = [NSString stringWithFormat:@"%@.%@", className, methodName]; - - FIRStackFrame *frame = [FIRStackFrame - stackFrameWithSymbol:symbol - file:[errorElement valueForKey:kCrashlyticsArgumentFile] - line:[[errorElement valueForKey:kCrashlyticsArgumentLine] intValue]]; - return frame; -} - -#pragma mark - FLTFirebasePlugin - -- (void)didReinitializeFirebaseCore:(void (^)(void))completion { - // Not required for this plugin, nothing to cleanup between reloads. - completion(); -} - -- (NSDictionary *_Nonnull)pluginConstantsForFIRApp:(FIRApp *)firebase_app { - return @{ - @"isCrashlyticsCollectionEnabled" : - @([FIRCrashlytics crashlytics].isCrashlyticsCollectionEnabled) - }; -} - -- (NSString *_Nonnull)firebaseLibraryName { - return @LIBRARY_NAME; -} - -- (NSString *_Nonnull)firebaseLibraryVersion { - return @LIBRARY_VERSION; -} - -- (NSString *_Nonnull)flutterChannelName { - return kFLTFirebaseCrashlyticsChannelName; -} - -- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { - self.testEventSink = nil; - return nil; -} - -- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - self.testEventSink = events; - return nil; -} - -@end diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsMessages.g.swift b/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsMessages.g.swift new file mode 100644 index 0000000..1a0bf58 --- /dev/null +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsMessages.g.swift @@ -0,0 +1,594 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +// tvOS: Flutter.framework is the iOS-flavoured embedder on Apple TV. +#if (os(iOS) || os(tvOS)) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(Swift.type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +private func doubleEqualsFirebaseCrashlyticsMessages(_ lhs: Double, _ rhs: Double) -> Bool { + (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashFirebaseCrashlyticsMessages(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8_0000_0000_0000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + +func deepEqualsFirebaseCrashlyticsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + + case is (Void, Void): + return true + + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsFirebaseCrashlyticsMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsFirebaseCrashlyticsMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsFirebaseCrashlyticsMessages(lhsKey, rhsKey) { + if deepEqualsFirebaseCrashlyticsMessages(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsFirebaseCrashlyticsMessages(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + + default: + return false + } +} + +func deepHashFirebaseCrashlyticsMessages(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashFirebaseCrashlyticsMessages(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashFirebaseCrashlyticsMessages(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashFirebaseCrashlyticsMessages(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashFirebaseCrashlyticsMessages(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashFirebaseCrashlyticsMessages(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } + } else { + hasher.combine(0) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct CrashlyticsStackFrame: Hashable { + /// Dart reserved word `class`; native maps this to the historical "class" key. + var className: String? + var method: String + var file: String + var line: String + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CrashlyticsStackFrame? { + let className: String? = nilOrValue(pigeonVar_list[0]) + let method = pigeonVar_list[1] as! String + let file = pigeonVar_list[2] as! String + let line = pigeonVar_list[3] as! String + + return CrashlyticsStackFrame( + className: className, + method: method, + file: file, + line: line + ) + } + + func toList() -> [Any?] { + [ + className, + method, + file, + line, + ] + } + + static func == (lhs: CrashlyticsStackFrame, rhs: CrashlyticsStackFrame) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsFirebaseCrashlyticsMessages(lhs.className, rhs.className) + && deepEqualsFirebaseCrashlyticsMessages( + lhs.method, + rhs.method + ) && deepEqualsFirebaseCrashlyticsMessages(lhs.file, rhs.file) + && deepEqualsFirebaseCrashlyticsMessages( + lhs.line, + rhs.line + ) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("CrashlyticsStackFrame") + deepHashFirebaseCrashlyticsMessages(value: className, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: method, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: file, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: line, hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct RecordErrorRequest: Hashable { + var exception: String + var information: String + var reason: String? + var fatal: Bool + var buildId: String + var loadingUnits: [String] + var stackTraceElements: [CrashlyticsStackFrame] + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RecordErrorRequest? { + let exception = pigeonVar_list[0] as! String + let information = pigeonVar_list[1] as! String + let reason: String? = nilOrValue(pigeonVar_list[2]) + let fatal = pigeonVar_list[3] as! Bool + let buildId = pigeonVar_list[4] as! String + let loadingUnits = pigeonVar_list[5] as! [String] + let stackTraceElements = pigeonVar_list[6] as! [CrashlyticsStackFrame] + + return RecordErrorRequest( + exception: exception, + information: information, + reason: reason, + fatal: fatal, + buildId: buildId, + loadingUnits: loadingUnits, + stackTraceElements: stackTraceElements + ) + } + + func toList() -> [Any?] { + [ + exception, + information, + reason, + fatal, + buildId, + loadingUnits, + stackTraceElements, + ] + } + + static func == (lhs: RecordErrorRequest, rhs: RecordErrorRequest) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsFirebaseCrashlyticsMessages(lhs.exception, rhs.exception) + && deepEqualsFirebaseCrashlyticsMessages( + lhs.information, + rhs.information + ) && deepEqualsFirebaseCrashlyticsMessages(lhs.reason, rhs.reason) + && deepEqualsFirebaseCrashlyticsMessages( + lhs.fatal, + rhs.fatal + ) && deepEqualsFirebaseCrashlyticsMessages(lhs.buildId, rhs.buildId) + && deepEqualsFirebaseCrashlyticsMessages( + lhs.loadingUnits, + rhs.loadingUnits + ) && deepEqualsFirebaseCrashlyticsMessages(lhs.stackTraceElements, rhs.stackTraceElements) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("RecordErrorRequest") + deepHashFirebaseCrashlyticsMessages(value: exception, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: information, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: reason, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: fatal, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: buildId, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: loadingUnits, hasher: &hasher) + deepHashFirebaseCrashlyticsMessages(value: stackTraceElements, hasher: &hasher) + } +} + +private class FirebaseCrashlyticsMessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return CrashlyticsStackFrame.fromList(readValue() as! [Any?]) + case 130: + return RecordErrorRequest.fromList(readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class FirebaseCrashlyticsMessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? CrashlyticsStackFrame { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? RecordErrorRequest { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class FirebaseCrashlyticsMessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + FirebaseCrashlyticsMessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + FirebaseCrashlyticsMessagesPigeonCodecWriter(data: data) + } +} + +class FirebaseCrashlyticsMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = + FirebaseCrashlyticsMessagesPigeonCodec( + readerWriter: FirebaseCrashlyticsMessagesPigeonCodecReaderWriter() + ) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol FirebaseCrashlyticsHostApi { + func checkForUnsentReports(completion: @escaping (Result) -> Void) + func crash(completion: @escaping (Result) -> Void) + func deleteUnsentReports(completion: @escaping (Result) -> Void) + func didCrashOnPreviousExecution(completion: @escaping (Result) -> Void) + func recordError(request: RecordErrorRequest, completion: @escaping (Result) -> Void) + func log(message: String, completion: @escaping (Result) -> Void) + func sendUnsentReports(completion: @escaping (Result) -> Void) + func setCrashlyticsCollectionEnabled( + enabled: Bool, + completion: @escaping (Result) -> Void) + func setUserIdentifier(identifier: String, completion: @escaping (Result) -> Void) + func setCustomKey(key: String, value: String, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class FirebaseCrashlyticsHostApiSetup { + static var codec: FlutterStandardMessageCodec { + FirebaseCrashlyticsMessagesPigeonCodec.shared + } + + /// Sets up an instance of `FirebaseCrashlyticsHostApi` to handle messages through the + /// `binaryMessenger`. + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: FirebaseCrashlyticsHostApi?, + messageChannelSuffix: String = "" + ) { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let checkForUnsentReportsChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.checkForUnsentReports\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + checkForUnsentReportsChannel.setMessageHandler { _, reply in + api.checkForUnsentReports { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + checkForUnsentReportsChannel.setMessageHandler(nil) + } + let crashChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.crash\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + crashChannel.setMessageHandler { _, reply in + api.crash { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + crashChannel.setMessageHandler(nil) + } + let deleteUnsentReportsChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.deleteUnsentReports\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + deleteUnsentReportsChannel.setMessageHandler { _, reply in + api.deleteUnsentReports { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + deleteUnsentReportsChannel.setMessageHandler(nil) + } + let didCrashOnPreviousExecutionChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.didCrashOnPreviousExecution\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + didCrashOnPreviousExecutionChannel.setMessageHandler { _, reply in + api.didCrashOnPreviousExecution { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + didCrashOnPreviousExecutionChannel.setMessageHandler(nil) + } + let recordErrorChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.recordError\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + recordErrorChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! RecordErrorRequest + api.recordError(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + recordErrorChannel.setMessageHandler(nil) + } + let logChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.log\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + logChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let messageArg = args[0] as! String + api.log(message: messageArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + logChannel.setMessageHandler(nil) + } + let sendUnsentReportsChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.sendUnsentReports\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + sendUnsentReportsChannel.setMessageHandler { _, reply in + api.sendUnsentReports { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + sendUnsentReportsChannel.setMessageHandler(nil) + } + let setCrashlyticsCollectionEnabledChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.setCrashlyticsCollectionEnabled\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + setCrashlyticsCollectionEnabledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let enabledArg = args[0] as! Bool + api.setCrashlyticsCollectionEnabled(enabled: enabledArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setCrashlyticsCollectionEnabledChannel.setMessageHandler(nil) + } + let setUserIdentifierChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.setUserIdentifier\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + setUserIdentifierChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let identifierArg = args[0] as! String + api.setUserIdentifier(identifier: identifierArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setUserIdentifierChannel.setMessageHandler(nil) + } + let setCustomKeyChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.firebase_crashlytics_platform_interface.FirebaseCrashlyticsHostApi.setCustomKey\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + setCustomKeyChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let keyArg = args[0] as! String + let valueArg = args[1] as! String + api.setCustomKey(key: keyArg, value: valueArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setCustomKeyChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsPlugin.swift b/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsPlugin.swift new file mode 100644 index 0000000..3df5db7 --- /dev/null +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/FirebaseCrashlyticsPlugin.swift @@ -0,0 +1,210 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FirebaseCrashlytics + +#if canImport(firebase_crashlytics_objc) + import firebase_crashlytics_objc +#endif + +#if canImport(FlutterMacOS) + import FlutterMacOS +#else + import Flutter +#endif + +// tvOS: use our own tvOS core pod (upstream firebase_core has no tvOS platform). +import firebase_core_tvos + +private let kFLTFirebaseCrashlyticsChannelName = "plugins.flutter.io/firebase_crashlytics" +private let kFLTFirebaseCrashlyticsTestChannelName = + "plugins.flutter.io/firebase_crashlytics_test_stream" + +public class FirebaseCrashlyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, + FlutterStreamHandler, FirebaseCrashlyticsHostApi +{ + private var testEventChannel: FlutterEventChannel? + private var testEventSink: FlutterEventSink? + + private static let shared = FirebaseCrashlyticsPlugin() + + public static func register(with registrar: FlutterPluginRegistrar) { + let binaryMessenger: FlutterBinaryMessenger + + #if os(macOS) + binaryMessenger = registrar.messenger + // tvOS: the flutter-tvos registrar has the iOS `messenger()` signature. + #elseif (os(iOS) || os(tvOS)) + binaryMessenger = registrar.messenger() + #endif + + let instance = shared + FLTFirebasePluginRegistry.sharedInstance().register(instance) + CrashlyticsPlatformHelpers.setDevelopmentPlatformName("Flutter", version: "-1") + FirebaseCrashlyticsHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: instance) + + instance.testEventChannel = FlutterEventChannel( + name: kFLTFirebaseCrashlyticsTestChannelName, + binaryMessenger: binaryMessenger + ) + instance.testEventChannel?.setStreamHandler(instance) + } + + public func firebaseLibraryVersion() -> String { + versionNumber + } + + public func didReinitializeFirebaseCore(_ completion: @escaping () -> Void) { + completion() + } + + public func pluginConstants(for firebaseApp: FirebaseApp) -> [AnyHashable: Any] { + [ + "isCrashlyticsCollectionEnabled": Crashlytics.crashlytics().isCrashlyticsCollectionEnabled() + ] + } + + @objc public func firebaseLibraryName() -> String { + "flutter-fire-cls" + } + + @objc public func flutterChannelName() -> String { + kFLTFirebaseCrashlyticsChannelName + } + + // MARK: - FirebaseCrashlyticsHostApi + + func checkForUnsentReports(completion: @escaping (Result) -> Void) { + Crashlytics.crashlytics().checkForUnsentReports { unsentReports in + completion(.success(unsentReports)) + } + } + + func crash(completion: @escaping (Result) -> Void) { + NSException( + name: NSExceptionName("FirebaseCrashlyticsTestCrash"), + reason: "This is a test crash caused by calling .crash() in Dart.", + userInfo: nil + ).raise() + } + + func deleteUnsentReports(completion: @escaping (Result) -> Void) { + Crashlytics.crashlytics().deleteUnsentReports() + completion(.success(())) + } + + func didCrashOnPreviousExecution(completion: @escaping (Result) -> Void) { + completion(.success(Crashlytics.crashlytics().didCrashDuringPreviousExecution())) + } + + func recordError( + request: RecordErrorRequest, + completion: @escaping (Result) -> Void + ) { + var reason = request.reason + let information = request.information + let dartExceptionMessage = request.exception + let fatal = request.fatal + + if !information.isEmpty { + Crashlytics.crashlytics().log(information) + } + + var frames: [StackFrame] = [] + for errorElement in request.stackTraceElements { + frames.append(generateFrame(errorElement: errorElement)) + } + + if let reasonValue = reason { + let crashlyticsErrorReason = "thrown \(reasonValue)" + testEventSink?(crashlyticsErrorReason) + Crashlytics.crashlytics().setCustomValue( + crashlyticsErrorReason, + forKey: "flutter_error_reason" + ) + reason = "\(dartExceptionMessage). Error thrown \(reasonValue)." + } else { + reason = dartExceptionMessage + } + + if fatal { + let timeInterval = Date().timeIntervalSince1970 + Crashlytics.crashlytics().setCustomValue( + Int(timeInterval.rounded()), + forKey: "com.firebase.crashlytics.flutter.fatal" + ) + } + + Crashlytics.crashlytics().setCustomValue( + dartExceptionMessage, + forKey: "flutter_error_exception" + ) + + let exception = ExceptionModel(name: "FlutterError", reason: reason ?? "") + exception.stackTrace = frames + CrashlyticsPlatformHelpers.record(exception, fatal: fatal) + completion(.success(())) + } + + func log(message: String, completion: @escaping (Result) -> Void) { + Crashlytics.crashlytics().log(message) + completion(.success(())) + } + + func sendUnsentReports(completion: @escaping (Result) -> Void) { + Crashlytics.crashlytics().sendUnsentReports() + completion(.success(())) + } + + func setCrashlyticsCollectionEnabled( + enabled: Bool, + completion: @escaping (Result) -> Void + ) { + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(enabled) + completion(.success(Crashlytics.crashlytics().isCrashlyticsCollectionEnabled())) + } + + func setUserIdentifier( + identifier: String, + completion: @escaping (Result) -> Void + ) { + Crashlytics.crashlytics().setUserID(identifier) + completion(.success(())) + } + + func setCustomKey( + key: String, + value: String, + completion: @escaping (Result) -> Void + ) { + Crashlytics.crashlytics().setCustomValue(value, forKey: key) + completion(.success(())) + } + + // MARK: - Utilities + + private func generateFrame(errorElement: CrashlyticsStackFrame) -> StackFrame { + let methodName = errorElement.method + let className = errorElement.className ?? "" + let symbol = "\(className).\(methodName)" + let file = errorElement.file + let line = Int(errorElement.line) ?? 0 + return StackFrame(symbol: symbol, file: file, line: line) + } + + // MARK: - FlutterStreamHandler + + public func onListen( + withArguments arguments: Any?, + eventSink events: @escaping FlutterEventSink + ) -> FlutterError? { + testEventSink = events + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + testEventSink = nil + return nil + } +} diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/include/CrashlyticsPlatformHelpers.h b/packages/firebase_crashlytics_tvos/tvos/Classes/include/CrashlyticsPlatformHelpers.h new file mode 100644 index 0000000..cb97f9c --- /dev/null +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/include/CrashlyticsPlatformHelpers.h @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import FirebaseCrashlytics; +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +/// Thin ObjC helpers for Crashlytics private/platform APIs used by the Swift plugin. +@interface CrashlyticsPlatformHelpers : NSObject + ++ (void)setDevelopmentPlatformName:(NSString *)name version:(NSString *)version; + +/// Configures on-demand/fatal flags and records via the appropriate Crashlytics API. ++ (void)recordExceptionModel:(FIRExceptionModel *)exception fatal:(BOOL)fatal; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/include/Crashlytics_Platform.h b/packages/firebase_crashlytics_tvos/tvos/Classes/include/Crashlytics_Platform.h index 64543b5..9e42525 100644 --- a/packages/firebase_crashlytics_tvos/tvos/Classes/include/Crashlytics_Platform.h +++ b/packages/firebase_crashlytics_tvos/tvos/Classes/include/Crashlytics_Platform.h @@ -28,5 +28,3 @@ - (void)recordOnDemandExceptionModel:(FIRExceptionModel* _Nonnull)exceptionModel; @end - -void FIRCLSUserLoggingRecordInternalKeyValue(NSString* _Nullable key, id _Nullable value); diff --git a/packages/firebase_crashlytics_tvos/tvos/Classes/include/FLTFirebaseCrashlyticsPlugin.h b/packages/firebase_crashlytics_tvos/tvos/Classes/include/FLTFirebaseCrashlyticsPlugin.h deleted file mode 100644 index 28b4b4c..0000000 --- a/packages/firebase_crashlytics_tvos/tvos/Classes/include/FLTFirebaseCrashlyticsPlugin.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -#import - -#if TARGET_OS_OSX -#import -#else -#import -#endif - -#import - -#if __has_include() -#import -#else -#import "FLTFirebasePlugin.h" -#endif - -@interface FLTFirebaseCrashlyticsPlugin : FLTFirebasePlugin -@end diff --git a/packages/firebase_crashlytics_tvos/tvos/firebase_crashlytics_tvos.podspec b/packages/firebase_crashlytics_tvos/tvos/firebase_crashlytics_tvos.podspec index 2aa3418..e97c034 100644 --- a/packages/firebase_crashlytics_tvos/tvos/firebase_crashlytics_tvos.podspec +++ b/packages/firebase_crashlytics_tvos/tvos/firebase_crashlytics_tvos.podspec @@ -6,7 +6,7 @@ # Pod::Spec.new do |s| s.name = 'firebase_crashlytics_tvos' - s.version = '0.0.1' + s.version = '0.0.2' s.summary = 'tvOS implementation of firebase_crashlytics.' s.description = <<-DESC tvOS implementation of firebase_crashlytics, the federated platform @@ -21,7 +21,7 @@ package that ships native code targeting Apple tvOS. # Firebase/Crashlytics 12.x requires tvOS 15.0+ — bumped from the porter's # generic 13.0 default to satisfy that dependency. s.platform = :tvos, '15.0' - s.swift_version = '5.0' + s.swift_version = '5.5' # upstream 5.3.0's value # IMPORTANT: this podspec must not depend on the Flutter CocoaPod. That # pod does not declare tvOS support, so adding a dependency on it breaks @@ -32,20 +32,15 @@ package that ships native code targeting Apple tvOS. 'OTHER_SWIFT_FLAGS' => '$(inherited) -DTARGET_OS_TV', } - # FLTFirebaseCrashlyticsPlugin.m reads @LIBRARY_NAME / @LIBRARY_VERSION as - # preprocessor token-pasted string literals (same as upstream - # firebase_crashlytics's podspec) — without these the file fails to compile - # with "unexpected '@' in program". - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - 'GCC_PREPROCESSOR_DEFINITIONS' => - '$(inherited) LIBRARY_VERSION=\"0.0.1\" LIBRARY_NAME=\"flutter-fire-cls-tvos\"', - } + # Lets the Swift plugin see the ObjC helper headers (upstream uses a + # separate SPM target for them). + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - # The ported Classes/ call into FIRCrashlytics (Firebase/Crashlytics) and into + # The ported Classes/ call into Crashlytics (Firebase/Crashlytics) and into # our own firebase_core_tvos pod, not upstream's "firebase_core" pod — that # pod's own podspec declares only `s.platform = :ios`, so depending on it # directly would make `pod install` fail to find a tvOS-compatible spec. + # 12.15 matches firebase_core_tvos (upstream 5.3.0 uses 12.18). s.dependency 'Firebase/Crashlytics', '~> 12.15.0' s.dependency 'firebase_core_tvos' s.static_framework = true