Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmake/FindSparkle.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,15 @@ string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<key>SUEnableAutomaticChecks</key>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<false/>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<key>SUScheduledCheckInterval</key>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<integer>0</integer>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<key>CFBundleURLTypes</key>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<array>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<dict>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<key>CFBundleURLName</key>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<string>net.kushview.Element</string>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<key>CFBundleURLSchemes</key>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<array>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "<string>element</string>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "</array>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "</dict>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "</array>")
string(APPEND ELEMENT_APP_PLIST_TO_MERGE "</dict></plist>")
38 changes: 33 additions & 5 deletions include/element/application.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,29 @@ class Application : public juce::JUCEApplication,

/** Called when another instance of the application is started.

Attempts to open any file specified in the command line of the new instance.
Attempts to open any file specified in the command line of the new instance,
or handles custom URL scheme callbacks (e.g., OAuth).

@param commandLine The command line from the new instance
*/
void anotherInstanceStarted (const juce::String& commandLine) override;

/** Handles custom URL scheme callbacks.

Processes URLs with the element:// scheme, such as OAuth authorization callbacks.

@param urlString The complete URL string (e.g., element://auth/callback?code=...)
*/
void handleURLSchemeCallback (const juce::String& urlString);

#if JUCE_MAC
/** Registers the URL scheme handler for macOS Apple Events. */
void registerURLSchemeHandler();

/** Unregisters the URL scheme handler. */
void unregisterURLSchemeHandler();
#endif

/** Called when the application is suspended (mobile platforms). */
void suspended() override;

Expand Down Expand Up @@ -124,10 +141,21 @@ class Application : public juce::JUCEApplication,
virtual std::unique_ptr<ContentFactory> createContentFactory() { return nullptr; }

private:
juce::String launchCommandLine; ///< The command line used to launch the app
std::unique_ptr<Context> world; ///< The application context and services
std::unique_ptr<Startup> startup; ///< Handles startup initialization
juce::OwnedArray<juce::ChildProcessWorker> workers; ///< Worker processes (e.g., plugin scanner)
/** Background thread that attempts a token refresh on startup. */
class AuthStartupThread : public juce::Thread {
public:
explicit AuthStartupThread (Context& c) : juce::Thread ("AuthStartup"), ctx (c) {}
void run() override;

private:
Context& ctx;
};

juce::String launchCommandLine; ///< The command line used to launch the app
std::unique_ptr<Context> world; ///< The application context and services
std::unique_ptr<Startup> startup; ///< Handles startup initialization
std::unique_ptr<AuthStartupThread> authStartupThread; ///< Startup auth refresh thread
juce::OwnedArray<juce::ChildProcessWorker> workers; ///< Worker processes (e.g., plugin scanner)
#if JUCE_LINUX
class MidiSettingsApply {
public:
Expand Down
17 changes: 16 additions & 1 deletion include/element/settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Context;

struct MidiPanicParams;

class Settings : public juce::ApplicationProperties {
class Settings : public juce::ApplicationProperties,
public juce::ChangeBroadcaster {
public:
Settings();
~Settings();
Expand Down Expand Up @@ -49,6 +50,8 @@ class Settings : public juce::ApplicationProperties {
static const char* updateKeyTypeKey;
static const char* updateKeyKey;
static const char* updateKeyUserKey;
static const char* authPreviewUpdatesKey;
static const char* authAppcastUrlKey;
static const char* transportStartStopContinue;

bool getBool (std::string_view key, bool fallback = false) const noexcept;
Expand Down Expand Up @@ -153,6 +156,18 @@ class Settings : public juce::ApplicationProperties {
/** Change the update channgel. stable or nightly. */
void setUpdateChannel (const String& key);

/** Returns true if the authenticated user has preview update access. */
bool getAuthPreviewUpdates() const;

/** Set whether the authenticated user has preview update access. */
void setAuthPreviewUpdates (bool enabled);

/** Returns the cached signed appcast URL, or empty if not set. */
juce::String getAuthAppcastUrl() const;

/** Stores the signed appcast URL returned by the server. */
void setAuthAppcastUrl (const juce::String& url);

/** Set global midi panic settings. */
void setMidiPanicParams (MidiPanicParams);

Expand Down
9 changes: 9 additions & 0 deletions include/element/ui.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ class GuiService : public Service,
/** Check for a newer version and show alert, if available. */
void checkUpdates (bool background);

/** Override the updater's feed URL at runtime.
Pass an empty string to revert to the compiled-in default. */
void setUpdaterFeedUrl (const juce::String& url);

/** Re-reads the saved release channel from Settings and applies the
appropriate feed URL to the updater. Safe to call from the message
thread at any time (e.g. after a token refresh or sign-out). */
void applyStoredChannelToUpdater();

Services& services() const { return controller; }
juce::KeyListener* getKeyListener() const;

Expand Down
5 changes: 4 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ file(GLOB_RECURSE ELEMENT_SOURCES
target_sources(element PRIVATE ${ELEMENT_SOURCES})

if(APPLE)
target_sources(element PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/ui/nsviewwithparent.mm")
target_sources(element PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/ui/nsviewwithparent.mm"
"${CMAKE_CURRENT_SOURCE_DIR}/urlhandler.mm"
)
endif()

target_compile_definitions(element
Expand Down
112 changes: 107 additions & 5 deletions src/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "services/sessionservice.hpp"
#include "log.hpp"
#include "messages.hpp"
#include "auth.hpp"
#include "utils.hpp"

/** Define to force the application to behave as if running for the first time.
Expand Down Expand Up @@ -223,7 +224,16 @@ void Application::initialise (const String& commandLine)

initializeModulePath();
printCopyNotice();

#if JUCE_MAC
registerURLSchemeHandler();
#endif

launchApplication();

// Handle URL scheme if passed on command line
if (commandLine.startsWith ("element://"))
handleURLSchemeCallback (commandLine);
}

void Application::actionListenerCallback (const String& message)
Expand All @@ -234,16 +244,17 @@ void Application::actionListenerCallback (const String& message)

bool Application::canShutdown()
{
if (! MessageManager::getInstance()->isThisTheMessageThread())
if (! MessageManager::getInstance()->isThisTheMessageThread())
{
auto result = MessageManager::getInstance()->callSync (Application::canShutdown);
return result.has_value() ? * result : true;
return result.has_value() ? *result : true;
}

if (auto app = dynamic_cast<Application*> (getInstance())) {
if (auto app = dynamic_cast<Application*> (getInstance()))
{
auto& services = app->world->services();
auto ssvc = services.find<SessionService>();
return !ssvc->hasSessionChanged();
return ! ssvc->hasSessionChanged();
}

return true;
Expand All @@ -253,13 +264,22 @@ void Application::shutdown()
{
if (! world)
return;

#if JUCE_MAC
unregisterURLSchemeHandler();
#endif

#if JUCE_LINUX
applyMidiSettings.reset();
#endif
workers.clearQuick (true);
auto& srvs = world->services();
srvs.saveSettings();

if (authStartupThread && authStartupThread->isThreadRunning())
authStartupThread->stopThread (3000);
authStartupThread.reset();

auto& devices (world->devices());
devices.closeAudioDevice();

Expand Down Expand Up @@ -351,9 +371,75 @@ void Application::anotherInstanceStarted (const String& commandLine)
if (! world)
return;

// Handle custom URL scheme callbacks (e.g., auth)
if (commandLine.startsWith ("element://"))
{
handleURLSchemeCallback (commandLine);
return;
}

maybeOpenCommandLineFile (commandLine);
}

void Application::handleURLSchemeCallback (const String& urlString)
{
URL url (urlString);

// Handle auth callback: element://auth/callback?code=...
// Note: getSubPath() returns "callback" without leading slash
if (url.getDomain() == "auth" && url.getSubPath() == "callback")
{
// Bring main window to front
if (world)
{
if (auto* gui = world->services().find<GuiService>())
{
if (auto* mainWindow = gui->getMainWindow())
mainWindow->toFront (true);
}
}

const auto parameters = auth::parseQueryParameters (url.toString (true));

const auto authError = parameters["error"];
if (authError.isNotEmpty())
{
Logger::writeToLog ("Auth callback error: " + authError);
return;
}

String authCode = parameters["code"];
if (authCode.isNotEmpty())
{
auto& settings = world->settings();
String expectedState;
String codeVerifier;
if (! auth::consumePendingPKCE (settings, expectedState, codeVerifier))
{
Logger::writeToLog ("Auth token exchange failed: missing PKCE session; restart sign-in");
return;
}

const auto callbackState = parameters["state"].trim();
if (callbackState.isEmpty() || callbackState != expectedState)
{
Logger::writeToLog ("Auth callback rejected: invalid state");
return;
}

auto tokenResponse = auth::exchangeAuthorizationCode (authCode, codeVerifier);
if (! tokenResponse.success)
{
Logger::writeToLog (tokenResponse.error);
return;
}

auth::persistTokens (settings, tokenResponse);
Logger::writeToLog ("Auth token exchange completed successfully");
}
}
}

void Application::suspended() {}

void Application::resumed()
Expand All @@ -370,7 +456,7 @@ void Application::finishLaunching()
if (world->settings().scanForPluginsOnStartup())
world->plugins().scanAudioPlugins();

const bool isFirstRun =startup->isFirstRun;
const bool isFirstRun = startup->isFirstRun;
startup.reset();

auto& ui = *world->services().find<UI>();
Expand All @@ -379,12 +465,28 @@ void Application::finishLaunching()

world->services().run();

// Attempt to restore auth state from a stored refresh token in the background.
authStartupThread = std::make_unique<AuthStartupThread> (*world);
authStartupThread->startThread (Thread::Priority::low);

if (! isFirstRun && world->settings().checkForUpdates())
startTimer (10 * 1000);

maybeOpenCommandLineFile (getCommandLineParameters());
}

void Application::AuthStartupThread::run()
{
auth::maybeRefreshOnStartup (ctx.settings());

// Re-apply the channel preference to the updater from the message thread:
// the startup refresh may have fetched a new signed appcast URL.
juce::MessageManager::callAsync ([&ctx = ctx]() {
if (auto* gui = ctx.services().find<GuiService>())
gui->applyStoredChannelToUpdater();
});
}

void Application::printCopyNotice()
{
String appName = Util::appName();
Expand Down
Loading
Loading