upgrader icon indicating copy to clipboard operation
upgrader copied to clipboard

In iOS 18, the UpgradeAlert dialog is displayed, but the "Update Now" button is not functional because the appStoreListingURL is empty

Open HelmiAshraf opened this issue 1 year ago β€’ 4 comments

This issue is observed on iOS 18, while it works fine on Android and iOS 17 or below.

Steps to reproduce:

  1. Use UpgradeAlert in an iOS 18 environment.
  2. Trigger an update check where appStoreListingURL is empty.
  3. Attempt to click the "Update Now" button.
  4. Notice that the button does not redirect to the App Store.
flutter: upgrader: showTheDialog releaseNotes: null
flutter: upgrader: onCanPop called
flutter: upgrader: button tapped: update now
flutter: upgrader: empty appStoreListingURL
flutter: upgrader: showTheDialog onPopInvoked: true

HelmiAshraf avatar Sep 19 '24 09:09 HelmiAshraf

@HelmiAshraf Why was appStoreListingURL empty in this case? Since appStoreListingURL was empty, what are your expected results? (Also, if you can provide the entire upgrader log that might be more helpful.)

larryaasen avatar Sep 22 '24 15:09 larryaasen

Case 1: Wrong Country Code with Malaysia (English US) When the device is set to Malaysia and the language is English (US), the upgrader package incorrectly detects the countryCode: US instead of MY and current locale: en_US instead of en_MY. This prevents the app from retrieving the correct App Store listing.

Steps to Reproduce:

  1. Set the device to Malaysia with English (US).
  2. Run the app.
  3. Observe that the country code is detected as US instead of MY, and the store listing cannot be retrieved.

Expected Behavior: The app should detect the country code as MY and retrieve the correct store listing.

Actual Behavior: The country code is detected as US, and the store listing cannot be retrieved.

Case 2: ActionKit Errors with Malaysia (English UK) When the device is set to Malaysia with English (UK), the country code is correctly detected as MY, and the store listing is retrieved successfully. However, the logs are filled with ActionKit conversion errors related to <private> parameters.

Steps to Reproduce: Set the device to Malaysia with English (UK). Run the app. Observe that the correct country code (MY) is detected, and the store listing is retrieved, but ActionKit errors appear in the logs.

Failed to index parameter type: <private> in is.workflow.actions.encodemedia: Error Domain=(extension in ActionKit):__C.WFParameter.ToolKitConversionError Code=0

Device Information: iOS Version: 18.0 Upgrader Version: ^11.1.0 Flutter Version: 3.24.2

HelmiAshraf avatar Sep 23 '24 02:09 HelmiAshraf

I am seeing a similar issue in production, but cannot reproduce in testing. I live on the French/Swiss border, so maybe localisation is an issue. How can I print out which store upgrader is trying to access, so that I can complete my issue report.

The upgrader popup shows perfectly in both French and English (depending on the phone settings) with the latest release notes. But when we click on the upgrade now button, nothing happens.

Galaxy A53 5G (my wife's Samsung has the same issue) Android version 14 Upgader version:^11.1.0

stuartrapop avatar Sep 23 '24 07:09 stuartrapop

Hi @HelmiAshraf and @stuartrapop πŸ‘‹,

I faced a similar issue, and it turned out to be due to UpgradeAlert being called multiple times in the widget tree. Please ensure you're only calling UpgradeAlert once, otherwise it can cause the upgrade dialog to appear multiple times or behave unexpectedly.

βœ… How to Debug

To verify this, you can:

  1. Set a breakpoint inside your UpgradeAlert widget.
  2. Restart your app and observe if the breakpoint is hit more than once.

If it hits twice (or more), it means the widget is being rebuilt unnecessarily, probably due to a state update.


πŸ’‘ Suggested Fix

To avoid this, I recommend placing UpgradeAlert inside a splash screen or any initial screen that loads only once. Here’s how I structured it:

splash_view.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:upgrader/upgrader.dart';
import '../../gen/assets.gen.dart';
import '../controllers/splash_controller.dart';

class SplashView extends GetView<SplashController> {
  const SplashView({super.key});

  @override
  Widget build(BuildContext context) {
    return wrapWithUpgradeAlert(
      upgrader: controller.upgrader,
      child: Scaffold(
        body: Center(
          child: Assets.images.logo.image(
            width: 150,
            height: 150,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

Widget wrapWithUpgradeAlert({
  required Upgrader upgrader,
  required Widget child,
}) {
  return UpgradeAlert(
    showIgnore: false,
    showLater: false,
    showReleaseNotes: true,
    upgrader: upgrader,
    dialogStyle: UpgradeDialogStyle.cupertino,
    child: child,
  );
}

splash_controller.dart

import 'dart:async';
import 'dart:developer';

import 'package:get/get.dart';
import 'package:upgrader/upgrader.dart';
import '../routes/app_pages.dart';
import '../utils/data_storage.dart';

class SplashController extends GetxController {
  final locale = Get.locale?.languageCode ?? 'en';
  late Upgrader upgrader;
  Timer? timer;

  @override
  void onInit() {
    super.onInit();
    upgrader = Upgrader(
      durationUntilAlertAgain: Duration.zero,
      messages: locale == 'en'
          ? EnglishUpgradeMessages()
          : locale == 'es'
              ? SpanishUpgradeMessages()
              : locale == 'pt'
                  ? PortugueseUpgradeMessages()
                  : EnglishUpgradeMessages(),
    );

    timer = Timer(const Duration(seconds: 2), () {
      if (!upgrader.isUpdateAvailable()) {
        move();
      }
      timer?.cancel();
    });
  }

  @override
  void onClose() {
    timer?.cancel();
    super.onClose();
  }

  void move() {
    bool isGetStarted = DataStorage.retrieve('get_started') ?? false;
    String? token = DataStorage.retrieve('token');
    bool isLogin = token != null;

    if (isLogin) {
      Get.offAllNamed(Routes.BOTTOM_NAVIGATION);
    } else if (isGetStarted) {
      Get.offAllNamed(Routes.LOGIN);
    } else {
      Get.offAllNamed(Routes.GET_STARTED);
    }
  }
}

This setup ensures the alert is triggered only once during the splash, and doesn't interfere with state updates later in the app lifecycle.

Hope this helps! 😊

whitecodel avatar May 28 '25 06:05 whitecodel