| @@ -59,7 +59,7 @@ static void doBasicProjectSetup (Project& project, const NewProjectTemplates::Pr | |||||
| project.getProjectValue (Ids::useAppConfig) = false; | project.getProjectValue (Ids::useAppConfig) = false; | ||||
| project.getProjectValue (Ids::addUsingNamespaceToJuceHeader) = false; | project.getProjectValue (Ids::addUsingNamespaceToJuceHeader) = false; | ||||
| if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL()) | |||||
| if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures()) | |||||
| project.getProjectValue (Ids::displaySplashScreen) = true; | project.getProjectValue (Ids::displaySplashScreen) = true; | ||||
| if (NewProjectTemplates::isPlugin (projectTemplate)) | if (NewProjectTemplates::isPlugin (projectTemplate)) | ||||
| @@ -19,14 +19,23 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "jucer_LicenseState.h" | #include "jucer_LicenseState.h" | ||||
| #include "jucer_LicenseQueryThread.h" | |||||
| //============================================================================== | //============================================================================== | ||||
| class LicenseController | |||||
| class LicenseController : private Timer | |||||
| { | { | ||||
| public: | public: | ||||
| LicenseController() = default; | |||||
| LicenseController() | |||||
| { | |||||
| checkLicense(); | |||||
| } | |||||
| //============================================================================== | //============================================================================== | ||||
| static LicenseState getGPLState() | |||||
| { | |||||
| return { LicenseState::Type::gpl, projucerMajorVersion, {}, {} }; | |||||
| } | |||||
| LicenseState getCurrentState() const noexcept | LicenseState getCurrentState() const noexcept | ||||
| { | { | ||||
| return state; | return state; | ||||
| @@ -34,10 +43,13 @@ public: | |||||
| void setState (const LicenseState& newState) | 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() | void resetState() | ||||
| @@ -45,26 +57,21 @@ public: | |||||
| setState ({}); | 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; | 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) | static LicenseState licenseStateFromSettings (PropertiesFile& props) | ||||
| { | { | ||||
| if (auto licenseXml = props.getXmlValue ("license")) | if (auto licenseXml = props.getXmlValue ("license")) | ||||
| @@ -140,9 +129,9 @@ private: | |||||
| auto stateFromOldSettings = [&licenseXml]() -> LicenseState | auto stateFromOldSettings = [&licenseXml]() -> LicenseState | ||||
| { | { | ||||
| return { getLicenseTypeFromValue (licenseXml->getChildElementAllSubText ("type", {})), | return { getLicenseTypeFromValue (licenseXml->getChildElementAllSubText ("type", {})), | ||||
| licenseXml->getChildElementAllSubText ("authToken", {}), | |||||
| licenseXml->getChildElementAllSubText ("version", "-1").getIntValue(), | |||||
| licenseXml->getChildElementAllSubText ("username", {}), | licenseXml->getChildElementAllSubText ("username", {}), | ||||
| avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||||
| licenseXml->getChildElementAllSubText ("authToken", {}) }; | |||||
| }(); | }(); | ||||
| licenseStateToSettings (stateFromOldSettings, props); | licenseStateToSettings (stateFromOldSettings, props); | ||||
| @@ -151,9 +140,9 @@ private: | |||||
| } | } | ||||
| return { getLicenseTypeFromValue (licenseXml->getStringAttribute ("type", {})), | return { getLicenseTypeFromValue (licenseXml->getStringAttribute ("type", {})), | ||||
| licenseXml->getStringAttribute ("authToken", {}), | |||||
| licenseXml->getIntAttribute ("version", -1), | |||||
| licenseXml->getStringAttribute ("username", {}), | licenseXml->getStringAttribute ("username", {}), | ||||
| avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||||
| licenseXml->getStringAttribute ("authToken", {}) }; | |||||
| } | } | ||||
| return {}; | return {}; | ||||
| @@ -163,16 +152,16 @@ private: | |||||
| { | { | ||||
| props.removeValue ("license"); | props.removeValue ("license"); | ||||
| if (state.isValid()) | |||||
| if (state.isSignedIn()) | |||||
| { | { | ||||
| XmlElement licenseXml ("license"); | XmlElement licenseXml ("license"); | ||||
| if (auto* typeString = getLicenseStateValue (state.type)) | if (auto* typeString = getLicenseStateValue (state.type)) | ||||
| licenseXml.setAttribute ("type", typeString); | licenseXml.setAttribute ("type", typeString); | ||||
| licenseXml.setAttribute ("authToken", state.authToken); | |||||
| licenseXml.setAttribute ("version", state.version); | |||||
| licenseXml.setAttribute ("username", state.username); | licenseXml.setAttribute ("username", state.username); | ||||
| licenseXml.setAttribute ("avatar", avatarToLicenseState (state.avatar)); | |||||
| licenseXml.setAttribute ("authToken", state.authToken); | |||||
| props.setValue ("license", &licenseXml); | props.setValue ("license", &licenseXml); | ||||
| } | } | ||||
| @@ -180,6 +169,38 @@ private: | |||||
| props.saveIfNeeded(); | 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 | #if JUCER_ENABLE_GPL_MODE | ||||
| LicenseState state = getGPLState(); | LicenseState state = getGPLState(); | ||||
| @@ -188,6 +209,8 @@ private: | |||||
| #endif | #endif | ||||
| ListenerList<LicenseStateListener> stateListeners; | ListenerList<LicenseStateListener> stateListeners; | ||||
| LicenseQueryThread licenseQueryThread; | |||||
| bool hasRetriedLicenseCheck = false; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseController) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseController) | ||||
| @@ -18,47 +18,147 @@ | |||||
| #pragma once | #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: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| struct AccountEnquiryBase | struct AccountEnquiryBase | ||||
| @@ -115,22 +215,7 @@ private: | |||||
| auto json = JSON::parse (serverResponse); | auto json = JSON::parse (serverResponse); | ||||
| licenseState.authToken = json.getProperty ("token", {}).toString(); | 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()); | return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty()); | ||||
| } | } | ||||
| @@ -174,30 +259,27 @@ private: | |||||
| if (auto* licensesJson = json.getArray()) | if (auto* licensesJson = json.getArray()) | ||||
| { | { | ||||
| StringArray licenseTypes; | |||||
| std::vector<LicenseHelpers::LicenseVersionAndType> licenses; | |||||
| for (auto& license : *licensesJson) | 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; | return false; | ||||
| @@ -217,33 +299,34 @@ private: | |||||
| return JSON::toString (var (d.get())); | 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"; | 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) | 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; | int statusCode = 0; | ||||
| auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, | auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, | ||||
| accountEnquiryJob->getExtraHeaders(), | |||||
| accountEnquiryTask->getExtraHeaders(), | |||||
| 5000, nullptr, &statusCode); | 5000, nullptr, &statusCode); | ||||
| if (urlStream == nullptr) | 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; | String response; | ||||
| @@ -252,8 +335,8 @@ private: | |||||
| char buffer [8192]; | char buffer [8192]; | ||||
| auto num = urlStream->read (buffer, sizeof (buffer)); | auto num = urlStream->read (buffer, sizeof (buffer)); | ||||
| if (threadShouldExit()) | |||||
| return "Cancelled."; | |||||
| if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||||
| return cancelledError; | |||||
| if (num <= 0) | if (num <= 0) | ||||
| break; | break; | ||||
| @@ -261,18 +344,17 @@ private: | |||||
| response += buffer; | 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 {}; | return {}; | ||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| const String email, password; | |||||
| const std::function<void (LicenseState, String)> completionCallback; | |||||
| ThreadPool jobPool { 1 }; | |||||
| //============================================================================== | //============================================================================== | ||||
| JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) | JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) | ||||
| @@ -34,16 +34,32 @@ struct LicenseState | |||||
| LicenseState() = default; | 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 | String getLicenseTypeString() const | ||||
| { | { | ||||
| @@ -63,6 +79,6 @@ struct LicenseState | |||||
| } | } | ||||
| Type type = Type::none; | Type type = Type::none; | ||||
| String authToken, username; | |||||
| Image avatar; | |||||
| int version = -1; | |||||
| String username, authToken; | |||||
| }; | }; | ||||
| @@ -18,7 +18,7 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "jucer_LicenseQueryThread.h" | |||||
| #include "../../Project/UI/jucer_UserAvatarComponent.h" | #include "../../Project/UI/jucer_UserAvatarComponent.h" | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -74,6 +74,11 @@ public: | |||||
| setSize (300, 350); | setSize (300, 350); | ||||
| } | } | ||||
| ~LoginFormComponent() override | |||||
| { | |||||
| ProjucerApplication::getApp().getLicenseController().cancelSignIn(); | |||||
| } | |||||
| void resized() override | void resized() override | ||||
| { | { | ||||
| auto bounds = getLocalBounds().reduced (20); | auto bounds = getLocalBounds().reduced (20); | ||||
| @@ -185,9 +190,6 @@ private: | |||||
| void submitDetails() | void submitDetails() | ||||
| { | { | ||||
| if ((licenseQueryThread != nullptr && licenseQueryThread->isThreadRunning())) | |||||
| return; | |||||
| auto loginFormError = checkLoginFormsAreValid(); | auto loginFormError = checkLoginFormsAreValid(); | ||||
| if (loginFormError.isNotEmpty()) | if (loginFormError.isNotEmpty()) | ||||
| @@ -199,29 +201,27 @@ private: | |||||
| updateLoginButtonStates (true); | updateLoginButtonStates (true); | ||||
| WeakReference<Component> weakThis (this); | 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 | String checkLoginFormsAreValid() const | ||||
| @@ -263,11 +263,9 @@ private: | |||||
| findColour (treeIconColourId), | findColour (treeIconColourId), | ||||
| findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)), | findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)), | ||||
| findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) }; | findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) }; | ||||
| UserAvatarComponent userAvatar { false, false }; | |||||
| UserAvatarComponent userAvatar { false }; | |||||
| Label createAccountLabel { {}, "Create an account" }, | Label createAccountLabel { {}, "Create an account" }, | ||||
| errorMessageLabel { {}, {} }; | errorMessageLabel { {}, {} }; | ||||
| std::unique_ptr<LicenseQueryThread> licenseQueryThread; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LoginFormComponent) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LoginFormComponent) | ||||
| }; | }; | ||||
| @@ -1087,7 +1087,7 @@ void ProjucerApplication::getCommandInfo (CommandID commandID, ApplicationComman | |||||
| if (licenseState.isGPL()) | if (licenseState.isGPL()) | ||||
| result.setInfo ("Disable GPL mode", "Disables GPL mode", CommandCategories::general, 0); | result.setInfo ("Disable GPL mode", "Disables GPL mode", CommandCategories::general, 0); | ||||
| else | 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", | "Sign out of your JUCE account", | ||||
| CommandCategories::general, 0); | CommandCategories::general, 0); | ||||
| break; | break; | ||||
| @@ -1349,7 +1349,11 @@ void ProjucerApplication::launchTutorialsBrowser() | |||||
| void ProjucerApplication::doLoginOrLogout() | void ProjucerApplication::doLoginOrLogout() | ||||
| { | { | ||||
| if (licenseController->getCurrentState().type == LicenseState::Type::none) | |||||
| if (licenseController->getCurrentState().isSignedIn()) | |||||
| { | |||||
| licenseController->resetState(); | |||||
| } | |||||
| else | |||||
| { | { | ||||
| if (auto* window = mainWindowList.getMainWindowWithLoginFormOpen()) | if (auto* window = mainWindowList.getMainWindowWithLoginFormOpen()) | ||||
| { | { | ||||
| @@ -1361,10 +1365,6 @@ void ProjucerApplication::doLoginOrLogout() | |||||
| mainWindowList.getFrontmostWindow()->showLoginFormOverlay(); | mainWindowList.getFrontmostWindow()->showLoginFormOverlay(); | ||||
| } | } | ||||
| } | } | ||||
| else | |||||
| { | |||||
| licenseController->resetState(); | |||||
| } | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -87,3 +87,6 @@ enum ColourIds | |||||
| widgetBackgroundColourId = 0x2340010, | widgetBackgroundColourId = 0x2340010, | ||||
| secondaryWidgetBackgroundColourId = 0x2340011, | secondaryWidgetBackgroundColourId = 0x2340011, | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| static constexpr int projucerMajorVersion = 5; | |||||
| @@ -94,7 +94,7 @@ private: | |||||
| Label configLabel { "Config Label", "Selected exporter" }, projectNameLabel; | Label configLabel { "Config Label", "Selected exporter" }, projectNameLabel; | ||||
| ImageComponent juceIcon; | ImageComponent juceIcon; | ||||
| UserAvatarComponent userAvatar { true, true }; | |||||
| UserAvatarComponent userAvatar { true }; | |||||
| IconButton projectSettingsButton { "Project Settings", getIcons().settings }, | IconButton projectSettingsButton { "Project Settings", getIcons().settings }, | ||||
| saveAndOpenInIDEButton { "Save and Open in IDE", Image() }, | 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 | void handleAsyncUpdate() override | ||||
| { | { | ||||
| @@ -20,18 +20,18 @@ | |||||
| #include "../../Application/jucer_Application.h" | #include "../../Application/jucer_Application.h" | ||||
| //============================================================================== | |||||
| class UserAvatarComponent : public Component, | class UserAvatarComponent : public Component, | ||||
| public SettableTooltipClient, | public SettableTooltipClient, | ||||
| public ChangeBroadcaster, | public ChangeBroadcaster, | ||||
| private LicenseController::LicenseStateListener | private LicenseController::LicenseStateListener | ||||
| { | { | ||||
| public: | public: | ||||
| UserAvatarComponent (bool tooltip, bool signIn) | |||||
| : displayTooltip (tooltip), | |||||
| signInOnClick (signIn) | |||||
| UserAvatarComponent (bool isInteractive) | |||||
| : interactive (isInteractive) | |||||
| { | { | ||||
| ProjucerApplication::getApp().getLicenseController().addListener (this); | ProjucerApplication::getApp().getLicenseController().addListener (this); | ||||
| licenseStateChanged(); | |||||
| lookAndFeelChanged(); | |||||
| } | } | ||||
| ~UserAvatarComponent() override | ~UserAvatarComponent() override | ||||
| @@ -53,12 +53,12 @@ public: | |||||
| g.reduceClipRegion (ellipse); | g.reduceClipRegion (ellipse); | ||||
| } | } | ||||
| g.drawImage (userAvatarImage, bounds.toFloat(), RectanglePlacement::fillDestination); | |||||
| g.drawImage (currentAvatar, bounds.toFloat(), RectanglePlacement::fillDestination); | |||||
| } | } | ||||
| void mouseUp (const MouseEvent&) override | void mouseUp (const MouseEvent&) override | ||||
| { | { | ||||
| if (signInOnClick) | |||||
| if (interactive) | |||||
| { | { | ||||
| PopupMenu menu; | PopupMenu menu; | ||||
| menu.addCommandItem (ProjucerApplication::getApp().commandManager.get(), CommandIDs::loginLogout); | menu.addCommandItem (ProjucerApplication::getApp().commandManager.get(), CommandIDs::loginLogout); | ||||
| @@ -70,7 +70,25 @@ public: | |||||
| bool isDisplaingGPLLogo() const noexcept { return isGPL; } | bool isDisplaingGPLLogo() const noexcept { return isGPL; } | ||||
| private: | 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); | Image image (Image::ARGB, 250, 250, true); | ||||
| Graphics g (image); | Graphics g (image); | ||||
| @@ -87,17 +105,18 @@ private: | |||||
| return image; | return image; | ||||
| } | } | ||||
| //============================================================================== | |||||
| void licenseStateChanged() override | void licenseStateChanged() override | ||||
| { | { | ||||
| auto state = ProjucerApplication::getApp().getLicenseController().getCurrentState(); | auto state = ProjucerApplication::getApp().getLicenseController().getCurrentState(); | ||||
| isGPL = ProjucerApplication::getApp().getLicenseController().getCurrentState().isGPL(); | isGPL = ProjucerApplication::getApp().getLicenseController().getCurrentState().isGPL(); | ||||
| if (displayTooltip) | |||||
| if (interactive) | |||||
| { | { | ||||
| auto formattedUserString = [state]() -> String | auto formattedUserString = [state]() -> String | ||||
| { | { | ||||
| if (state.isValid()) | |||||
| if (state.isSignedIn()) | |||||
| return (state.isGPL() ? "" : (state.username + " - ")) + state.getLicenseTypeString(); | return (state.isGPL() ? "" : (state.username + " - ")) + state.getLicenseTypeString(); | ||||
| return "Not logged in"; | return "Not logged in"; | ||||
| @@ -106,18 +125,26 @@ private: | |||||
| setTooltip (formattedUserString); | setTooltip (formattedUserString); | ||||
| } | } | ||||
| userAvatarImage = state.isValid() && state.avatar.isValid() ? state.avatar : defaultAvatarImage; | |||||
| currentAvatar = isGPL ? gplAvatarImage | |||||
| : state.isSignedIn() ? standardAvatarImage : signedOutAvatarImage; | |||||
| repaint(); | repaint(); | ||||
| sendChangeMessage(); | sendChangeMessage(); | ||||
| } | } | ||||
| void lookAndFeelChanged() override | void lookAndFeelChanged() override | ||||
| { | { | ||||
| defaultAvatarImage = createDefaultAvatarImage(); | |||||
| standardAvatarImage = createStandardAvatarImage(); | |||||
| signedOutAvatarImage = createStandardAvatarImage(); | |||||
| if (interactive) | |||||
| signedOutAvatarImage.multiplyAllAlphas (0.4f); | |||||
| licenseStateChanged(); | licenseStateChanged(); | ||||
| repaint(); | 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."); | || companyName == "ROLI Ltd."); | ||||
| return ! ProjucerApplication::getApp().isRunningCommandLine && ! isJUCEProject && ! shouldDisplaySplashScreen() | return ! ProjucerApplication::getApp().isRunningCommandLine && ! isJUCEProject && ! shouldDisplaySplashScreen() | ||||
| && ! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL(); | |||||
| && ! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures(); | |||||
| } | } | ||||
| bool Project::isSaveAndExportDisabled() const | bool Project::isSaveAndExportDisabled() const | ||||
| @@ -747,9 +747,15 @@ void Project::updateLicenseWarning() | |||||
| { | { | ||||
| if (hasIncompatibleLicenseTypeAndSplashScreenSetting()) | 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, | 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 | else | ||||
| { | { | ||||