| @@ -59,7 +59,7 @@ static void doBasicProjectSetup (Project& project, const NewProjectTemplates::Pr | |||
| project.getProjectValue (Ids::useAppConfig) = false; | |||
| project.getProjectValue (Ids::addUsingNamespaceToJuceHeader) = false; | |||
| if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL()) | |||
| if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures()) | |||
| project.getProjectValue (Ids::displaySplashScreen) = true; | |||
| if (NewProjectTemplates::isPlugin (projectTemplate)) | |||
| @@ -19,14 +19,23 @@ | |||
| #pragma once | |||
| #include "jucer_LicenseState.h" | |||
| #include "jucer_LicenseQueryThread.h" | |||
| //============================================================================== | |||
| class LicenseController | |||
| class LicenseController : private Timer | |||
| { | |||
| public: | |||
| LicenseController() = default; | |||
| LicenseController() | |||
| { | |||
| checkLicense(); | |||
| } | |||
| //============================================================================== | |||
| static LicenseState getGPLState() | |||
| { | |||
| return { LicenseState::Type::gpl, projucerMajorVersion, {}, {} }; | |||
| } | |||
| LicenseState getCurrentState() const noexcept | |||
| { | |||
| return state; | |||
| @@ -34,10 +43,13 @@ public: | |||
| void setState (const LicenseState& newState) | |||
| { | |||
| state = newState; | |||
| licenseStateToSettings (state, getGlobalProperties()); | |||
| if (state != newState) | |||
| { | |||
| state = newState; | |||
| licenseStateToSettings (state, getGlobalProperties()); | |||
| stateListeners.call ([] (LicenseStateListener& l) { l.licenseStateChanged(); }); | |||
| stateListeners.call ([] (LicenseStateListener& l) { l.licenseStateChanged(); }); | |||
| } | |||
| } | |||
| void resetState() | |||
| @@ -45,26 +57,21 @@ public: | |||
| setState ({}); | |||
| } | |||
| static LicenseState getGPLState() | |||
| void signIn (const String& email, const String& password, | |||
| std::function<void (const String&)> completionCallback) | |||
| { | |||
| static auto logoImage = []() -> Image | |||
| { | |||
| if (auto logo = Drawable::createFromImageData (BinaryData::gpl_logo_svg, BinaryData::gpl_logo_svgSize)) | |||
| { | |||
| auto bounds = logo->getDrawableBounds(); | |||
| Image image (Image::ARGB, roundToInt (bounds.getWidth()), roundToInt (bounds.getHeight()), true); | |||
| Graphics g (image); | |||
| logo->draw (g, 1.0f); | |||
| return image; | |||
| } | |||
| jassertfalse; | |||
| return {}; | |||
| }(); | |||
| licenseQueryThread.doSignIn (email, password, | |||
| [this, completionCallback] (LicenseQueryThread::ErrorMessageAndType error, | |||
| LicenseState newState) | |||
| { | |||
| completionCallback (error.first); | |||
| setState (newState); | |||
| }); | |||
| } | |||
| return { LicenseState::Type::gpl, {}, {}, logoImage }; | |||
| void cancelSignIn() | |||
| { | |||
| licenseQueryThread.cancelRunningJobs(); | |||
| } | |||
| //============================================================================== | |||
| @@ -112,24 +119,6 @@ private: | |||
| return LicenseState::Type::none; | |||
| } | |||
| static Image avatarFromLicenseState (const String& licenseState) | |||
| { | |||
| MemoryOutputStream imageData; | |||
| Base64::convertFromBase64 (imageData, licenseState); | |||
| return ImageFileFormat::loadFrom (imageData.getData(), imageData.getDataSize()); | |||
| } | |||
| static String avatarToLicenseState (Image avatarImage) | |||
| { | |||
| MemoryOutputStream imageData; | |||
| if (avatarImage.isValid() && PNGImageFormat().writeImageToStream (avatarImage, imageData)) | |||
| return Base64::toBase64 (imageData.getData(), imageData.getDataSize()); | |||
| return {}; | |||
| } | |||
| static LicenseState licenseStateFromSettings (PropertiesFile& props) | |||
| { | |||
| if (auto licenseXml = props.getXmlValue ("license")) | |||
| @@ -140,9 +129,9 @@ private: | |||
| auto stateFromOldSettings = [&licenseXml]() -> LicenseState | |||
| { | |||
| return { getLicenseTypeFromValue (licenseXml->getChildElementAllSubText ("type", {})), | |||
| licenseXml->getChildElementAllSubText ("authToken", {}), | |||
| licenseXml->getChildElementAllSubText ("version", "-1").getIntValue(), | |||
| licenseXml->getChildElementAllSubText ("username", {}), | |||
| avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||
| licenseXml->getChildElementAllSubText ("authToken", {}) }; | |||
| }(); | |||
| licenseStateToSettings (stateFromOldSettings, props); | |||
| @@ -151,9 +140,9 @@ private: | |||
| } | |||
| return { getLicenseTypeFromValue (licenseXml->getStringAttribute ("type", {})), | |||
| licenseXml->getStringAttribute ("authToken", {}), | |||
| licenseXml->getIntAttribute ("version", -1), | |||
| licenseXml->getStringAttribute ("username", {}), | |||
| avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||
| licenseXml->getStringAttribute ("authToken", {}) }; | |||
| } | |||
| return {}; | |||
| @@ -163,16 +152,16 @@ private: | |||
| { | |||
| props.removeValue ("license"); | |||
| if (state.isValid()) | |||
| if (state.isSignedIn()) | |||
| { | |||
| XmlElement licenseXml ("license"); | |||
| if (auto* typeString = getLicenseStateValue (state.type)) | |||
| licenseXml.setAttribute ("type", typeString); | |||
| licenseXml.setAttribute ("authToken", state.authToken); | |||
| licenseXml.setAttribute ("version", state.version); | |||
| licenseXml.setAttribute ("username", state.username); | |||
| licenseXml.setAttribute ("avatar", avatarToLicenseState (state.avatar)); | |||
| licenseXml.setAttribute ("authToken", state.authToken); | |||
| props.setValue ("license", &licenseXml); | |||
| } | |||
| @@ -180,6 +169,38 @@ private: | |||
| props.saveIfNeeded(); | |||
| } | |||
| //============================================================================== | |||
| void checkLicense() | |||
| { | |||
| if (state.isSignedIn() && ! state.isGPL()) | |||
| { | |||
| auto completionCallback = [this] (LicenseQueryThread::ErrorMessageAndType error, | |||
| LicenseState updatedState) | |||
| { | |||
| if (error == LicenseQueryThread::ErrorMessageAndType()) | |||
| { | |||
| setState (updatedState); | |||
| } | |||
| else if ((error.second == LicenseQueryThread::ErrorType::busy | |||
| || error.second == LicenseQueryThread::ErrorType::cancelled | |||
| || error.second == LicenseQueryThread::ErrorType::connectionError) | |||
| && ! hasRetriedLicenseCheck) | |||
| { | |||
| hasRetriedLicenseCheck = true; | |||
| startTimer (10000); | |||
| } | |||
| }; | |||
| licenseQueryThread.checkLicenseValidity (state, std::move (completionCallback)); | |||
| } | |||
| } | |||
| void timerCallback() override | |||
| { | |||
| stopTimer(); | |||
| checkLicense(); | |||
| } | |||
| //============================================================================== | |||
| #if JUCER_ENABLE_GPL_MODE | |||
| LicenseState state = getGPLState(); | |||
| @@ -188,6 +209,8 @@ private: | |||
| #endif | |||
| ListenerList<LicenseStateListener> stateListeners; | |||
| LicenseQueryThread licenseQueryThread; | |||
| bool hasRetriedLicenseCheck = false; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseController) | |||
| @@ -18,47 +18,147 @@ | |||
| #pragma once | |||
| //============================================================================== | |||
| class LicenseQueryThread : public Thread | |||
| namespace LicenseHelpers | |||
| { | |||
| public: | |||
| LicenseQueryThread (const String& userEmail, const String& userPassword, | |||
| std::function<void (LicenseState, String)>&& cb) | |||
| : Thread ("LicenseQueryThread"), | |||
| email (userEmail), | |||
| password (userPassword), | |||
| completionCallback (std::move (cb)) | |||
| inline LicenseState::Type licenseTypeForString (const String& licenseString) | |||
| { | |||
| startThread(); | |||
| if (licenseString == "juce-pro") return LicenseState::Type::pro; | |||
| if (licenseString == "juce-indie") return LicenseState::Type::indie; | |||
| if (licenseString == "juce-edu") return LicenseState::Type::educational; | |||
| if (licenseString == "juce-personal") return LicenseState::Type::personal; | |||
| jassertfalse; // unknown type | |||
| return LicenseState::Type::none; | |||
| } | |||
| ~LicenseQueryThread() override | |||
| using LicenseVersionAndType = std::pair<int, LicenseState::Type>; | |||
| inline LicenseVersionAndType findBestLicense (std::vector<LicenseVersionAndType>&& licenses) | |||
| { | |||
| signalThreadShouldExit(); | |||
| waitForThreadToExit (6000); | |||
| if (licenses.size() == 1) | |||
| return licenses[0]; | |||
| auto getValueForLicenceType = [] (LicenseState::Type type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case LicenseState::Type::pro: return 4; | |||
| case LicenseState::Type::indie: return 3; | |||
| case LicenseState::Type::educational: return 2; | |||
| case LicenseState::Type::personal: return 1; | |||
| case LicenseState::Type::gpl: | |||
| case LicenseState::Type::none: | |||
| default: return -1; | |||
| } | |||
| }; | |||
| std::sort (licenses.begin(), licenses.end(), | |||
| [getValueForLicenceType] (const LicenseVersionAndType& l1, const LicenseVersionAndType& l2) | |||
| { | |||
| if (l1.first > l2.first) | |||
| return true; | |||
| if (l1.first == l2.first) | |||
| return getValueForLicenceType (l1.second) > getValueForLicenceType (l2.second); | |||
| return false; | |||
| }); | |||
| auto findFirstLicense = [&licenses] (bool isPaid) | |||
| { | |||
| auto iter = std::find_if (licenses.begin(), licenses.end(), | |||
| [isPaid] (const LicenseVersionAndType& l) | |||
| { | |||
| auto proOrIndie = (l.second == LicenseState::Type::pro || l.second == LicenseState::Type::indie); | |||
| return isPaid ? proOrIndie : ! proOrIndie; | |||
| }); | |||
| return iter != licenses.end() ? *iter | |||
| : LicenseVersionAndType(); | |||
| }; | |||
| auto newestPaid = findFirstLicense (true); | |||
| auto newestFree = findFirstLicense (false); | |||
| if (newestPaid.first >= projucerMajorVersion || newestPaid.first >= newestFree.first) | |||
| return newestPaid; | |||
| return newestFree; | |||
| } | |||
| } | |||
| //============================================================================== | |||
| class LicenseQueryThread | |||
| { | |||
| public: | |||
| enum class ErrorType | |||
| { | |||
| busy, | |||
| cancelled, | |||
| connectionError, | |||
| webResponseError | |||
| }; | |||
| using ErrorMessageAndType = std::pair<String, ErrorType>; | |||
| using LicenseQueryCallback = std::function<void (ErrorMessageAndType, LicenseState)>; | |||
| //============================================================================== | |||
| LicenseQueryThread() = default; | |||
| void run() override | |||
| void checkLicenseValidity (const LicenseState& state, LicenseQueryCallback completionCallback) | |||
| { | |||
| LicenseState state; | |||
| if (jobPool.getNumJobs() > 0) | |||
| { | |||
| completionCallback ({ {}, ErrorType::busy }, {}); | |||
| return; | |||
| } | |||
| auto errorMessage = runJob (std::make_unique<UserLogin> (email, password), state); | |||
| jobPool.addJob ([this, state, completionCallback] | |||
| { | |||
| auto updatedState = state; | |||
| if (errorMessage.isEmpty()) | |||
| errorMessage = runJob (std::make_unique<UserLicenseQuery> (state.authToken), state); | |||
| auto result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), updatedState); | |||
| if (errorMessage.isNotEmpty()) | |||
| state = {}; | |||
| WeakReference<LicenseQueryThread> weakThis (this); | |||
| MessageManager::callAsync ([weakThis, result, updatedState, completionCallback] | |||
| { | |||
| if (weakThis != nullptr) | |||
| completionCallback (result, updatedState); | |||
| }); | |||
| }); | |||
| } | |||
| WeakReference<LicenseQueryThread> weakThis (this); | |||
| MessageManager::callAsync ([this, weakThis, state, errorMessage] | |||
| void doSignIn (const String& email, const String& password, LicenseQueryCallback completionCallback) | |||
| { | |||
| cancelRunningJobs(); | |||
| jobPool.addJob ([this, email, password, completionCallback] | |||
| { | |||
| if (weakThis != nullptr) | |||
| completionCallback (state, errorMessage); | |||
| LicenseState state; | |||
| auto result = runTask (std::make_unique<UserLogin> (email, password), state); | |||
| if (result == ErrorMessageAndType()) | |||
| result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), state); | |||
| if (result != ErrorMessageAndType()) | |||
| state = {}; | |||
| WeakReference<LicenseQueryThread> weakThis (this); | |||
| MessageManager::callAsync ([weakThis, result, state, completionCallback] | |||
| { | |||
| if (weakThis != nullptr) | |||
| completionCallback (result, state); | |||
| }); | |||
| }); | |||
| } | |||
| void cancelRunningJobs() | |||
| { | |||
| jobPool.removeAllJobs (true, 500); | |||
| } | |||
| private: | |||
| //============================================================================== | |||
| struct AccountEnquiryBase | |||
| @@ -115,22 +215,7 @@ private: | |||
| auto json = JSON::parse (serverResponse); | |||
| licenseState.authToken = json.getProperty ("token", {}).toString(); | |||
| licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); | |||
| auto avatarURL = json.getProperty ("user", {}).getProperty ("avatar_url", {}).toString(); | |||
| if (avatarURL.isNotEmpty()) | |||
| { | |||
| URL url (avatarURL); | |||
| if (auto stream = url.createInputStream (false, nullptr, nullptr, {}, 5000)) | |||
| { | |||
| MemoryBlock mb; | |||
| stream->readIntoMemoryBlock (mb); | |||
| licenseState.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize()); | |||
| } | |||
| } | |||
| licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); | |||
| return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty()); | |||
| } | |||
| @@ -174,30 +259,27 @@ private: | |||
| if (auto* licensesJson = json.getArray()) | |||
| { | |||
| StringArray licenseTypes; | |||
| std::vector<LicenseHelpers::LicenseVersionAndType> licenses; | |||
| for (auto& license : *licensesJson) | |||
| { | |||
| auto status = license.getProperty ("status", {}).toString(); | |||
| auto version = license.getProperty ("product_version", {}).toString().trim(); | |||
| auto type = license.getProperty ("licence_type", {}).toString(); | |||
| auto status = license.getProperty ("status", {}).toString(); | |||
| if (status == "active") | |||
| licenseTypes.add (license.getProperty ("licence_type", {}).toString()); | |||
| if (status == "active" && type.isNotEmpty() && version.isNotEmpty()) | |||
| licenses.push_back ({ version.getIntValue(), LicenseHelpers::licenseTypeForString (type) }); | |||
| } | |||
| licenseTypes.removeEmptyStrings(); | |||
| licenseTypes.removeDuplicates (false); | |||
| licenseState.type = [licenseTypes]() | |||
| if (! licenses.empty()) | |||
| { | |||
| if (licenseTypes.contains ("juce-pro")) return LicenseState::Type::pro; | |||
| else if (licenseTypes.contains ("juce-indie")) return LicenseState::Type::indie; | |||
| else if (licenseTypes.contains ("juce-personal")) return LicenseState::Type::personal; | |||
| else if (licenseTypes.contains ("juce-edu")) return LicenseState::Type::educational; | |||
| auto bestLicense = LicenseHelpers::findBestLicense (std::move (licenses)); | |||
| return LicenseState::Type::none; | |||
| }(); | |||
| licenseState.version = bestLicense.first; | |||
| licenseState.type = bestLicense.second; | |||
| } | |||
| return (licenseState.type != LicenseState::Type::none); | |||
| return true; | |||
| } | |||
| return false; | |||
| @@ -217,33 +299,34 @@ private: | |||
| return JSON::toString (var (d.get())); | |||
| } | |||
| String runJob (std::unique_ptr<AccountEnquiryBase> accountEnquiryJob, LicenseState& state) | |||
| static ErrorMessageAndType runTask (std::unique_ptr<AccountEnquiryBase> accountEnquiryTask, LicenseState& state) | |||
| { | |||
| const ErrorMessageAndType cancelledError ("Cancelled.", ErrorType::cancelled); | |||
| const String endpointURL = "https://api.juce.com/api/v1"; | |||
| auto url = URL (endpointURL + accountEnquiryJob->getEndpointURLSuffix()); | |||
| auto url = URL (endpointURL + accountEnquiryTask->getEndpointURLSuffix()); | |||
| auto isPOST = accountEnquiryJob->isPOSTLikeRequest(); | |||
| auto isPOST = accountEnquiryTask->isPOSTLikeRequest(); | |||
| if (isPOST) | |||
| url = url.withPOSTData (postDataStringAsJSON (accountEnquiryJob->getParameterNamesAndValues())); | |||
| url = url.withPOSTData (postDataStringAsJSON (accountEnquiryTask->getParameterNamesAndValues())); | |||
| if (threadShouldExit()) | |||
| return "Cancelled."; | |||
| if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
| return cancelledError; | |||
| int statusCode = 0; | |||
| auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, | |||
| accountEnquiryJob->getExtraHeaders(), | |||
| accountEnquiryTask->getExtraHeaders(), | |||
| 5000, nullptr, &statusCode); | |||
| if (urlStream == nullptr) | |||
| return "Failed to connect to the web server."; | |||
| return { "Failed to connect to the web server.", ErrorType::connectionError }; | |||
| if (statusCode != accountEnquiryJob->getSuccessCode()) | |||
| return accountEnquiryJob->errorCodeToString (statusCode); | |||
| if (statusCode != accountEnquiryTask->getSuccessCode()) | |||
| return { accountEnquiryTask->errorCodeToString (statusCode), ErrorType::webResponseError }; | |||
| if (threadShouldExit()) | |||
| return "Cancelled."; | |||
| if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
| return cancelledError; | |||
| String response; | |||
| @@ -252,8 +335,8 @@ private: | |||
| char buffer [8192]; | |||
| auto num = urlStream->read (buffer, sizeof (buffer)); | |||
| if (threadShouldExit()) | |||
| return "Cancelled."; | |||
| if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
| return cancelledError; | |||
| if (num <= 0) | |||
| break; | |||
| @@ -261,18 +344,17 @@ private: | |||
| response += buffer; | |||
| } | |||
| if (threadShouldExit()) | |||
| return "Cancelled."; | |||
| if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
| return cancelledError; | |||
| if (! accountEnquiryJob->parseServerResponse (response, state)) | |||
| return "Failed to parse server response."; | |||
| if (! accountEnquiryTask->parseServerResponse (response, state)) | |||
| return { "Failed to parse server response.", ErrorType::webResponseError }; | |||
| return {}; | |||
| } | |||
| //============================================================================== | |||
| const String email, password; | |||
| const std::function<void (LicenseState, String)> completionCallback; | |||
| ThreadPool jobPool { 1 }; | |||
| //============================================================================== | |||
| JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) | |||
| @@ -34,16 +34,32 @@ struct LicenseState | |||
| LicenseState() = default; | |||
| LicenseState (Type t, String token, String user, Image avatarImage) | |||
| : type (t), authToken (token), username (user), avatar (avatarImage) | |||
| LicenseState (Type t, int v, String user, String token) | |||
| : type (t), version (v), username (user), authToken (token) | |||
| { | |||
| } | |||
| bool isValid() const noexcept { return isGPL() || (type != Type::none && authToken.isNotEmpty() && username.isNotEmpty()); } | |||
| bool operator== (const LicenseState& other) const noexcept | |||
| { | |||
| return type == other.type | |||
| && version == other.version | |||
| && username == other.username | |||
| && authToken == other.authToken; | |||
| } | |||
| bool operator != (const LicenseState& other) const noexcept | |||
| { | |||
| return ! operator== (other); | |||
| } | |||
| bool isSignedIn() const noexcept { return isGPL() || (version > 0 && username.isNotEmpty()); } | |||
| bool isOldLicense() const noexcept { return isSignedIn() && version < projucerMajorVersion; } | |||
| bool isGPL() const noexcept { return type == Type::gpl; } | |||
| bool isPaid() const noexcept { return type == Type::indie || type == Type::pro; } | |||
| bool isGPL() const noexcept { return type == Type::gpl; } | |||
| bool isPaidOrGPL() const noexcept { return isPaid() || isGPL(); } | |||
| bool canUnlockFullFeatures() const noexcept | |||
| { | |||
| return isGPL() || (isSignedIn() && ! isOldLicense() && (type == Type::indie || type == Type::pro)); | |||
| } | |||
| String getLicenseTypeString() const | |||
| { | |||
| @@ -63,6 +79,6 @@ struct LicenseState | |||
| } | |||
| Type type = Type::none; | |||
| String authToken, username; | |||
| Image avatar; | |||
| int version = -1; | |||
| String username, authToken; | |||
| }; | |||
| @@ -18,7 +18,7 @@ | |||
| #pragma once | |||
| #include "jucer_LicenseQueryThread.h" | |||
| #include "../../Project/UI/jucer_UserAvatarComponent.h" | |||
| //============================================================================== | |||
| @@ -74,6 +74,11 @@ public: | |||
| setSize (300, 350); | |||
| } | |||
| ~LoginFormComponent() override | |||
| { | |||
| ProjucerApplication::getApp().getLicenseController().cancelSignIn(); | |||
| } | |||
| void resized() override | |||
| { | |||
| auto bounds = getLocalBounds().reduced (20); | |||
| @@ -185,9 +190,6 @@ private: | |||
| void submitDetails() | |||
| { | |||
| if ((licenseQueryThread != nullptr && licenseQueryThread->isThreadRunning())) | |||
| return; | |||
| auto loginFormError = checkLoginFormsAreValid(); | |||
| if (loginFormError.isNotEmpty()) | |||
| @@ -199,29 +201,27 @@ private: | |||
| updateLoginButtonStates (true); | |||
| WeakReference<Component> weakThis (this); | |||
| licenseQueryThread.reset (new LicenseQueryThread (emailBox.getText(), passwordBox.getText(), | |||
| [this, weakThis] (LicenseState newState, String errorMessage) | |||
| { | |||
| if (weakThis == nullptr) | |||
| return; | |||
| updateLoginButtonStates (false); | |||
| if (errorMessage.isNotEmpty()) | |||
| { | |||
| showErrorMessage (errorMessage); | |||
| } | |||
| else | |||
| { | |||
| hideErrorMessage(); | |||
| auto& licenseController = ProjucerApplication::getApp().getLicenseController(); | |||
| licenseController.setState (newState); | |||
| mainWindow.hideLoginFormOverlay(); | |||
| ProjucerApplication::getApp().getCommandManager().commandStatusChanged(); | |||
| } | |||
| })); | |||
| auto completionCallback = [this, weakThis] (const String& errorMessage) | |||
| { | |||
| if (weakThis == nullptr) | |||
| return; | |||
| updateLoginButtonStates (false); | |||
| if (errorMessage.isNotEmpty()) | |||
| { | |||
| showErrorMessage (errorMessage); | |||
| } | |||
| else | |||
| { | |||
| hideErrorMessage(); | |||
| mainWindow.hideLoginFormOverlay(); | |||
| ProjucerApplication::getApp().getCommandManager().commandStatusChanged(); | |||
| } | |||
| }; | |||
| ProjucerApplication::getApp().getLicenseController().signIn (emailBox.getText(), passwordBox.getText(), | |||
| std::move (completionCallback)); | |||
| } | |||
| String checkLoginFormsAreValid() const | |||
| @@ -263,11 +263,9 @@ private: | |||
| findColour (treeIconColourId), | |||
| findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)), | |||
| findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) }; | |||
| UserAvatarComponent userAvatar { false, false }; | |||
| UserAvatarComponent userAvatar { false }; | |||
| Label createAccountLabel { {}, "Create an account" }, | |||
| errorMessageLabel { {}, {} }; | |||
| std::unique_ptr<LicenseQueryThread> licenseQueryThread; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LoginFormComponent) | |||
| }; | |||
| @@ -1087,7 +1087,7 @@ void ProjucerApplication::getCommandInfo (CommandID commandID, ApplicationComman | |||
| if (licenseState.isGPL()) | |||
| result.setInfo ("Disable GPL mode", "Disables GPL mode", CommandCategories::general, 0); | |||
| else | |||
| result.setInfo (licenseState.isValid() ? String ("Sign out ") + licenseState.username + "..." : String ("Sign in..."), | |||
| result.setInfo (licenseState.isSignedIn() ? String ("Sign out ") + licenseState.username + "..." : String ("Sign in..."), | |||
| "Sign out of your JUCE account", | |||
| CommandCategories::general, 0); | |||
| break; | |||
| @@ -1349,7 +1349,11 @@ void ProjucerApplication::launchTutorialsBrowser() | |||
| void ProjucerApplication::doLoginOrLogout() | |||
| { | |||
| if (licenseController->getCurrentState().type == LicenseState::Type::none) | |||
| if (licenseController->getCurrentState().isSignedIn()) | |||
| { | |||
| licenseController->resetState(); | |||
| } | |||
| else | |||
| { | |||
| if (auto* window = mainWindowList.getMainWindowWithLoginFormOpen()) | |||
| { | |||
| @@ -1361,10 +1365,6 @@ void ProjucerApplication::doLoginOrLogout() | |||
| mainWindowList.getFrontmostWindow()->showLoginFormOverlay(); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| licenseController->resetState(); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| @@ -87,3 +87,6 @@ enum ColourIds | |||
| widgetBackgroundColourId = 0x2340010, | |||
| secondaryWidgetBackgroundColourId = 0x2340011, | |||
| }; | |||
| //============================================================================== | |||
| static constexpr int projucerMajorVersion = 5; | |||
| @@ -94,7 +94,7 @@ private: | |||
| Label configLabel { "Config Label", "Selected exporter" }, projectNameLabel; | |||
| ImageComponent juceIcon; | |||
| UserAvatarComponent userAvatar { true, true }; | |||
| UserAvatarComponent userAvatar { true }; | |||
| IconButton projectSettingsButton { "Project Settings", getIcons().settings }, | |||
| saveAndOpenInIDEButton { "Save and Open in IDE", Image() }, | |||
| @@ -245,12 +245,12 @@ private: | |||
| }; | |||
| //============================================================================== | |||
| void valueTreePropertyChanged (ValueTree&, const Identifier&) override { triggerAsyncUpdate(); } | |||
| void valueTreeChildAdded (ValueTree&, ValueTree&) override { triggerAsyncUpdate(); } | |||
| void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { triggerAsyncUpdate(); } | |||
| void valueTreeChildOrderChanged (ValueTree&, int, int) override { triggerAsyncUpdate(); } | |||
| void valueTreeParentChanged (ValueTree&) override { triggerAsyncUpdate(); } | |||
| void valueTreeRedirected (ValueTree&) override { triggerAsyncUpdate(); } | |||
| void valueTreePropertyChanged (ValueTree&, const Identifier&) override { messagesChanged(); } | |||
| void valueTreeChildAdded (ValueTree&, ValueTree&) override { messagesChanged(); } | |||
| void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { messagesChanged(); } | |||
| void valueTreeChildOrderChanged (ValueTree&, int, int) override { messagesChanged(); } | |||
| void valueTreeParentChanged (ValueTree&) override { messagesChanged(); } | |||
| void valueTreeRedirected (ValueTree&) override { messagesChanged(); } | |||
| void handleAsyncUpdate() override | |||
| { | |||
| @@ -20,18 +20,18 @@ | |||
| #include "../../Application/jucer_Application.h" | |||
| //============================================================================== | |||
| class UserAvatarComponent : public Component, | |||
| public SettableTooltipClient, | |||
| public ChangeBroadcaster, | |||
| private LicenseController::LicenseStateListener | |||
| { | |||
| public: | |||
| UserAvatarComponent (bool tooltip, bool signIn) | |||
| : displayTooltip (tooltip), | |||
| signInOnClick (signIn) | |||
| UserAvatarComponent (bool isInteractive) | |||
| : interactive (isInteractive) | |||
| { | |||
| ProjucerApplication::getApp().getLicenseController().addListener (this); | |||
| licenseStateChanged(); | |||
| lookAndFeelChanged(); | |||
| } | |||
| ~UserAvatarComponent() override | |||
| @@ -53,12 +53,12 @@ public: | |||
| g.reduceClipRegion (ellipse); | |||
| } | |||
| g.drawImage (userAvatarImage, bounds.toFloat(), RectanglePlacement::fillDestination); | |||
| g.drawImage (currentAvatar, bounds.toFloat(), RectanglePlacement::fillDestination); | |||
| } | |||
| void mouseUp (const MouseEvent&) override | |||
| { | |||
| if (signInOnClick) | |||
| if (interactive) | |||
| { | |||
| PopupMenu menu; | |||
| menu.addCommandItem (ProjucerApplication::getApp().commandManager.get(), CommandIDs::loginLogout); | |||
| @@ -70,7 +70,25 @@ public: | |||
| bool isDisplaingGPLLogo() const noexcept { return isGPL; } | |||
| private: | |||
| Image createDefaultAvatarImage() | |||
| //============================================================================== | |||
| static Image createGPLAvatarImage() | |||
| { | |||
| if (auto logo = Drawable::createFromImageData (BinaryData::gpl_logo_svg, BinaryData::gpl_logo_svgSize)) | |||
| { | |||
| auto bounds = logo->getDrawableBounds(); | |||
| Image image (Image::ARGB, roundToInt (bounds.getWidth()), roundToInt (bounds.getHeight()), true); | |||
| Graphics g (image); | |||
| logo->draw (g, 1.0f); | |||
| return image; | |||
| } | |||
| jassertfalse; | |||
| return {}; | |||
| } | |||
| Image createStandardAvatarImage() | |||
| { | |||
| Image image (Image::ARGB, 250, 250, true); | |||
| Graphics g (image); | |||
| @@ -87,17 +105,18 @@ private: | |||
| return image; | |||
| } | |||
| //============================================================================== | |||
| void licenseStateChanged() override | |||
| { | |||
| auto state = ProjucerApplication::getApp().getLicenseController().getCurrentState(); | |||
| isGPL = ProjucerApplication::getApp().getLicenseController().getCurrentState().isGPL(); | |||
| if (displayTooltip) | |||
| if (interactive) | |||
| { | |||
| auto formattedUserString = [state]() -> String | |||
| { | |||
| if (state.isValid()) | |||
| if (state.isSignedIn()) | |||
| return (state.isGPL() ? "" : (state.username + " - ")) + state.getLicenseTypeString(); | |||
| return "Not logged in"; | |||
| @@ -106,18 +125,26 @@ private: | |||
| setTooltip (formattedUserString); | |||
| } | |||
| userAvatarImage = state.isValid() && state.avatar.isValid() ? state.avatar : defaultAvatarImage; | |||
| currentAvatar = isGPL ? gplAvatarImage | |||
| : state.isSignedIn() ? standardAvatarImage : signedOutAvatarImage; | |||
| repaint(); | |||
| sendChangeMessage(); | |||
| } | |||
| void lookAndFeelChanged() override | |||
| { | |||
| defaultAvatarImage = createDefaultAvatarImage(); | |||
| standardAvatarImage = createStandardAvatarImage(); | |||
| signedOutAvatarImage = createStandardAvatarImage(); | |||
| if (interactive) | |||
| signedOutAvatarImage.multiplyAllAlphas (0.4f); | |||
| licenseStateChanged(); | |||
| repaint(); | |||
| } | |||
| Image userAvatarImage, defaultAvatarImage { createDefaultAvatarImage() }; | |||
| bool isGPL = false, displayTooltip = false, signInOnClick = false; | |||
| //============================================================================== | |||
| Image standardAvatarImage, signedOutAvatarImage, gplAvatarImage { createGPLAvatarImage() }, currentAvatar; | |||
| bool isGPL = false, interactive = false; | |||
| }; | |||
| @@ -735,7 +735,7 @@ bool Project::hasIncompatibleLicenseTypeAndSplashScreenSetting() const | |||
| || companyName == "ROLI Ltd."); | |||
| return ! ProjucerApplication::getApp().isRunningCommandLine && ! isJUCEProject && ! shouldDisplaySplashScreen() | |||
| && ! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL(); | |||
| && ! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures(); | |||
| } | |||
| bool Project::isSaveAndExportDisabled() const | |||
| @@ -747,9 +747,15 @@ void Project::updateLicenseWarning() | |||
| { | |||
| if (hasIncompatibleLicenseTypeAndSplashScreenSetting()) | |||
| { | |||
| ProjectMessages::MessageAction action; | |||
| if (ProjucerApplication::getApp().getLicenseController().getCurrentState().isOldLicense()) | |||
| action = { "Upgrade", [] { URL ("https://juce.com/get-juce").launchInDefaultBrowser(); } }; | |||
| else | |||
| action = { "Sign in", [this] { ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())->showLoginFormOverlay(); } }; | |||
| addProjectMessage (ProjectMessages::Ids::incompatibleLicense, | |||
| { { "Sign in", [this] { ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())->showLoginFormOverlay(); } }, | |||
| { "Enable splash screen", [this] { displaySplashScreenValue = true; } } }); | |||
| { std::move (action), { "Enable splash screen", [this] { displaySplashScreenValue = true; } } }); | |||
| } | |||
| else | |||
| { | |||