launchAction static method
- BuildContext context, {
- required LaunchType type,
- String? value,
- Map<
String, dynamic> ? queryParameters, - List<
String> ? allowedSchemes, - LaunchMode mode = LaunchMode.platformDefault,
- LaunchOptions? options,
- 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;
}