@@ -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 | ||||
{ | { | ||||