Repository: zulip/zulip-flutter (language: dart)

Issue:
notif: Use live value for app ID on registering APNs token
In my draft for #321 I have a TODO like this:
```dart
      case TargetPlatform.iOS:
        const appBundleId = 'com.zulip.flutter'; // TODO find actual value live
        await registerApnsToken(connection, token: token, appid: appBundleId);
```

This will be just fine as long as the app ID is indeed `com.zulip.flutter`. That means it will basically be fine right through the beta period, until we start rolling this app out as the new version of `org.zulip.Zulip` (the app ID currently occupied by the zulip-mobile RN app).

The fix is easy, locally:
```dart
        final appBundleId = (await PackageInfo.fromPlatform()).packageName;
```

The one wrinkle is that that version breaks tests, because it's directly calling a plugin. To fix it, we'll want to indirect `PackageInfo.fromPlatform` through a method on `ZulipBinding`, like we've done for other plugins. This still isn't hard, but given the impending beta, I'm deferring it for now.



Discussion hints:
My approach to solve the issue:

1. Update ZulipBinding to provide package info: Add a method in ZulipBinding to return the package name. This method will wrap the call to PackageInfo.fromPlatform() and make it test-friendly.

2. Override the method in TestZulipBinding: In the TestZulipBinding, override getAppBundleId to return a mock or predefined value for testing.

3. Refactor the code to use getAppBundleId: Replace direct calls to PackageInfo.fromPlatform() with the new getAppBundleId method from ZulipBinding. Like this (final appBundleId = await ZulipBinding.instance.getAppBundleId();).

4. Write a test for the above logic.
I have started working on the issue and would like to claim it.
I will be making changes to the Zulip binding class and its corresponding test class. Adding packageInfo wrapper for bundleID.
Plus will be adding test for the same.

Pull request: notif: Use live value for app ID on registering APNs token
Fixes #407 

Changes made:
- Replaced the hardcoded app bundle ID with `packageInfo.packageName`.
- Added a new test that sets a specific appId via packageInfo and confirms it’s used in a notification request.

The first change caused the "token initially unknown" test in `store_test.dart` to fail due to the added asynchronous call to `ZulipBinding.instance.packageInfo`, which delayed the completion of `registerToken`.

To resolve this, a `flushMicrotasks` was added after `await startFuture` in the test to ensure `_registerNotificationToken` completes before the test checks for the request.



Test command: flutter test test/api/core_test.dart test/model/store_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/lib/model/binding.dart b/lib/model/binding.dart
index 94435631ac..198e0ae119 100644
--- a/lib/model/binding.dart
+++ b/lib/model/binding.dart
@@ -310,10 +310,12 @@ class LinuxDeviceInfo implements BaseDeviceInfo {
 class PackageInfo {
   final String version;
   final String buildNumber;
+  final String packageName;
 
   const PackageInfo({
     required this.version,
     required this.buildNumber,
+    required this.packageName,
   });
 }
 
@@ -427,6 +429,7 @@ class LiveZulipBinding extends ZulipBinding {
       _syncPackageInfo = PackageInfo(
         version: info.version,
         buildNumber: info.buildNumber,
+        packageName: info.packageName,
       );
     } catch (e, st) {
       assert(debugLog('Failed to prefetch package info: $e\n$st')); // TODO(log)
diff --git a/lib/notifications/receive.dart b/lib/notifications/receive.dart
index 738bb98a2e..d60469ff30 100644
--- a/lib/notifications/receive.dart
+++ b/lib/notifications/receive.dart
@@ -149,8 +149,10 @@ class NotificationService {
         await addFcmToken(connection, token: token);
 
       case TargetPlatform.iOS:
-        const appBundleId = 'com.zulip.flutter'; // TODO(#407) find actual value live
-        await addApnsToken(connection, token: token, appid: appBundleId);
+        final packageInfo = await ZulipBinding.instance.packageInfo;
+        await addApnsToken(connection,
+          token: token,
+          appid: packageInfo!.packageName);
 
       case TargetPlatform.linux:
       case TargetPlatform.macOS:


Test patch:
diff --git a/test/api/core_test.dart b/test/api/core_test.dart
index 297dcfc441..e2c6e72ecb 100644
--- a/test/api/core_test.dart
+++ b/test/api/core_test.dart
@@ -460,7 +460,7 @@ void main() {
       });
     }
 
-    const packageInfo = PackageInfo(version: '0.0.1', buildNumber: '1');
+    final packageInfo = eg.packageInfo(version: '0.0.1', buildNumber: '1');
 
     const testCases = [
       ('ZulipFlutter/0.0.1+1 (Android 14)',             AndroidDeviceInfo(release: '14', sdkInt: 34),                      ),
diff --git a/test/example_data.dart b/test/example_data.dart
index f31337d303..fc3acfc5a4 100644
--- a/test/example_data.dart
+++ b/test/example_data.dart
@@ -10,6 +10,7 @@ import 'package:zulip/api/model/submessage.dart';
 import 'package:zulip/api/route/messages.dart';
 import 'package:zulip/api/route/realm.dart';
 import 'package:zulip/api/route/channels.dart';
+import 'package:zulip/model/binding.dart';
 import 'package:zulip/model/database.dart';
 import 'package:zulip/model/narrow.dart';
 import 'package:zulip/model/settings.dart';
@@ -994,3 +995,15 @@ UpdateMachine updateMachine({
   return UpdateMachine.fromInitialSnapshot(
     store: store, initialSnapshot: initialSnapshot);
 }
+
+PackageInfo packageInfo({
+  String? version,
+  String? buildNumber,
+  String? packageName,
+}) {
+  return PackageInfo(
+    version: version ?? '1.0.0',
+    buildNumber: buildNumber ?? '1',
+    packageName: packageName ?? 'com.example.app',
+  );
+}
diff --git a/test/model/binding.dart b/test/model/binding.dart
index ced2a4d4b3..31f5738ddf 100644
--- a/test/model/binding.dart
+++ b/test/model/binding.dart
@@ -260,7 +260,7 @@ class TestZulipBinding extends ZulipBinding {
 
   /// The value that `ZulipBinding.instance.packageInfo` should return.
   PackageInfo packageInfoResult = _defaultPackageInfo;
-  static const _defaultPackageInfo = PackageInfo(version: '0.0.1', buildNumber: '1');
+  static final _defaultPackageInfo = eg.packageInfo();
 
   void _resetPackageInfo() {
     packageInfoResult = _defaultPackageInfo;
diff --git a/test/model/store_test.dart b/test/model/store_test.dart
index 1dfbb51273..ef0a7a72be 100644
--- a/test/model/store_test.dart
+++ b/test/model/store_test.dart
@@ -1291,6 +1291,7 @@ void main() {
       addTearDown(testBinding.reset);
       testBinding.firebaseMessagingInitialToken = '012abc';
       addTearDown(NotificationService.debugReset);
+      testBinding.packageInfoResult = eg.packageInfo(packageName: 'com.zulip.flutter');
       await NotificationService.instance.start();
 
       // On store startup, send the token.
@@ -1318,6 +1319,7 @@ void main() {
       addTearDown(testBinding.reset);
       testBinding.firebaseMessagingInitialToken = '012abc';
       addTearDown(NotificationService.debugReset);
+      testBinding.packageInfoResult = eg.packageInfo(packageName: 'com.zulip.flutter');
       final startFuture = NotificationService.instance.start();
 
       // TODO this test is a bit brittle in its interaction with asynchrony;
@@ -1336,6 +1338,7 @@ void main() {
       // When the token later appears, send it.
       connection.prepare(json: {});
       await startFuture;
+      async.flushMicrotasks();
       if (defaultTargetPlatform == TargetPlatform.android) {
         checkLastRequestFcm(token: '012abc');
       } else {
@@ -1350,6 +1353,22 @@ void main() {
         checkLastRequestFcm(token: '456def');
       }
     }));
+
+    test('on iOS, use provided app ID from packageInfo', () => awaitFakeAsync((async) async {
+      final origTargetPlatform = debugDefaultTargetPlatformOverride;
+      addTearDown(() => debugDefaultTargetPlatformOverride = origTargetPlatform);
+      debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+      addTearDown(testBinding.reset);
+      testBinding.firebaseMessagingInitialToken = '012abc';
+      testBinding.packageInfoResult = eg.packageInfo(packageName: 'com.example.test');
+      addTearDown(NotificationService.debugReset);
+      await NotificationService.instance.start();
+
+      prepareStore();
+      connection.prepare(json: {});
+      await updateMachine.registerNotificationToken();
+      checkLastRequestApns(token: '012abc', appid: 'com.example.test');
+    }));
   });
 
   group('ZulipVersionData', () {