launchAction static method

Future<bool> launchAction(
  1. BuildContext context, {
  2. required LaunchType type,
  3. String? value,
  4. Map<String, dynamic>? queryParameters,
  5. List<String>? allowedSchemes,
  6. LaunchMode mode = LaunchMode.platformDefault,
  7. LaunchOptions? options,
  8. CalendarEvent? calendarEvent,
})

Launches an action based on the specified LaunchType and parameters.

This method now supports various LaunchTypes, including social media with web fallbacks, calendar events, and file sharing.

type - The category of launch (e.g., website, phone, email, social media). value - The primary data string (e.g., URL, phone number, email address, social media handle). emailSubject - Optional subject for email. emailBody - Optional body content for email. smsBody - Optional pre-filled text for SMS. calendarEvent - Optional CalendarEvent object for calendar events. shareText - Text to share for LaunchType.share. shareSubject - Subject for sharing for LaunchType.share. allowedSchemes - An optional list of allowed URL schemes for security. mode - How the URL should be opened (e.g., in-app, system browser).

Returns true if successfully launched, false otherwise.

Implementation

static Future<bool> launchAction(
  BuildContext context, {
  required LaunchType type,
  String? value,
  Map<String, dynamic>? queryParameters,
  List<String>? allowedSchemes,
  LaunchMode mode = LaunchMode.platformDefault,
  LaunchOptions? options,
  CalendarEvent? calendarEvent, // New parameter
}) async {
  final AppLocalizations l10n = AppLocalizations.of(context)!;
  debugPrint('Attempting to launch type: $type with value: $value');

  // Helper function to check if a string is a valid web URL
  bool isWebUrl(String? url) {
    if (url == null || url.isEmpty) return false;
    return url.startsWith('http://') || url.startsWith('https://');
  }

  // Prioritize direct web URL launch for specific types if value is a web URL
  if ([
        LaunchType.tiktok,
        LaunchType.instagram,
        LaunchType.linkedin,
        LaunchType.facebook,
        LaunchType.github
      ].contains(type) &&
      isWebUrl(value)) {
    final Uri webUri = Uri.parse(value!);
    debugPrint('Attempting direct web launch for $type with URI: $webUri');
    final bool launchedDirectly = await _launchUrl(
      context,
      webUri,
      type: type,
      allowedSchemes: allowedSchemes,
      mode: mode,
      options: options,
    );
    if (launchedDirectly) {
      debugPrint('Successfully launched web URI directly for $type');
      options?.onLaunchResult?.call(type, webUri, true);
      return true;
    } else {
      debugPrint(
          'Direct web launch failed for $type. Proceeding with app/fallback logic.');
    }
  }

  if (options?.requireConfirmation == true) {
    if (!context.mounted) return false;
    final bool? confirm = await showDialog<bool>(
      context: context,
      builder: (BuildContext dialogContext) {
        final l10n = AppLocalizations.of(dialogContext)!;
        return AlertDialog(
          title: Text(options?.confirmationDialogTitle ??
              l10n.confirmationDialogTitle),
          content: Text(options?.confirmationDialogMessage ??
              l10n.confirmationDialogMessage(type.name)),
          actions: <Widget>[
            TextButton(
              onPressed: () => Navigator.of(dialogContext).pop(false),
              child: Text(l10n.cancelButtonLabel),
            ),
            TextButton(
              onPressed: () => Navigator.of(dialogContext).pop(true),
              child: Text(l10n.confirmButtonLabel),
            ),
          ],
        );
      },
    );
    if (!context.mounted) return false;
    if (confirm != true) {
      debugPrint('Launch action cancelled by user confirmation dialog.');
      options?.onLaunchResult?.call(type, Uri.parse(value ?? ''), false);
      return false;
    }
  }

  // The initial onLaunchAttempt is removed here because the specific URIs (appUri/webUri) are not yet built.
  // The onLaunchAttempt callback should ideally be called with the specific URI that is actually being attempted.
  // This is handled by the _launchUrl helper (after fixing its 'type' parameter) or by the onLaunchResult callbacks.
  // The print statement below already covers the initial intent.

  // Extract specific parameters from queryParameters
  final String? emailSubject = queryParameters?['subject'] as String?;
  final String? emailBody = queryParameters?['body'] as String?;
  final String? smsBody = queryParameters?['body'] as String?;
  final String? shareText = queryParameters?['text'] as String?;
  final String? shareSubject = queryParameters?['subject'] as String?;

  debugPrint(
      'LaunchAction: queryParameters before extraction = $queryParameters');
  debugPrint(
      'LaunchAction: type of queryParameters?[\'calendarEvent\'] = ${queryParameters?['calendarEvent'].runtimeType}');

  // Prepare common query parameters for UrlBuilder.buildUri
  final Map<String, dynamic> effectiveQueryParams = {
    ...?queryParameters, // Include all original query parameters
    if (emailSubject != null) 'subject': emailSubject,
    if (emailBody != null) 'body': emailBody,
    if (smsBody != null) 'body': smsBody,
    if (shareText != null) 'text': shareText,
    if (shareSubject != null) 'subject': shareSubject,
  };

  // Handle special cases first
  if (type == LaunchType.share) {
    if (shareText == null || shareText.isEmpty) {
      debugPrint('Share text is empty for LaunchType.share');
      return false;
    }
    try {
      await Share.share(shareText, subject: shareSubject);
      debugPrint('Successfully shared: $shareText');
      return true;
    } catch (e) {
      debugPrint('Failed to share: $e');
      return false;
    }
  }

  if (type == LaunchType.calendar) {
    debugPrint('LaunchType.calendar: queryParameters = $queryParameters');
    debugPrint(
        'LaunchType.calendar: direct calendarEvent parameter = $calendarEvent');
    if (calendarEvent == null) {
      debugPrint('CalendarEvent is null for LaunchType.calendar');
      return false;
    }
    final uri = UrlBuilder.buildCalendarUri(calendarEvent);
    if (!context.mounted) return false;
    final bool launched = await _launchUrl(
      context,
      uri,
      type: type,
      allowedSchemes: allowedSchemes,
      mode: mode,
      options: options,
    );
    if (!context.mounted) return false;
    options?.onLaunchResult?.call(type, uri, launched);
    return launched;
  }

  // For other LaunchTypes, build URIs and handle fallbacks
  Uri? appUri;
  Uri? webUri;

  switch (type) {
    case LaunchType.instagram:
      appUri = Uri.tryParse(value ?? '');
      webUri = Uri.tryParse(value ?? '');
      break;
    case LaunchType.tiktok:
      appUri = Uri.tryParse(value ?? '');
      webUri = Uri.tryParse(value ?? '');
      break;
    case LaunchType.linkedin:
      appUri = Uri.tryParse(value ?? '');
      webUri = Uri.tryParse(value ?? '');
      break;
    case LaunchType.facebook:
      appUri = Uri.tryParse(value ?? '');
      webUri = Uri.tryParse(value ?? '');
      break;
    case LaunchType.github:
      webUri = isWebUrl(value)
          ? Uri.parse(value!)
          : Uri.parse('https://github.com/$value');
      break;
    case LaunchType.website:
      webUri = UrlBuilder.buildUri(type: type, value: value);
      break;
    case LaunchType.email:
    case LaunchType.sms:
    case LaunchType.phone:
    case LaunchType.whatsapp:
      appUri = UrlBuilder.buildUri(
        type: type,
        value: value,
        queryParameters: effectiveQueryParams,
      );
      break;
    case LaunchType.map:
      appUri = UrlBuilder.buildMapUri(
        value,
        queryParameters: effectiveQueryParams,
      );
      webUri = UrlBuilder.buildUri(
        type: type,
        value: value,
        queryParameters: effectiveQueryParams,
      );
      break;
    case LaunchType.custom:
      appUri = UrlBuilder.buildUri(
        type: type,
        value: value,
        queryParameters: effectiveQueryParams,
      );
      break;
    case LaunchType.share:
    case LaunchType.calendar:
      // Handled above, should not reach here
      break;
  }

  // Try app URI first, then web URI if app URI fails or is not available.
  // If both fail, the method will return false.
  if (appUri != null && appUri.toString().isNotEmpty) {
    bool appIsInstalled =
        true; // Assume installed unless checked and found otherwise

    if (options?.checkAppInstallation == true) {
      final String? androidPackageName =
          _appIdentifiers[type]?['androidPackage'];
      final String? iOSScheme = _appIdentifiers[type]?['iOSScheme'];

      if (const LocalPlatform().isAndroid && androidPackageName != null) {
        // For TikTok, we bypass the explicit installation check and directly attempt to launch the URI.
        // The system's intent resolution will handle whether the app is installed or offer alternatives.
        if (type != LaunchType.tiktok) {
          appIsInstalled = await LaunchApp.isAppInstalled(
            androidPackageName: androidPackageName,
          );
          debugPrint(
              'Android app check for $androidPackageName: $appIsInstalled');
        } else {
          debugPrint(
              'Bypassing explicit app installation check for TikTok. Attempting direct URI launch.');
          appIsInstalled =
              true; // Assume installed to proceed with URI launch
        }
      } else if (const LocalPlatform().isIOS && iOSScheme != null) {
        final List<AppInfo> availableApps =
            await IosAppLauncher.getAvailableApps(
          type: type,
          uri: appUri,
          iOSAppSchemes: [iOSScheme],
        );
        appIsInstalled = availableApps.isNotEmpty;
        debugPrint('iOS app check for $iOSScheme: $appIsInstalled');
      }
    }

    if (!appIsInstalled) {
      debugPrint(
          'App not installed for $type. Showing app not installed dialog.');
      if (!context.mounted) return false;

      if (!context.mounted) return false;
      final AppNotInstalledAction? action =
          await showDialog<AppNotInstalledAction>(
        context: context,
        builder: (BuildContext dialogContext) {
          return AlertDialog(
            title: Text(options?.appNotInstalledDialogTitle ??
                l10n.appNotInstalledDialogTitle),
            content: Text(options?.appNotInstalledDialogMessage ??
                l10n.appNotInstalledDialogMessage(type.name)),
            actions: <Widget>[
              if (options?.appStoreLink != null)
                TextButton(
                  onPressed: () => Navigator.of(dialogContext)
                      .pop(AppNotInstalledAction.openStore),
                  child: Text(l10n.goToAppStoreButtonLabel),
                ),
              if (webUri != null &&
                  webUri.toString().isNotEmpty &&
                  options?.fallbackMode == LaunchFallbackMode.prompt)
                TextButton(
                  onPressed: () => Navigator.of(dialogContext)
                      .pop(AppNotInstalledAction.openWeb),
                  child: Text(l10n.openInWebButtonLabel),
                ),
              TextButton(
                onPressed: () => Navigator.of(dialogContext)
                    .pop(AppNotInstalledAction.cancel),
                child: Text(l10n.cancelButtonLabel),
              ),
            ],
          );
        },
      );

      if (!context.mounted) return false;
      switch (action) {
        case AppNotInstalledAction.openStore:
          if (options?.appStoreLink != null) {
            final bool launchedStore = await _launchUrl(
              context,
              Uri.parse(options!.appStoreLink!),
              type: LaunchType.website,
              // Treat app store link as a website launch
              mode: mode,
              options: options,
            );
            options.onLaunchResult
                ?.call(type, Uri.parse(options.appStoreLink!), launchedStore);
            return launchedStore;
          }
          break;
        case AppNotInstalledAction.openWeb:
          if (webUri != null && webUri.toString().isNotEmpty) {
            final bool launchedWeb = await _launchUrl(
              context,
              webUri,
              type: type,
              allowedSchemes: allowedSchemes,
              mode: mode,
              options: options,
            );
            if (!context.mounted) return false;
            options?.onLaunchResult?.call(type, webUri, launchedWeb);
            return launchedWeb;
          }
          break;
        case AppNotInstalledAction.cancel:
        case null:
          debugPrint('App not installed dialog cancelled by user.');
          options?.onLaunchResult?.call(type, appUri, false);
          return false;
      }
    }

    // If app is installed or checkAppInstallation is false, proceed with launching
    final bool canLaunchApp = await canLaunchUrl(appUri);
    if (!context.mounted) return false;

    // If bypassAppDetectionAndFallback is true and canLaunchApp is true, launch directly.
    if (options?.bypassAppDetectionAndFallback == true && canLaunchApp) {
      debugPrint(
          'Bypassing app detection and fallback. Launching app URI directly.');
      final bool launched = await _launchUrl(
        context,
        appUri,
        type: type,
        allowedSchemes: allowedSchemes,
        mode: mode,
        options: options,
      );
      if (!context.mounted) return false;
      options?.onLaunchResult?.call(type, appUri, launched);
      if (launched) {
        debugPrint('Successfully launched app URI for $type (bypassed)');
        return true;
      }
    }

    if (canLaunchApp) {
      final bool launched = await _launchUrl(
        context,
        appUri,
        type: type,
        allowedSchemes: allowedSchemes,
        mode: mode,
        options: options,
      );
      options?.onLaunchResult?.call(type, appUri, launched);
      if (launched) {
        debugPrint(
          'Successfully launched app URI for $type',
        );
        return true;
      }
    } else {
      debugPrint(
          'App URI ($appUri) cannot be launched for $type. Checking web fallback.');
      bool shouldLaunchWeb = false;
      switch (options?.fallbackMode) {
        case LaunchFallbackMode.prompt:
          if (webUri != null && webUri.toString().isNotEmpty) {
            if (!context.mounted) return false;
            final bool? openWeb = await showDialog<bool>(
              context: context,
              builder: (BuildContext dialogContext) {
                return AlertDialog(
                  title: Text(l10n.webFallbackDialogTitle),
                  content: Text(l10n.webFallbackDialogMessage),
                  actions: <Widget>[
                    TextButton(
                      onPressed: () => Navigator.of(dialogContext).pop(false),
                      child: Text(l10n.cancelButtonLabel),
                    ),
                    TextButton(
                      onPressed: () => Navigator.of(dialogContext).pop(true),
                      child: Text(l10n.openInWebButtonLabel),
                    ),
                  ],
                );
              },
            );
            if (!context.mounted) return false;
            shouldLaunchWeb = openWeb == true;
            debugPrint('User decision for web fallback: $shouldLaunchWeb');
          } else {
            debugPrint('Web fallback not initiated or cancelled for $type.');
            options?.onLaunchResult?.call(type, appUri, false);
            return false;
          }
          break;
        case LaunchFallbackMode.automatic:
          shouldLaunchWeb = true;
          debugPrint('Automatic web fallback for $type.');
          break;
        case LaunchFallbackMode.none:
          shouldLaunchWeb = false;
          debugPrint('Web fallback disabled for $type.');
          break;
        case null:
          shouldLaunchWeb = false;
          debugPrint(
              'Fallback mode is null for $type. Web fallback disabled.');
          break;
      }

      if (shouldLaunchWeb && webUri != null && webUri.toString().isNotEmpty) {
        if (!context.mounted) return false;
        final bool launchedWeb = await _launchUrl(
          context,
          webUri,
          type: type,
          allowedSchemes: allowedSchemes,
          mode: mode,
          options: options,
        );
        options?.onLaunchResult?.call(type, webUri, launchedWeb);
        if (launchedWeb) {
          debugPrint(
              'Successfully launched web URI after app failure and user decision for $type');
          return true;
        }
      }
    }
  } else if (webUri != null && webUri.toString().isNotEmpty) {
    // If no app URI, try web URI directly
    debugPrint(
        'No app URI available for $type. Attempting to launch web URI.');
    if (!context.mounted) return false;
    final bool launchedWeb = await _launchUrl(
      context,
      webUri,
      type: type,
      allowedSchemes: allowedSchemes,
      mode: mode,
      options: options,
    );
    options?.onLaunchResult?.call(type, webUri, launchedWeb);
    if (launchedWeb) {
      debugPrint('Successfully launched web URI for $type');
      return true;
    }
  }

  debugPrint('Failed to launch anything for $type with value: $value');
  options?.onLaunchResult?.call(type, Uri.parse(value ?? ''), false);
  return false;
}