Bug 1729837: Prompt users on macOS to launch an existing installation from the Applications directory, rather than a .dmg disk image, when one is found. r=mstange,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D125015
This commit is contained in:
Stephen A Pohl
2021-10-05 14:41:05 +00:00
parent bade865adb
commit bb53b0b649
2 changed files with 101 additions and 9 deletions

View File

@@ -16,3 +16,12 @@ prompt-to-install-no-button = Dont Install
install-failed-title = { -brand-short-name } installation failed.
install-failed-message = { -brand-short-name } failed to install but will continue to run.
## Strings for a dialog that recommends to the user to start an existing
## installation of the app in the Applications directory if one is detected,
## rather than the app that was double-clicked in a .dmg.
prompt-to-launch-existing-app-title = Open existing { -brand-short-name } application?
prompt-to-launch-existing-app-message = You already have { -brand-short-name } installed. Use the installed application to stay up to date and prevent data loss.
prompt-to-launch-existing-app-yes-button = Open existing
prompt-to-launch-existing-app-no-button = No thanks

View File

@@ -38,6 +38,81 @@
namespace mozilla {
namespace MacRunFromDmgUtils {
/**
* Opens a dialog to ask the user whether the existing app in the Applications
* folder should be launched, or if the user wants to proceed with launching
* the app from the .dmg.
* Returns true if the dialog is successfully opened and the user chooses to
* launch the app from the Applications folder, otherwise returns false.
*/
static bool AskUserIfWeShouldLaunchExistingInstall() {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
// Try to get the localized strings:
nsTArray<nsCString> resIds = {
"branding/brand.ftl"_ns,
"toolkit/global/run-from-dmg.ftl"_ns,
};
RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true);
ErrorResult rv;
nsAutoCString mozTitle, mozMessage, mozLaunchExisting, mozLaunchFromDMG;
l10n->FormatValueSync("prompt-to-launch-existing-app-title"_ns, {}, mozTitle, rv);
if (rv.Failed()) {
return false;
}
l10n->FormatValueSync("prompt-to-launch-existing-app-message"_ns, {}, mozMessage, rv);
if (rv.Failed()) {
return false;
}
l10n->FormatValueSync("prompt-to-launch-existing-app-yes-button"_ns, {}, mozLaunchExisting, rv);
if (rv.Failed()) {
return false;
}
l10n->FormatValueSync("prompt-to-launch-existing-app-no-button"_ns, {}, mozLaunchFromDMG, rv);
if (rv.Failed()) {
return false;
}
NSString* title = [NSString stringWithUTF8String:reinterpret_cast<const char*>(mozTitle.get())];
NSString* message =
[NSString stringWithUTF8String:reinterpret_cast<const char*>(mozMessage.get())];
NSString* launchExisting =
[NSString stringWithUTF8String:reinterpret_cast<const char*>(mozLaunchExisting.get())];
NSString* launchFromDMG =
[NSString stringWithUTF8String:reinterpret_cast<const char*>(mozLaunchFromDMG.get())];
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
// Note that we don't set an icon since the app icon is used by default.
[alert setAlertStyle:NSAlertStyleInformational];
[alert setMessageText:title];
[alert setInformativeText:message];
// Note that if the user hits 'Enter' the "Install" button is activated,
// whereas if they hit 'Space' the "Don't Install" button is activated.
// That's standard behavior so probably desirable.
[alert addButtonWithTitle:launchExisting];
NSButton* launchFromDMGButton = [alert addButtonWithTitle:launchFromDMG];
// Since the "Don't Install" button doesn't have the title "Cancel" we need
// to map the Escape key to it manually:
[launchFromDMGButton setKeyEquivalent:@"\e"];
__block NSInteger result = -1;
dispatch_async(dispatch_get_main_queue(), ^{
result = [alert runModal];
[NSApp stop:nil];
});
// We need to call run on NSApp here for accessibility. See
// AskUserIfWeShouldInstall for a detailed explanation.
[NSApp run];
MOZ_ASSERT(result != -1);
return result == NSAlertFirstButtonReturn;
NS_OBJC_END_TRY_BLOCK_RETURN(false);
}
/**
* Opens a dialog to ask the user whether the app should be installed to their
* Applications folder. Returns true if the dialog is successfully opened and
@@ -158,7 +233,16 @@ static void ShowInstallFailedDialog() {
[alert setMessageText:title];
[alert setInformativeText:message];
[alert runModal];
__block NSInteger result = -1;
dispatch_async(dispatch_get_main_queue(), ^{
result = [alert runModal];
[NSApp stop:nil];
});
// We need to call run on NSApp here for accessibility. See
// AskUserIfWeShouldInstall for a detailed explanation.
[NSApp run];
MOZ_ASSERT(result != -1);
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
@@ -389,15 +473,14 @@ bool MaybeInstallFromDmgAndRelaunch() {
NSString* destPath = [applicationsDir stringByAppendingPathComponent:appName];
// If the app (an app of the same name) is already installed we can't really
// tell if we're dealing with the edge case of an inexperienced user running
// from .dmg by mistake, or if we're dealing with a more sophisticated user
// intentionally running from .dmg.
// We could throw a series of prompts at the user to figure out if they want
// to overwrite the installed app, or maybe just launch it, or continue with
// running from .dmg, but that seems like overkill for an edge case when
// we're just trying to provide mitigate inexperienced mac users trying to
// get and run our app for the first time.
// tell without asking if we're dealing with the edge case of an
// inexperienced user running from .dmg by mistake, or if we're dealing with
// a more sophisticated user intentionally running from .dmg.
if ([fileManager fileExistsAtPath:destPath]) {
if (AskUserIfWeShouldLaunchExistingInstall()) {
LaunchInstalledApp(destPath);
return true;
}
return false;
}