mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
1 Commits
feat/impro
...
feat/ai-la
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0c2913bbff |
@@ -52,25 +52,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
.SetHidden();
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"KeyFromTextPressed",
|
||||
_("Key pressed"),
|
||||
_("Check if a key is pressed. This stays true as long as "
|
||||
"the key is held down. To check if a key was pressed during "
|
||||
"the frame, use \"Key just pressed\" instead."),
|
||||
_("_PARAM1_ key is pressed"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("keyboardKey", _("Key to check"))
|
||||
.MarkAsSimple();
|
||||
|
||||
extension
|
||||
.AddCondition("KeyFromTextJustPressed",
|
||||
_("Key just pressed"),
|
||||
_("Check if a key was just pressed."),
|
||||
_("_PARAM1_ key was just pressed"),
|
||||
.AddCondition("KeyFromTextPressed",
|
||||
_("Key pressed"),
|
||||
_("Check if a key is pressed"),
|
||||
_("_PARAM1_ key is pressed"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
@@ -81,7 +66,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
extension
|
||||
.AddCondition("KeyFromTextReleased",
|
||||
_("Key released"),
|
||||
_("Check if a key was just released."),
|
||||
_("Check if a key was just released"),
|
||||
_("_PARAM1_ key is released"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
|
@@ -298,19 +298,6 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the behavior can be used on objects from event-based objects.
|
||||
*/
|
||||
bool IsRelevantForChildObjects() const { return isRelevantForChildObjects; }
|
||||
|
||||
/**
|
||||
* Set that behavior can't be used on objects from event-based objects.
|
||||
*/
|
||||
BehaviorMetadata &MarkAsIrrelevantForChildObjects() {
|
||||
isRelevantForChildObjects = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
QuickCustomization::Visibility GetQuickCustomizationVisibility() const {
|
||||
return quickCustomizationVisibility;
|
||||
}
|
||||
@@ -406,7 +393,6 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
|
||||
mutable std::vector<gd::String> requiredBehaviors;
|
||||
bool isPrivate = false;
|
||||
bool isHidden = false;
|
||||
bool isRelevantForChildObjects = true;
|
||||
gd::String openFullEditorLabel;
|
||||
QuickCustomization::Visibility quickCustomizationVisibility = QuickCustomization::Visibility::Default;
|
||||
|
||||
|
@@ -76,7 +76,6 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
|
||||
if (project.HasEventsBasedObject(object.GetType())) {
|
||||
SerializerElement &variantsElement =
|
||||
objectAssetElement.AddChild("variants");
|
||||
@@ -88,6 +87,7 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
height = variant->GetAreaMaxY() - variant->GetAreaMinY();
|
||||
}
|
||||
|
||||
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
|
||||
gd::ObjectAssetSerializer::SerializeUsedVariantsTo(
|
||||
project, object, variantsElement, alreadyUsedVariantIdentifiers);
|
||||
}
|
||||
@@ -114,24 +114,14 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
resourceElement.SetAttribute("name", resource.GetName());
|
||||
}
|
||||
|
||||
std::unordered_set<gd::String> usedExtensionNames;
|
||||
usedExtensionNames.insert(extensionName);
|
||||
for (auto &usedVariantIdentifier : alreadyUsedVariantIdentifiers) {
|
||||
usedExtensionNames.insert(PlatformExtension::GetExtensionFromFullObjectType(
|
||||
usedVariantIdentifier));
|
||||
}
|
||||
SerializerElement &requiredExtensionsElement =
|
||||
objectAssetElement.AddChild("requiredExtensions");
|
||||
requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension");
|
||||
for (auto &usedExtensionName : usedExtensionNames) {
|
||||
if (project.HasEventsFunctionsExtensionNamed(usedExtensionName)) {
|
||||
auto &extension = project.GetEventsFunctionsExtension(usedExtensionName);
|
||||
SerializerElement &requiredExtensionElement =
|
||||
requiredExtensionsElement.AddChild("requiredExtension");
|
||||
requiredExtensionElement.SetAttribute("extensionName", usedExtensionName);
|
||||
requiredExtensionElement.SetAttribute("extensionVersion",
|
||||
extension.GetVersion());
|
||||
}
|
||||
if (project.HasEventsFunctionsExtensionNamed(extensionName)) {
|
||||
SerializerElement &requiredExtensionElement =
|
||||
requiredExtensionsElement.AddChild("requiredExtension");
|
||||
requiredExtensionElement.SetAttribute("extensionName", extensionName);
|
||||
requiredExtensionElement.SetAttribute("extensionVersion", "1.0.0");
|
||||
}
|
||||
|
||||
// TODO This can be removed when the asset script no longer require it.
|
||||
|
@@ -248,13 +248,12 @@ gd::String PropertyFunctionGenerator::GetStringifiedExtraInfo(
|
||||
gd::String arrayString;
|
||||
arrayString += "[";
|
||||
bool isFirst = true;
|
||||
for (const auto &choice : property.GetChoices()) {
|
||||
for (const gd::String &choice : property.GetExtraInfo()) {
|
||||
if (!isFirst) {
|
||||
arrayString += ",";
|
||||
}
|
||||
isFirst = false;
|
||||
// TODO Handle labels (and search "choice label")
|
||||
arrayString += "\"" + choice.GetValue() + "\"";
|
||||
arrayString += "\"" + choice + "\"";
|
||||
}
|
||||
arrayString += "]";
|
||||
return arrayString;
|
||||
|
@@ -34,20 +34,6 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
}
|
||||
}
|
||||
|
||||
if (!choices.empty()
|
||||
// Compatibility with GD <= 5.5.239
|
||||
|| !extraInformation.empty()
|
||||
// end of compatibility code
|
||||
) {
|
||||
SerializerElement &choicesElement = element.AddChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (const auto &choice : choices) {
|
||||
auto &choiceElement = choicesElement.AddChild("Choice");
|
||||
choiceElement.SetStringAttribute("value", choice.GetValue());
|
||||
choiceElement.SetStringAttribute("label", choice.GetLabel());
|
||||
}
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
element.AddChild("hidden").SetBoolValue(hidden);
|
||||
}
|
||||
@@ -94,26 +80,6 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
extraInformationElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
if (element.HasChild("choices")) {
|
||||
choices.clear();
|
||||
const SerializerElement &choicesElement = element.GetChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (std::size_t i = 0; i < choicesElement.GetChildrenCount(); ++i) {
|
||||
auto &choiceElement = choicesElement.GetChild(i);
|
||||
AddChoice(choiceElement.GetStringAttribute("value"),
|
||||
choiceElement.GetStringAttribute("label"));
|
||||
}
|
||||
}
|
||||
// Compatibility with GD <= 5.5.239
|
||||
else if (type == "Choice") {
|
||||
choices.clear();
|
||||
for (auto &choiceValue : extraInformation) {
|
||||
AddChoice(choiceValue, choiceValue);
|
||||
}
|
||||
extraInformation.clear();
|
||||
}
|
||||
// end of compatibility code
|
||||
|
||||
hidden = element.HasChild("hidden")
|
||||
? element.GetChild("hidden").GetBoolValue()
|
||||
: false;
|
||||
|
@@ -116,11 +116,6 @@ class GD_CORE_API PropertyDescriptor {
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& ClearChoices() {
|
||||
choices.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& AddChoice(const gd::String& value,
|
||||
const gd::String& label) {
|
||||
choices.push_back(PropertyDescriptorChoice(value, label));
|
||||
|
@@ -139,8 +139,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateBehaviorGetterAndSetter(
|
||||
project, extension, behavior, property, false);
|
||||
@@ -157,7 +157,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in a behavior") {
|
||||
@@ -386,8 +386,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateObjectGetterAndSetter(
|
||||
project, extension, object, property);
|
||||
@@ -404,7 +404,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in an object") {
|
||||
|
@@ -473,14 +473,7 @@ namespace gdjs {
|
||||
this._parentOldMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
||||
}
|
||||
|
||||
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
||||
// Custom objects can be resized during the events step.
|
||||
// The anchor constraints must be applied on child-objects after the parent events.
|
||||
const isChildObject = instanceContainer !== instanceContainer.getScene();
|
||||
if (isChildObject) {
|
||||
this.doStepPreEvents(instanceContainer);
|
||||
}
|
||||
}
|
||||
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
|
||||
|
||||
private _convertCoords(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
|
@@ -531,7 +531,6 @@ module.exports = {
|
||||
physics2Behavior,
|
||||
sharedData
|
||||
)
|
||||
.markAsIrrelevantForChildObjects()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js')
|
||||
.addRequiredFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm')
|
||||
|
@@ -679,7 +679,6 @@ module.exports = {
|
||||
behavior,
|
||||
sharedData
|
||||
)
|
||||
.markAsIrrelevantForChildObjects()
|
||||
.addIncludeFile(
|
||||
'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js'
|
||||
)
|
||||
|
@@ -37,8 +37,7 @@ void DeclarePhysicsBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"res/physics-deprecated32.png",
|
||||
"PhysicsBehavior",
|
||||
std::make_shared<PhysicsBehavior>(),
|
||||
std::make_shared<ScenePhysicsDatas>())
|
||||
.MarkAsIrrelevantForChildObjects();
|
||||
std::make_shared<ScenePhysicsDatas>());
|
||||
|
||||
aut.AddAction("SetStatic",
|
||||
("Make the object static"),
|
||||
|
@@ -18,8 +18,6 @@ KeyboardExtension::KeyboardExtension() {
|
||||
"gdjs.evtTools.input.wasKeyReleased");
|
||||
GetAllConditions()["KeyFromTextPressed"].SetFunctionName(
|
||||
"gdjs.evtTools.input.isKeyPressed");
|
||||
GetAllConditions()["KeyFromTextJustPressed"].SetFunctionName(
|
||||
"gdjs.evtTools.input.wasKeyJustPressed");
|
||||
GetAllConditions()["KeyFromTextReleased"].SetFunctionName(
|
||||
"gdjs.evtTools.input.wasKeyReleased");
|
||||
GetAllConditions()["AnyKeyPressed"].SetFunctionName(
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<name>GDJS_PROJECTNAME</name>
|
||||
<content src="index.html" />
|
||||
<plugin name="cordova-plugin-whitelist" version="1" />
|
||||
<plugin name="cordova-plugin-screen-orientation" version="3.0.4" />
|
||||
<plugin name="cordova-plugin-screen-orientation" version="3.0.2" />
|
||||
<access origin="*" />
|
||||
<allow-intent href="http://*/*" />
|
||||
<allow-intent href="https://*/*" />
|
||||
@@ -67,4 +67,4 @@
|
||||
<!-- Keep cordova-plugin-ionic-webview plugin last as it has a deployment-target to 11, which
|
||||
affects the installation of other plugins.-->
|
||||
<plugin name="cordova-plugin-ionic-webview" version="5.0.1" />
|
||||
</widget>
|
||||
</widget>
|
@@ -300,13 +300,8 @@ namespace gdjs {
|
||||
if (profiler) {
|
||||
profiler.end(this.type);
|
||||
}
|
||||
}
|
||||
|
||||
override stepBehaviorsPostEvents(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
super.stepBehaviorsPostEvents(instanceContainer);
|
||||
this._instanceContainer._stepBehaviorsPostEvents();
|
||||
this._instanceContainer._updateObjectsPostEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -576,9 +576,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Call each behavior stepPostEvents method.
|
||||
* Update the objects (update positions, time management...)
|
||||
*/
|
||||
_stepBehaviorsPostEvents() {
|
||||
_updateObjectsPostEvents() {
|
||||
this._cacheOrClearRemovedInstances();
|
||||
|
||||
// It is *mandatory* to create and iterate on a external list of all objects, as the behaviors
|
||||
|
@@ -153,7 +153,7 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key is pressed (i.e: just pressed or held down).
|
||||
* Return true if the specified key is pressed
|
||||
*
|
||||
*/
|
||||
export const isKeyPressed = function (
|
||||
@@ -170,22 +170,8 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key was just pressed (i.e: it started being pressed
|
||||
* during this frame).
|
||||
*/
|
||||
export const wasKeyJustPressed = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
key: string
|
||||
) {
|
||||
return instanceContainer
|
||||
.getGame()
|
||||
.getInputManager()
|
||||
.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key was just released (i.e: it stopped being pressed
|
||||
* during this frame).
|
||||
* Return true if the specified key was just released
|
||||
*
|
||||
*/
|
||||
export const wasKeyReleased = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
@@ -201,7 +187,7 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the name of the last key pressed in the game.
|
||||
* Return the name of the last key pressed in the game
|
||||
*/
|
||||
export const lastPressedKey = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
|
@@ -23,13 +23,12 @@ namespace gdjs {
|
||||
* variants and should default to their left variant values
|
||||
* if location is not specified.
|
||||
*/
|
||||
private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
|
||||
private _pressedKeys: Hashtable<boolean>;
|
||||
private _justPressedKeys: Hashtable<boolean>;
|
||||
private _releasedKeys: Hashtable<boolean>;
|
||||
private _lastPressedKey: float = 0;
|
||||
private _pressedMouseButtons: Array<boolean>;
|
||||
private _releasedMouseButtons: Array<boolean>;
|
||||
static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
|
||||
_pressedKeys: Hashtable<boolean>;
|
||||
_releasedKeys: Hashtable<boolean>;
|
||||
_lastPressedKey: float = 0;
|
||||
_pressedMouseButtons: Array<boolean>;
|
||||
_releasedMouseButtons: Array<boolean>;
|
||||
/**
|
||||
* The cursor X position (moved by mouse and touch events).
|
||||
*/
|
||||
@@ -80,7 +79,6 @@ namespace gdjs {
|
||||
|
||||
constructor() {
|
||||
this._pressedKeys = new Hashtable();
|
||||
this._justPressedKeys = new Hashtable();
|
||||
this._releasedKeys = new Hashtable();
|
||||
this._pressedMouseButtons = new Array(5);
|
||||
this._releasedMouseButtons = new Array(5);
|
||||
@@ -126,7 +124,6 @@ namespace gdjs {
|
||||
location
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, true);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, true);
|
||||
this._lastPressedKey = locationAwareKeyCode;
|
||||
}
|
||||
|
||||
@@ -143,34 +140,9 @@ namespace gdjs {
|
||||
location
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
this._releasedKeys.put(locationAwareKeyCode, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all keys that are currently pressed.
|
||||
* Note: if you want to discard pressed keys without considering them as
|
||||
* released, check `clearAllPressedKeys` instead.
|
||||
*/
|
||||
releaseAllPressedKeys(): void {
|
||||
for (const locationAwareKeyCode in this._pressedKeys.items) {
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
this._releasedKeys.put(locationAwareKeyCode, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored pressed keys without making the keys go through
|
||||
* the release state.
|
||||
* Note: prefer to use `releaseAllPressedKeys` instead, as it corresponds
|
||||
* to a normal key release.
|
||||
*/
|
||||
clearAllPressedKeys(): void {
|
||||
this._pressedKeys.clear();
|
||||
this._justPressedKeys.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location-aware code of the last key that was pressed.
|
||||
* @return The location-aware code of the last key pressed.
|
||||
@@ -180,21 +152,14 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the key corresponding to the location-aware keyCode is pressed
|
||||
* (either it was just pressed or is still held down).
|
||||
* Return true if the key corresponding to the location-aware keyCode is pressed.
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
isKeyPressed(locationAwareKeyCode: number): boolean {
|
||||
return !!this._pressedKeys.get(locationAwareKeyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the key corresponding to the location-aware keyCode
|
||||
* was just pressed during the last frame.
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
wasKeyJustPressed(locationAwareKeyCode: number): boolean {
|
||||
return !!this._justPressedKeys.get(locationAwareKeyCode);
|
||||
return (
|
||||
this._pressedKeys.containsKey(locationAwareKeyCode) &&
|
||||
this._pressedKeys.get(locationAwareKeyCode)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,7 +167,10 @@ namespace gdjs {
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
wasKeyReleased(locationAwareKeyCode: number) {
|
||||
return !!this._releasedKeys.get(locationAwareKeyCode);
|
||||
return (
|
||||
this._releasedKeys.containsKey(locationAwareKeyCode) &&
|
||||
this._releasedKeys.get(locationAwareKeyCode)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,7 +544,6 @@ namespace gdjs {
|
||||
this._startedTouches.length = 0;
|
||||
this._endedTouches.length = 0;
|
||||
this._releasedKeys.clear();
|
||||
this._justPressedKeys.clear();
|
||||
this._releasedMouseButtons.length = 0;
|
||||
this._mouseWheelDelta = 0;
|
||||
this._lastStartedTouchIndex = 0;
|
||||
@@ -597,6 +564,14 @@ namespace gdjs {
|
||||
return this.getMouseWheelDelta() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored pressed keys without making the keys go through
|
||||
* the release state.
|
||||
*/
|
||||
clearAllPressedKeys(): void {
|
||||
this._pressedKeys.clear();
|
||||
}
|
||||
|
||||
static _allTouchIds: Array<integer> = [];
|
||||
}
|
||||
}
|
||||
|
@@ -657,14 +657,6 @@ namespace gdjs {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.repeat) {
|
||||
// If `repeat` is true, this is not the first press of the key.
|
||||
// We only communicate the changes of states ("first" key down, key up)
|
||||
// to the manager, which then tracks the state of the key:
|
||||
// pressed, just pressed or released.
|
||||
return;
|
||||
}
|
||||
|
||||
manager.onKeyPressed(e.keyCode, e.location);
|
||||
};
|
||||
document.onkeyup = function (e) {
|
||||
|
@@ -405,7 +405,7 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.begin('objects (post-events)');
|
||||
}
|
||||
this._stepBehaviorsPostEvents();
|
||||
this._updateObjectsPostEvents();
|
||||
if (this._profiler) {
|
||||
this._profiler.end('objects (post-events)');
|
||||
}
|
||||
|
@@ -34,14 +34,9 @@ describe('gdjs.InputManager', () => {
|
||||
inputManager.onKeyPressed(33);
|
||||
expect(inputManager.getLastPressedKey()).to.be(33);
|
||||
expect(inputManager.isKeyPressed(32)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(30)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(30)).to.be(false);
|
||||
expect(inputManager.isKeyPressed(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(true);
|
||||
inputManager.onKeyReleased(32);
|
||||
expect(inputManager.isKeyPressed(32)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
expect(inputManager.wasKeyReleased(32)).to.be(true);
|
||||
expect(inputManager.anyKeyReleased()).to.be(true);
|
||||
expect(inputManager.anyKeyPressed()).to.be(true);
|
||||
@@ -51,14 +46,10 @@ describe('gdjs.InputManager', () => {
|
||||
expect(inputManager.anyKeyPressed()).to.be(true);
|
||||
expect(inputManager.anyKeyReleased()).to.be(false);
|
||||
expect(inputManager.isKeyPressed(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
|
||||
inputManager.onFrameEnded();
|
||||
inputManager.onKeyReleased(33);
|
||||
expect(inputManager.wasKeyReleased(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
expect(inputManager.anyKeyPressed()).to.be(false);
|
||||
expect(inputManager.anyKeyReleased()).to.be(true);
|
||||
inputManager.onFrameEnded();
|
||||
@@ -70,16 +61,12 @@ describe('gdjs.InputManager', () => {
|
||||
expect(inputManager.getLastPressedKey()).to.be(2016);
|
||||
expect(inputManager.isKeyPressed(2016)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(1016)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(2016)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(1016)).to.be(false);
|
||||
|
||||
// Pressed Control with no location - expect to default to left.
|
||||
inputManager.onKeyPressed(17);
|
||||
expect(inputManager.getLastPressedKey()).to.be(1017);
|
||||
expect(inputManager.isKeyPressed(1017)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(2017)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(1017)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(2017)).to.be(false);
|
||||
|
||||
inputManager.onKeyReleased(16, 2);
|
||||
expect(inputManager.wasKeyReleased(2016)).to.be(true);
|
||||
|
@@ -285,7 +285,6 @@ interface Variable {
|
||||
boolean RenameChild([Const] DOMString oldName, [Const] DOMString newName);
|
||||
[Value] VectorString GetAllChildrenNames();
|
||||
void RemoveRecursively([Const, Ref] Variable variableToRemove);
|
||||
void ClearChildren();
|
||||
|
||||
[Ref] Variable GetAtIndex([Const] unsigned long index);
|
||||
[Ref] Variable PushNew();
|
||||
@@ -1175,7 +1174,6 @@ interface PropertyDescriptor {
|
||||
[Const, Ref] DOMString GetDescription();
|
||||
[Ref] PropertyDescriptor SetGroup([Const] DOMString label);
|
||||
[Const, Ref] DOMString GetGroup();
|
||||
[Ref] PropertyDescriptor ClearChoices();
|
||||
[Ref] PropertyDescriptor AddChoice([Const] DOMString value, [Const] DOMString label);
|
||||
[Const, Ref] VectorPropertyDescriptorChoice GetChoices();
|
||||
[Ref] PropertyDescriptor AddExtraInfo([Const] DOMString type);
|
||||
@@ -2147,9 +2145,6 @@ interface BehaviorMetadata {
|
||||
boolean IsHidden();
|
||||
[Ref] BehaviorMetadata SetHidden();
|
||||
|
||||
boolean IsRelevantForChildObjects();
|
||||
[Ref] BehaviorMetadata MarkAsIrrelevantForChildObjects();
|
||||
|
||||
QuickCustomization_Visibility GetQuickCustomizationVisibility();
|
||||
[Ref] BehaviorMetadata SetQuickCustomizationVisibility(QuickCustomization_Visibility visibility);
|
||||
|
||||
|
4
GDevelop.js/types.d.ts
vendored
4
GDevelop.js/types.d.ts
vendored
@@ -323,7 +323,6 @@ export class Variable extends EmscriptenObject {
|
||||
renameChild(oldName: string, newName: string): boolean;
|
||||
getAllChildrenNames(): VectorString;
|
||||
removeRecursively(variableToRemove: Variable): void;
|
||||
clearChildren(): void;
|
||||
getAtIndex(index: number): Variable;
|
||||
pushNew(): Variable;
|
||||
removeAtIndex(index: number): void;
|
||||
@@ -972,7 +971,6 @@ export class PropertyDescriptor extends EmscriptenObject {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): PropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): PropertyDescriptor;
|
||||
addChoice(value: string, label: string): PropertyDescriptor;
|
||||
getChoices(): VectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): PropertyDescriptor;
|
||||
@@ -1673,8 +1671,6 @@ export class BehaviorMetadata extends EmscriptenObject {
|
||||
setPrivate(): BehaviorMetadata;
|
||||
isHidden(): boolean;
|
||||
setHidden(): BehaviorMetadata;
|
||||
isRelevantForChildObjects(): boolean;
|
||||
markAsIrrelevantForChildObjects(): BehaviorMetadata;
|
||||
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
|
||||
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): BehaviorMetadata;
|
||||
setOpenFullEditorLabel(label: string): BehaviorMetadata;
|
||||
|
@@ -33,8 +33,6 @@ declare class gdBehaviorMetadata {
|
||||
setPrivate(): gdBehaviorMetadata;
|
||||
isHidden(): boolean;
|
||||
setHidden(): gdBehaviorMetadata;
|
||||
isRelevantForChildObjects(): boolean;
|
||||
markAsIrrelevantForChildObjects(): gdBehaviorMetadata;
|
||||
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
|
||||
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): gdBehaviorMetadata;
|
||||
setOpenFullEditorLabel(label: string): gdBehaviorMetadata;
|
||||
|
@@ -11,7 +11,6 @@ declare class gdPropertyDescriptor {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): gdPropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): gdPropertyDescriptor;
|
||||
addChoice(value: string, label: string): gdPropertyDescriptor;
|
||||
getChoices(): gdVectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): gdPropertyDescriptor;
|
||||
|
@@ -28,7 +28,6 @@ declare class gdVariable {
|
||||
renameChild(oldName: string, newName: string): boolean;
|
||||
getAllChildrenNames(): gdVectorString;
|
||||
removeRecursively(variableToRemove: gdVariable): void;
|
||||
clearChildren(): void;
|
||||
getAtIndex(index: number): gdVariable;
|
||||
pushNew(): gdVariable;
|
||||
removeAtIndex(index: number): void;
|
||||
|
@@ -33,6 +33,9 @@ Images resources, GDJS Runtime, extensions will be copied in resources, and [lib
|
||||
|
||||
You can run the standalone app with Electron. **Make sure that you've launched `npm start` (or `yarn start`) in the `app` folder before** (see above) and **keep it running** (in development, the app is served from a local server, even for the standalone app).
|
||||
|
||||
> Note for Windows: With **Node.js 14 or older**, there is an error related to `git-sh-setup` when running npm install.
|
||||
> To solve this problem: add [this folder to your path environment variable](https://stackoverflow.com/questions/49256190/how-to-fix-git-sh-setup-file-not-found-in-windows) **OR** run `npm install` in newIDE/electron-app/app **before** npm install in newIDE/electron-app.
|
||||
|
||||
```bash
|
||||
cd newIDE/app && npm start # Be sure to have this running in another terminal, before the rest!
|
||||
|
||||
@@ -78,11 +81,17 @@ It's pretty easy to create new themes. Check the [README about themes](./README-
|
||||
|
||||
### Development of the game engine or extensions
|
||||
|
||||
- If you want to create/modify _extensions_, check the [README about extensions](./README-extensions.md) for step-by-step explanations to get started in 5 minutes.
|
||||
- The _game engine core_ ([GDJS](https://github.com/4ian/GDevelop/tree/master/GDJS)) is in [GDJS/Runtime folder](https://github.com/4ian/GDevelop/tree/master/GDJS/Runtime).
|
||||
Make sure to have the standalone app running with Electron.
|
||||
|
||||
If you modify any file while the editor is running, a watcher will _automatically rebuild_ the engine (look at the console to be sure).
|
||||
You can then _launch a preview_ in GDevelop.
|
||||
- If you want to create/modify _extensions_, check the [README about extensions](./README-extensions.md) for step-by-step explanations to get started in 5 minutes.
|
||||
|
||||
- The _game engine core_ ([GDJS](https://github.com/4ian/GDevelop/tree/master/GDJS)) is in [GDJS/Runtime folder](https://github.com/4ian/GDevelop/tree/master/GDJS/Runtime).
|
||||
|
||||
If you modify any file while the IDE is running with Electron, a watcher will _automatically import_ your changes (look at the console to be sure).
|
||||
|
||||
You can then _launch a preview_ in GDevelop (again, be sure to be using [the standalone app running with Electron](https://github.com/4ian/GDevelop/blob/master/newIDE/README.md#development-of-the-standalone-app) to be sure to have your changes reflected immediately).
|
||||
|
||||
> If you deactivated the watcher in preferences, run the `import-GDJS-Runtime.js` script manually (`cd newIDE/app/scripts` then `node import-GDJS-Runtime.js`) after every change, before launching a preview.
|
||||
|
||||
### Recommended tools for development
|
||||
|
||||
|
590
newIDE/app/package-lock.json
generated
590
newIDE/app/package-lock.json
generated
@@ -81,13 +81,10 @@
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-webpack5": "7.4.6",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"@types/node": "22.18.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
@@ -99,7 +96,6 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"serve-handler": "6.1.6",
|
||||
"shelljs": "0.8.4",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-mock": "4.3.0",
|
||||
@@ -7069,12 +7065,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"dev": true,
|
||||
@@ -8063,12 +8053,6 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-common/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/core-common/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
@@ -8391,12 +8375,6 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-server/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/core-server/node_modules/fs-extra": {
|
||||
"version": "11.1.1",
|
||||
"dev": true,
|
||||
@@ -8568,12 +8546,6 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/csf": {
|
||||
"version": "0.1.1",
|
||||
"dev": true,
|
||||
@@ -8871,12 +8843,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack/node_modules/fs-extra": {
|
||||
"version": "11.1.1",
|
||||
"dev": true,
|
||||
@@ -9101,18 +9067,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/react/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/react/node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"dev": true,
|
||||
@@ -10101,12 +10055,8 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
|
||||
"integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
"version": "16.18.12",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
@@ -12249,15 +12199,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
@@ -12766,18 +12712,29 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"version": "3.5.3",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
@@ -13196,171 +13153,6 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"dev": true,
|
||||
@@ -16987,30 +16779,6 @@
|
||||
"webpack": "^5.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"dev": true,
|
||||
@@ -17082,18 +16850,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -18608,9 +18364,8 @@
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@@ -22858,6 +22613,14 @@
|
||||
"shell-quote": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor/node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy-universal-dotenv": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -25157,12 +24920,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
@@ -27043,30 +26800,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"dev": true,
|
||||
@@ -28785,18 +28518,6 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -28900,6 +28621,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
@@ -29382,16 +29111,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recast": {
|
||||
@@ -30362,87 +30089,6 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
|
||||
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.0.0",
|
||||
"content-disposition": "0.5.2",
|
||||
"mime-types": "2.1.18",
|
||||
"minimatch": "3.1.2",
|
||||
"path-is-inside": "1.0.2",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"range-parser": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/content-disposition": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "~1.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/path-to-regexp": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
|
||||
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/range-parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
"version": "1.9.1",
|
||||
"dev": true,
|
||||
@@ -30552,18 +30198,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shelljs": {
|
||||
"version": "0.8.4",
|
||||
"dev": true,
|
||||
@@ -31696,42 +31330,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"dev": true,
|
||||
@@ -31876,18 +31474,6 @@
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/yaml": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
@@ -32325,15 +31911,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
@@ -32542,11 +32119,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
@@ -32727,42 +32299,6 @@
|
||||
"webpack-virtual-modules": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
@@ -33320,30 +32856,6 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ipaddr.js": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
@@ -33373,18 +32885,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
|
@@ -17,13 +17,10 @@
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-webpack5": "7.4.6",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"@types/node": "22.18.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
@@ -35,7 +32,6 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"serve-handler": "6.1.6",
|
||||
"shelljs": "0.8.4",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-mock": "4.3.0",
|
||||
@@ -120,7 +116,7 @@
|
||||
"import-resources": "npm run import-zipped-external-editors && npm run build-theme-resources && cd scripts && node import-libGD.js && node import-GDJS-Runtime.js && node import-monaco-editor.js && node import-zipped-external-libs.js",
|
||||
"make-version-metadata": "cd scripts && node make-version-metadata.js",
|
||||
"make-service-worker": "cd scripts && node make-service-worker.js",
|
||||
"start": "npm run import-resources && npm run make-version-metadata && concurrently \"react-app-rewired start\" \"node scripts/watch-serve-GDJS-runtime.js\" -n \"editor,game engine\"",
|
||||
"start": "npm run import-resources && npm run make-version-metadata && react-app-rewired start",
|
||||
"electron-app": "cd ../electron-app && npm run start",
|
||||
"build": "npm run import-resources && npm run make-version-metadata && react-app-rewired build && npm run make-service-worker",
|
||||
"format": "prettier --write \"src/!(locales)/**/*.js\"",
|
||||
|
@@ -632,56 +632,6 @@ const generateExtensionRawText = (
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
{ text: '' },
|
||||
...extension
|
||||
.getExtensionEffectTypes()
|
||||
.toJSArray()
|
||||
.map(
|
||||
/**
|
||||
* @param {string} effectType
|
||||
* @returns {RawText}
|
||||
*/
|
||||
effectType => {
|
||||
const effectMetadata = extension.getEffectMetadata(effectType);
|
||||
const properties = effectMetadata.getProperties();
|
||||
const propertyNames = properties.keys().toJSArray();
|
||||
|
||||
return {
|
||||
text: [
|
||||
`### Effect "${effectMetadata.getFullName()}"`,
|
||||
'',
|
||||
`${effectMetadata.getDescription().replace(/\n/g, ' ')}`,
|
||||
'',
|
||||
...[
|
||||
effectMetadata.isMarkedAsUnique()
|
||||
? 'This effect can be added only once on a layer.'
|
||||
: null,
|
||||
effectMetadata.isMarkedAsOnlyWorkingFor2D()
|
||||
? effectMetadata.isMarkedAsNotWorkingForObjects()
|
||||
? 'This effect is for 2D layers only.'
|
||||
: 'This effect is for 2D layers or objects only.'
|
||||
: null,
|
||||
effectMetadata.isMarkedAsOnlyWorkingFor3D()
|
||||
? 'This effect is for 3D layers only.'
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
'',
|
||||
`Properties of this effect are:`,
|
||||
'',
|
||||
...propertyNames.map(propertyName => {
|
||||
const propertyMetadata = properties.get(propertyName);
|
||||
return [
|
||||
propertyMetadata.getDescription()
|
||||
? `- **${propertyMetadata.getLabel()}**: ${propertyMetadata.getDescription()}.`
|
||||
: `- **${propertyMetadata.getLabel()}**.`,
|
||||
`Default value is \`${propertyMetadata.getValue()}\`. For events, write: \`"${propertyName}"\`.`,
|
||||
].join(' ');
|
||||
}),
|
||||
'',
|
||||
].join(`\n`),
|
||||
};
|
||||
}
|
||||
),
|
||||
generateExtensionFooterText({ extension }),
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
@@ -1,239 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/*
|
||||
* This script watches GDJS Runtime and Extensions files for changes
|
||||
* and automatically runs import-GDJS-Runtime.js when changes are detected.
|
||||
*
|
||||
* Usage: node watch-GDJS-runtime.js
|
||||
* Stop with Ctrl+C
|
||||
*/
|
||||
|
||||
const chokidar = require('chokidar');
|
||||
const debounce = require('lodash.debounce');
|
||||
const child_process = require('child_process');
|
||||
const path = require('path');
|
||||
const handler = require('serve-handler');
|
||||
const http = require('http');
|
||||
|
||||
const PORT = 5002;
|
||||
|
||||
const gdevelopRootPath = path.join(__dirname, '..', '..', '..');
|
||||
const gdjsRootPath = path.join(__dirname, '..', 'resources', 'GDJS');
|
||||
|
||||
// Serve the built files for GDJS.
|
||||
const server = http.createServer((request, response) => {
|
||||
return handler(request, response, {
|
||||
public: gdjsRootPath,
|
||||
cleanUrls: false,
|
||||
headers: [
|
||||
{
|
||||
source: '**/*',
|
||||
headers: [
|
||||
// Tell the browser not to cache the files:
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'no-cache',
|
||||
},
|
||||
// Allow CORS because the web-app is served from a different origin:
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Headers',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Credentials',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Private-Network',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Launch the import-GDJS-Runtime script.
|
||||
* Cleaning the GDJS output folder and copying sources are both
|
||||
* skipped to speed up the build.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const importGDJSRuntime = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = performance.now();
|
||||
const scriptPath = path.join(__dirname, 'import-GDJS-Runtime.js');
|
||||
|
||||
console.log('🔄 Running import-GDJS-Runtime.js...');
|
||||
|
||||
child_process.exec(
|
||||
`node "${scriptPath}" --skip-clean --skip-sources`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`❌ GDJS Runtime update error: ${error.message}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`⚠️ GDJS Runtime update warnings: ${stderr}`);
|
||||
}
|
||||
|
||||
const duration = (performance.now() - startTime).toFixed(0);
|
||||
console.log(`✅ GDJS Runtime updated in ${duration}ms.`);
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for file changes, debounced to avoid running too frequently
|
||||
*/
|
||||
const onWatchEvent = debounce(
|
||||
(event, filePath) => {
|
||||
const eventName = event || 'unknown-event';
|
||||
const resolvedFilename =
|
||||
path.relative(gdevelopRootPath, filePath) || 'unknown-file';
|
||||
|
||||
console.log(
|
||||
`📁 Detected "${eventName}" in ${resolvedFilename}, updating GDJS Runtime...`
|
||||
);
|
||||
|
||||
importGDJSRuntime().catch(error => {
|
||||
console.error('❌ Failed to update GDJS Runtime:', error.message);
|
||||
});
|
||||
},
|
||||
100 // Avoid running the script too much in case multiple changes are fired at the same time
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up file watchers for GDJS and Extensions sources
|
||||
*/
|
||||
const setupWatcher = () => {
|
||||
const watchPaths = [
|
||||
path.join(gdevelopRootPath, 'GDJS', 'Runtime'),
|
||||
path.join(gdevelopRootPath, 'Extensions'),
|
||||
];
|
||||
|
||||
const watcher = chokidar.watch(watchPaths, {
|
||||
ignored: (filePath, stats) => {
|
||||
if (!stats) return false;
|
||||
if (!stats.isFile()) return false;
|
||||
|
||||
const ignoredExtensions = [
|
||||
'.d.ts',
|
||||
'.map',
|
||||
'.md',
|
||||
'.png',
|
||||
'.svg',
|
||||
'.svgz',
|
||||
'.txt',
|
||||
'.DS_Store',
|
||||
'.prettierrc',
|
||||
];
|
||||
const ignoredFolderNames = ['tests', 'example', 'diagrams'];
|
||||
|
||||
if (ignoredExtensions.some(ext => filePath.endsWith(ext))) return true;
|
||||
if (
|
||||
ignoredFolderNames.some(
|
||||
folder =>
|
||||
filePath.includes(folder + '/') || filePath.includes(folder + '\\')
|
||||
)
|
||||
)
|
||||
return true;
|
||||
|
||||
// Uncomment to log all watched files:
|
||||
// console.log('Watched file path: ' + filePath);
|
||||
return false;
|
||||
},
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100,
|
||||
pollInterval: 50,
|
||||
},
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
// Set up event handlers:
|
||||
const startTime = performance.now();
|
||||
watcher
|
||||
.on('all', onWatchEvent)
|
||||
.on('error', error => {
|
||||
console.error(
|
||||
'❌ GDJS watcher error. Please try to relaunch it. Full error is:',
|
||||
error
|
||||
);
|
||||
})
|
||||
.on('ready', () => {
|
||||
const duration = (performance.now() - startTime).toFixed(2);
|
||||
console.log(`✅ GDJS watcher ready in ${duration}ms.`);
|
||||
console.log(
|
||||
`ℹ️ Watching GDJS engine files for changes in:\n${watchPaths
|
||||
.map(path => `- ${path}`)
|
||||
.join('\n')}.`
|
||||
);
|
||||
});
|
||||
return watcher;
|
||||
};
|
||||
|
||||
const watcher = setupWatcher();
|
||||
|
||||
// Handle graceful shutdown:
|
||||
const shutdown = () => {
|
||||
console.log('\nℹ️ Shutting GDJS Runtime file watcher and server...');
|
||||
|
||||
server.close(() => {
|
||||
console.log('✅ GDJS Runtime server closed.');
|
||||
});
|
||||
|
||||
watcher
|
||||
.close()
|
||||
.then(() => {
|
||||
console.log('✅ GDJS Runtime watcher closed.');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Error closing GDJS Runtime watcher:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle Ctrl+C and other termination signals.
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGHUP', shutdown);
|
||||
|
||||
// Start the server (used to serve the game engine to the web-app).
|
||||
server.on('error', error => {
|
||||
// @ts-ignore
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.error(
|
||||
`❌ Error: Port ${PORT} is already in use and can't be used to serve the game engine to the web-app.`
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
else if (error.code === 'EACCES') {
|
||||
console.error(
|
||||
`❌ Error: Permission denied to bind to port ${PORT} and can't be used to serve the game engine to the web-app.`
|
||||
);
|
||||
} else {
|
||||
console.error('❌ Game engine server error:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
server.listen(PORT, () => {
|
||||
console.log(
|
||||
`ℹ️ GDJS Runtime served for the web-app at http://localhost:${PORT}.`
|
||||
);
|
||||
});
|
@@ -49,11 +49,10 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
};
|
||||
|
||||
export const getDefaultAiConfigurationPresetId = (
|
||||
mode: 'chat' | 'agent',
|
||||
aiConfigurationPresetsWithAvailability: Array<AiConfigurationPresetWithAvailability>
|
||||
): string => {
|
||||
const defaultPresetWithAvailability = aiConfigurationPresetsWithAvailability.find(
|
||||
preset => preset.isDefault && preset.mode === mode
|
||||
preset => preset.isDefault
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -92,12 +92,6 @@ export const ChatMessages = React.memo<Props>(function ChatMessages({
|
||||
const feedbackKey = `${messageIndex}-${messageContentIndex}`;
|
||||
const currentFeedback = messageFeedbacks[feedbackKey];
|
||||
|
||||
const trimmedText = messageContent.text.trim();
|
||||
if (!trimmedText) {
|
||||
// Sometimes the AI can return an empty string or a string with just a line break.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Line key={key} justifyContent="flex-start">
|
||||
<ChatBubble
|
||||
@@ -165,7 +159,7 @@ export const ChatMessages = React.memo<Props>(function ChatMessages({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ChatMarkdownText source={trimmedText} />
|
||||
<ChatMarkdownText source={messageContent.text} />
|
||||
</ChatBubble>
|
||||
</Line>
|
||||
);
|
||||
|
@@ -18,9 +18,7 @@ import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import classes from './FunctionCallRow.module.css';
|
||||
import {
|
||||
editorFunctions,
|
||||
editorFunctionsWithoutProject,
|
||||
type EditorFunction,
|
||||
type EditorFunctionWithoutProject,
|
||||
type EditorCallbacks,
|
||||
} from '../../EditorFunctions';
|
||||
import Link from '../../UI/Link';
|
||||
@@ -86,10 +84,8 @@ export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
|
||||
!!editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'working';
|
||||
|
||||
const editorFunction: EditorFunction | EditorFunctionWithoutProject | null =
|
||||
editorFunctions[functionCall.name] ||
|
||||
editorFunctionsWithoutProject[functionCall.name] ||
|
||||
null;
|
||||
const editorFunction: EditorFunction | null =
|
||||
editorFunctions[functionCall.name] || null;
|
||||
let text;
|
||||
let details;
|
||||
let hasDetailsToShow = false;
|
||||
|
@@ -466,10 +466,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
|
||||
const chosenOrDefaultAiConfigurationPresetId =
|
||||
aiConfigurationPresetId ||
|
||||
getDefaultAiConfigurationPresetId(
|
||||
newAiRequestMode,
|
||||
aiConfigurationPresetsWithAvailability
|
||||
);
|
||||
getDefaultAiConfigurationPresetId(aiConfigurationPresetsWithAvailability);
|
||||
|
||||
if (!aiRequest) {
|
||||
return (
|
||||
|
@@ -50,15 +50,11 @@ import {
|
||||
sendAiRequestMessageSent,
|
||||
sendAiRequestStarted,
|
||||
} from '../Utils/Analytics/EventSender';
|
||||
import {
|
||||
type ExampleShortHeader,
|
||||
listAllExamples,
|
||||
} from '../Utils/GDevelopServices/Example';
|
||||
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
|
||||
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
import { getAiConfigurationPresetsWithAvailability } from './AiConfiguration';
|
||||
import { type CreateProjectResult } from '../Utils/UseCreateProject';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -142,7 +138,7 @@ const useProcessFunctionCalls = ({
|
||||
ignore?: boolean,
|
||||
|}
|
||||
) => {
|
||||
if (!selectedAiRequest) return;
|
||||
if (!project || !selectedAiRequest) return;
|
||||
|
||||
addEditorFunctionCallResults(
|
||||
selectedAiRequest.id,
|
||||
@@ -338,15 +334,13 @@ type Props = {|
|
||||
storageProvider: ?StorageProvider,
|
||||
setToolbar: (?React.Node) => void,
|
||||
i18n: I18nType,
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onCreateProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<CreateProjectResult>,
|
||||
onCreateEmptyProject: (
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
onOpenLayout: (
|
||||
sceneName: string,
|
||||
options: {|
|
||||
@@ -416,8 +410,8 @@ export const AskAiEditor = React.memo<Props>(
|
||||
fileMetadata,
|
||||
storageProvider,
|
||||
i18n,
|
||||
onCreateProjectFromExample,
|
||||
onCreateEmptyProject,
|
||||
onCreateProjectFromExample,
|
||||
onOpenLayout,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
@@ -428,57 +422,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const onCreateProject = React.useCallback(
|
||||
async ({
|
||||
name,
|
||||
exampleSlug,
|
||||
}: {|
|
||||
name: string,
|
||||
exampleSlug: string | null,
|
||||
|}) => {
|
||||
const newProjectSetup: NewProjectSetup = {
|
||||
projectName: name,
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
};
|
||||
|
||||
if (exampleSlug) {
|
||||
const { exampleShortHeaders } = await listAllExamples();
|
||||
const exampleShortHeader = exampleShortHeaders.find(
|
||||
header => header.slug === exampleSlug
|
||||
);
|
||||
if (exampleShortHeader) {
|
||||
const { createdProject } = await onCreateProjectFromExample(
|
||||
exampleShortHeader,
|
||||
newProjectSetup,
|
||||
i18n,
|
||||
false
|
||||
);
|
||||
return { exampleSlug, createdProject };
|
||||
}
|
||||
|
||||
// The example was not found - still create an empty project.
|
||||
}
|
||||
|
||||
const { createdProject } = await onCreateEmptyProject({
|
||||
projectName: name,
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
});
|
||||
|
||||
return { exampleSlug: null, createdProject };
|
||||
},
|
||||
[onCreateProjectFromExample, onCreateEmptyProject, i18n]
|
||||
);
|
||||
|
||||
const editorCallbacks: EditorCallbacks = React.useMemo(
|
||||
() => ({
|
||||
onOpenLayout,
|
||||
onCreateProject,
|
||||
}),
|
||||
[onOpenLayout, onCreateProject]
|
||||
[onOpenLayout]
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -554,6 +502,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setLastSendError,
|
||||
} = aiRequestStorage;
|
||||
|
||||
const {
|
||||
createAiProject,
|
||||
renderCreateAiProjectDialog,
|
||||
} = useCreateAiProjectDialog();
|
||||
|
||||
const updateToolbar = React.useCallback(
|
||||
() => {
|
||||
if (setToolbar) {
|
||||
@@ -644,6 +597,29 @@ export const AskAiEditor = React.memo<Props>(
|
||||
} = newAiRequestOptions;
|
||||
startNewAiRequest(null);
|
||||
|
||||
// If no project is opened, create a new empty one if the request is for
|
||||
// the AI agent.
|
||||
if (mode === 'agent' && !project) {
|
||||
try {
|
||||
console.info(
|
||||
'No project opened, opening the dialog to create a new project.'
|
||||
);
|
||||
const result = await createAiProject();
|
||||
if (result === 'canceled') {
|
||||
return;
|
||||
}
|
||||
console.info('New project created - starting AI request.');
|
||||
startNewAiRequest({
|
||||
mode,
|
||||
userRequest,
|
||||
aiConfigurationPresetId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating a new empty project:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the user has enough credits to pay for the request, or ask them
|
||||
// to buy some more.
|
||||
let payWithCredits = false;
|
||||
@@ -694,7 +670,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
fileMetadata,
|
||||
storageProviderName,
|
||||
mode,
|
||||
toolsVersion: 'v3',
|
||||
toolsVersion: 'v2',
|
||||
aiConfiguration: {
|
||||
presetId: aiConfigurationPresetId,
|
||||
},
|
||||
@@ -758,6 +734,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setSendingAiRequest,
|
||||
upToDateSelectedAiRequestId,
|
||||
updateAiRequest,
|
||||
createAiProject,
|
||||
newAiRequestOptions,
|
||||
onOpenAskAi,
|
||||
]
|
||||
@@ -897,11 +874,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
selectedAiRequest &&
|
||||
createdSceneNames &&
|
||||
createdSceneNames.length > 0
|
||||
) {
|
||||
if (selectedAiRequest && createdSceneNames) {
|
||||
onOpenAskAi({
|
||||
mode: selectedAiRequest.mode || 'agent',
|
||||
aiRequestId: selectedAiRequestId,
|
||||
@@ -1044,6 +1017,10 @@ export const AskAiEditor = React.memo<Props>(
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
{renderCreateAiProjectDialog({
|
||||
onCreateEmptyProject,
|
||||
onCreateProjectFromExample,
|
||||
})}
|
||||
<AskAiHistory
|
||||
open={isHistoryOpen}
|
||||
onClose={onCloseHistory}
|
||||
@@ -1078,8 +1055,8 @@ export const renderAskAiEditorContainer = (
|
||||
storageProvider={props.storageProvider}
|
||||
setToolbar={props.setToolbar}
|
||||
isActive={props.isActive}
|
||||
onCreateProjectFromExample={props.onCreateProjectFromExample}
|
||||
onCreateEmptyProject={props.onCreateEmptyProject}
|
||||
onCreateProjectFromExample={props.onCreateProjectFromExample}
|
||||
onOpenLayout={props.onOpenLayout}
|
||||
onSceneEventsModifiedOutsideEditor={
|
||||
props.onSceneEventsModifiedOutsideEditor
|
||||
|
146
newIDE/app/src/AiGeneration/UseCreateAiProjectDialog.js
Normal file
146
newIDE/app/src/AiGeneration/UseCreateAiProjectDialog.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import EmptyAndStartingPointProjects from '../ProjectCreation/EmptyAndStartingPointProjects';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
|
||||
import { generateProjectName } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { Spacer } from '../UI/Grid';
|
||||
|
||||
type RenderCreateAiProjectDialogProps = {
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onCreateProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
};
|
||||
|
||||
type CreateAiProjectDialogProps = {
|
||||
onClose: () => void,
|
||||
onSelectExampleShortHeader: (
|
||||
exampleShortHeader: ExampleShortHeader
|
||||
) => Promise<void>,
|
||||
onSelectEmptyProject: () => Promise<void>,
|
||||
};
|
||||
|
||||
const CreateAiProjectDialog = ({
|
||||
onClose,
|
||||
onSelectExampleShortHeader,
|
||||
onSelectEmptyProject,
|
||||
}: CreateAiProjectDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Ok! Choose a game style to start with</Trans>}
|
||||
id="ai-project-dialog"
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
flexColumnBody
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Spacer />
|
||||
<EmptyAndStartingPointProjects
|
||||
onSelectExampleShortHeader={exampleShortHeader => {
|
||||
onSelectExampleShortHeader(exampleShortHeader);
|
||||
}}
|
||||
onSelectEmptyProject={() => {
|
||||
onSelectEmptyProject();
|
||||
}}
|
||||
/>
|
||||
{/* Use a spacer to avoid extra scrollbars when template tiles are hovered. */}
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type CreateAiProjectResult = 'canceled' | 'created';
|
||||
|
||||
export const useCreateAiProjectDialog = () => {
|
||||
const [createPromise, setCreatePromise] = React.useState<null | {|
|
||||
onFinished: (result: CreateAiProjectResult) => void,
|
||||
promise: Promise<CreateAiProjectResult>,
|
||||
|}>(null);
|
||||
|
||||
const createAiProject: () => Promise<CreateAiProjectResult> = React.useCallback(
|
||||
() => {
|
||||
if (createPromise) {
|
||||
return createPromise.promise;
|
||||
}
|
||||
|
||||
// Make a promise that we can resolve later from the creation dialog.
|
||||
let resolve: (result: CreateAiProjectResult) => void = () => {};
|
||||
const promise = new Promise(resolveFn => {
|
||||
resolve = resolveFn;
|
||||
});
|
||||
|
||||
setCreatePromise({
|
||||
onFinished: (result: CreateAiProjectResult) => {
|
||||
setCreatePromise(null);
|
||||
resolve(result);
|
||||
},
|
||||
promise,
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
[createPromise]
|
||||
);
|
||||
|
||||
return {
|
||||
createAiProject,
|
||||
renderCreateAiProjectDialog: (props: RenderCreateAiProjectDialogProps) => {
|
||||
if (!createPromise) return null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<CreateAiProjectDialog
|
||||
onClose={() => {
|
||||
createPromise.onFinished('canceled');
|
||||
}}
|
||||
onSelectExampleShortHeader={async exampleShortHeader => {
|
||||
const newProjectSetup: NewProjectSetup = {
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
};
|
||||
await props.onCreateProjectFromExample(
|
||||
exampleShortHeader,
|
||||
newProjectSetup,
|
||||
i18n,
|
||||
false // isQuickCustomization
|
||||
);
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
onSelectEmptyProject={async () => {
|
||||
await props.onCreateEmptyProject({
|
||||
projectName: generateProjectName('AI starter'),
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
});
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
@@ -51,7 +51,6 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'nothing-found',
|
||||
message: 'No assets found.',
|
||||
createdObjects: [],
|
||||
assetShortHeader: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,10 +58,9 @@ export const useSearchAndInstallAsset = ({
|
||||
// For now, we just return the first asset.
|
||||
const chosenResult = assetSearch.results[0];
|
||||
if (!chosenResult) throw new Error('No asset found.');
|
||||
const assetShortHeader = chosenResult.asset;
|
||||
|
||||
const installOutput = await installAsset({
|
||||
assetShortHeader,
|
||||
assetShortHeader: chosenResult.asset,
|
||||
objectsContainer: scene.getObjects(),
|
||||
requestedObjectName: objectName,
|
||||
});
|
||||
@@ -72,7 +70,6 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'error',
|
||||
message: 'Asset found but failed to install asset.',
|
||||
createdObjects: [],
|
||||
assetShortHeader: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,7 +77,6 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'asset-installed',
|
||||
message: 'Asset installed successfully.',
|
||||
createdObjects: installOutput.createdObjects,
|
||||
assetShortHeader,
|
||||
};
|
||||
},
|
||||
[installAsset, profile, getAuthorizationHeader]
|
||||
|
@@ -155,7 +155,6 @@ export const getBundleTiles = ({
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}: {|
|
||||
allBundleListingDatas: ?Array<BundleListingData>,
|
||||
displayedBundleListingDatas: ?Array<BundleListingData>,
|
||||
@@ -163,14 +162,12 @@ export const getBundleTiles = ({
|
||||
receivedBundles: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
onlyShowAssets?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allBundleListingDatas ||
|
||||
!displayedBundleListingDatas ||
|
||||
!onBundleSelection ||
|
||||
hasAssetFiltersApplied ||
|
||||
onlyShowAssets
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
|
@@ -580,7 +580,6 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}),
|
||||
[
|
||||
allBundleListingDatas,
|
||||
@@ -588,7 +587,6 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -33,7 +33,6 @@ type Props = {|
|
||||
id?: string,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
behaviorShortHeader: BehaviorShortHeader,
|
||||
matches: ?Array<SearchMatch>,
|
||||
onChoose: () => void,
|
||||
@@ -46,7 +45,6 @@ export const BehaviorListItem = ({
|
||||
id,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
behaviorShortHeader,
|
||||
matches,
|
||||
onChoose,
|
||||
@@ -55,28 +53,20 @@ export const BehaviorListItem = ({
|
||||
platform,
|
||||
}: Props) => {
|
||||
const alreadyAdded = objectBehaviorsTypes.includes(behaviorShortHeader.type);
|
||||
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
platform,
|
||||
behaviorShortHeader.type
|
||||
);
|
||||
// An empty object type means the base object, i.e: any object.
|
||||
const isObjectCompatible =
|
||||
// An empty object type means the base object, i.e: any object.
|
||||
(!behaviorShortHeader.objectType ||
|
||||
objectType === behaviorShortHeader.objectType) &&
|
||||
(!isChildObject || behaviorMetadata.isRelevantForChildObjects()) &&
|
||||
behaviorShortHeader.allRequiredBehaviorTypes.every(requiredBehaviorType => {
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
platform,
|
||||
requiredBehaviorType
|
||||
);
|
||||
return (
|
||||
(!isChildObject || behaviorMetadata.isRelevantForChildObjects()) &&
|
||||
(!behaviorMetadata.isHidden() ||
|
||||
objectBehaviorsTypes.includes(requiredBehaviorType))
|
||||
!behaviorMetadata.isHidden() ||
|
||||
objectBehaviorsTypes.includes(requiredBehaviorType)
|
||||
);
|
||||
});
|
||||
|
||||
const isEngineCompatible = isCompatibleWithGDevelopVersion(
|
||||
getIDEVersion(),
|
||||
behaviorShortHeader.gdevelopVersion
|
||||
|
@@ -62,7 +62,6 @@ type Props = {|
|
||||
project: gdProject,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
installedBehaviorMetadataList: Array<BehaviorShortHeader>,
|
||||
deprecatedBehaviorMetadataList: Array<BehaviorShortHeader>,
|
||||
onInstall: (behaviorShortHeader: BehaviorShortHeader) => Promise<boolean>,
|
||||
@@ -77,7 +76,6 @@ export const BehaviorStore = ({
|
||||
project,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
installedBehaviorMetadataList,
|
||||
deprecatedBehaviorMetadataList,
|
||||
onInstall,
|
||||
@@ -318,7 +316,6 @@ export const BehaviorStore = ({
|
||||
key={behaviorShortHeader.type}
|
||||
objectType={objectType}
|
||||
objectBehaviorsTypes={objectBehaviorsTypes}
|
||||
isChildObject={isChildObject}
|
||||
onHeightComputed={onHeightComputed}
|
||||
behaviorShortHeader={behaviorShortHeader}
|
||||
matches={getExtensionsMatches(behaviorShortHeader)}
|
||||
|
@@ -1,53 +1,80 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import BundlePageHeader from './BundlePageHeader';
|
||||
import { BundleStoreContext } from './BundleStoreContext';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import {
|
||||
type BundleListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
getBundle,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../Utils/GDevelopServices/Usage';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import Text from '../../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
} from '../ProductPageHelper';
|
||||
import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { GridList, GridListTile } from '@material-ui/core';
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { Column, LargeSpacer, Line, Spacer } from '../../UI/Grid';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
type UserPublicProfile,
|
||||
} from '../../Utils/GDevelopServices/User';
|
||||
import Link from '../../UI/Link';
|
||||
import ResponsiveMediaGallery from '../../UI/ResponsiveMediaGallery';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import Text from '../../UI/Text';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import { planIdSortingFunction } from '../../Profile/Subscription/PlanCard';
|
||||
import SubscriptionPlanPricingSummary from '../../Profile/Subscription/PromotionSubscriptionDialog/SubscriptionPlanPricingSummary';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import SubscriptionPlanTableSummary from '../../Profile/Subscription/PromotionSubscriptionDialog/SubscriptionPlanTableSummary';
|
||||
import { sendBundleBuyClicked } from '../../Utils/Analytics/EventSender';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
import { BundleStoreContext } from './BundleStoreContext';
|
||||
import {
|
||||
getBundlesContainingProductTiles,
|
||||
getOtherProductsFromSameAuthorTiles,
|
||||
getProductMediaItems,
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../ProductPageHelper';
|
||||
import SecureCheckout from '../SecureCheckout/SecureCheckout';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import BundlePurchaseDialog from './BundlePurchaseDialog';
|
||||
import PublicProfileContext from '../../Profile/PublicProfileContext';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import SectionContainer, {
|
||||
SectionRow,
|
||||
} from '../../MainFrame/EditorContainers/HomePage/SectionContainer';
|
||||
import { type CourseCompletion } from '../../MainFrame/EditorContainers/HomePage/UseCourses';
|
||||
import CourseCard from '../../MainFrame/EditorContainers/HomePage/LearnSection/CourseCard';
|
||||
import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import { getCreditsAmountFromId } from '../CreditsPackages/CreditsPackageStoreContext';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../Profile/Subscription/PlanCard';
|
||||
import RedemptionCodesDialog from '../../RedemptionCode/RedemptionCodesDialog';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
|
||||
const getColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
const cellSpacing = 10;
|
||||
|
||||
const getTemplateColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
isLandscape: boolean
|
||||
) => {
|
||||
switch (windowSize) {
|
||||
case 'small':
|
||||
return isLandscape ? 4 : 2;
|
||||
@@ -61,50 +88,80 @@ const getColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
|
||||
const cellSpacing = 10;
|
||||
const MAX_COLUMNS = getColumns('xlarge', true);
|
||||
const MAX_COLUMNS = getTemplateColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
disabledText: { opacity: 0.6 },
|
||||
scrollview: { overflowX: 'hidden' },
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
},
|
||||
leftColumnContainer: {
|
||||
flex: 1,
|
||||
minWidth: 0, // This is needed for the container to take the right size.
|
||||
},
|
||||
rightColumnContainer: {
|
||||
flex: 2,
|
||||
},
|
||||
leftColumnContainerMobile: {
|
||||
flex: 1,
|
||||
minWidth: 0, // This is needed for the container to take the right size.
|
||||
},
|
||||
rightColumnContainerMobile: {
|
||||
flex: 1,
|
||||
},
|
||||
avatar: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
ownedTag: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
color: 'black',
|
||||
},
|
||||
playIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
onBack?: () => void | Promise<void>,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
bundleListingDatasFromSameCreator?: ?Array<BundleListingData>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
onBundleOpen: BundleListingData => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
onCourseOpen: CourseListingData => void,
|
||||
courses: ?Array<Course>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
noPadding?: boolean,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const BundleInformationPage = ({
|
||||
bundleListingData,
|
||||
onBack,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
courses,
|
||||
bundleListingDatasFromSameCreator,
|
||||
receivedCourses,
|
||||
getCourseCompletion,
|
||||
noPadding,
|
||||
onBundleOpen,
|
||||
onGameTemplateOpen,
|
||||
onAssetPackOpen,
|
||||
onCourseOpen,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const { windowSize, isLandscape, isMobile } = useResponsiveWindowSize();
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext); // If archived, should use the one passed.
|
||||
const { id, name, sellerId } = bundleListingData;
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
@@ -112,125 +169,181 @@ const BundleInformationPage = ({
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const {
|
||||
receivedBundles,
|
||||
bundlePurchases,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [bundle, setBundle] = React.useState<?Bundle>(null);
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const [isFetching, setIsFetching] = React.useState<boolean>(false);
|
||||
const { openUserPublicProfile } = React.useContext(PublicProfileContext);
|
||||
const [
|
||||
sellerPublicProfile,
|
||||
setSellerPublicProfile,
|
||||
] = React.useState<?UserPublicProfile>(null);
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
const {
|
||||
palette: { type: paletteType },
|
||||
} = React.useContext(GDevelopThemeContext);
|
||||
windowSize,
|
||||
isLandscape,
|
||||
isMediumScreen,
|
||||
isMobile,
|
||||
} = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const [
|
||||
isRedemptionCodesDialogOpen,
|
||||
setIsRedemptionCodesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const courseAndTheirListingDataIncludedInBundle = React.useMemo(
|
||||
(): Array<{|
|
||||
course: Course,
|
||||
courseListingData: CourseListingData,
|
||||
|}> | null => {
|
||||
if (!bundle || !bundleListingData || !courses) return null;
|
||||
const productListingDatasInBundle = getProductsIncludedInBundle({
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: [...(listedCourses || [])],
|
||||
});
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
if (!productListingDatasInBundle) return null;
|
||||
// $FlowIgnore - Flow doesn't understand that we have filtered the products to only include courses.
|
||||
const courseListingDatasInBundle: CourseListingData[] = productListingDatasInBundle.filter(
|
||||
productListingData => productListingData.productType === 'COURSE'
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: bundleListingData ? bundleListingData.id : null,
|
||||
receivedProducts: receivedBundles,
|
||||
productPurchases: bundlePurchases,
|
||||
allProductListingDatas: bundleListingDatas,
|
||||
}),
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
const isOwningAnotherVariant = React.useMemo(
|
||||
() => {
|
||||
if (!bundle || isAlreadyReceived || !receivedBundles) return false;
|
||||
|
||||
// Another bundle older version of that bundle can be owned.
|
||||
// We look at the tag to determine if the bundle is the same.
|
||||
return !!receivedBundles.find(
|
||||
receivedBundle => receivedBundle.tag === bundle.tag
|
||||
);
|
||||
|
||||
return (courseListingDatasInBundle || [])
|
||||
.map(courseListingData => {
|
||||
const course = courses.find(
|
||||
course => course.id === courseListingData.id
|
||||
);
|
||||
if (!course) return null;
|
||||
return {
|
||||
course,
|
||||
courseListingData,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
[bundle, bundleListingData, listedCourses, courses]
|
||||
[bundle, isAlreadyReceived, receivedBundles]
|
||||
);
|
||||
|
||||
const productsExceptCoursesIncludedInBundleTiles = React.useMemo(
|
||||
const additionalProductThumbnailsIncludedInBundle: string[] = React.useMemo(
|
||||
() => {
|
||||
const productsIncludedInBundle = getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(listedCourses || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
});
|
||||
|
||||
if (!productsIncludedInBundle) return [];
|
||||
|
||||
const additionalThumbnails = productsIncludedInBundle
|
||||
.map(product => (product.thumbnailUrls || []).slice(0, 2))
|
||||
.reduce((acc, thumbnails) => acc.concat(thumbnails), []);
|
||||
return additionalThumbnails;
|
||||
},
|
||||
[
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
bundleListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const productsIncludedInBundleTiles = React.useMemo(
|
||||
() =>
|
||||
bundle && bundleListingData
|
||||
? getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
discountedPrice: true,
|
||||
})
|
||||
: null,
|
||||
getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(listedCourses || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedCourses || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
bundleListingData,
|
||||
receivedCourses,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
bundleListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
|
||||
const bundlesContainingBundleTiles = React.useMemo(
|
||||
() =>
|
||||
getBundlesContainingProductTiles({
|
||||
product: bundle,
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: bundleListingDatas,
|
||||
receivedProducts: receivedBundles,
|
||||
onPrivateAssetPackOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
receivedBundles,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const highestSubscriptionPlanIncludedInBundle = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingData) return null;
|
||||
|
||||
const sortedIncludedRedemptionCodes = (
|
||||
bundleListingData.includedRedemptionCodes || []
|
||||
).sort((a, b) =>
|
||||
planIdSortingFunction(
|
||||
a.givenSubscriptionPlanId,
|
||||
b.givenSubscriptionPlanId
|
||||
)
|
||||
);
|
||||
|
||||
if (!sortedIncludedRedemptionCodes.length) return null;
|
||||
|
||||
const planId =
|
||||
sortedIncludedRedemptionCodes[sortedIncludedRedemptionCodes.length - 1]
|
||||
.givenSubscriptionPlanId;
|
||||
|
||||
return subscriptionPlansWithPricingSystems
|
||||
? subscriptionPlansWithPricingSystems.find(
|
||||
subscriptionPlanWithPricingSystems =>
|
||||
subscriptionPlanWithPricingSystems.id === planId
|
||||
)
|
||||
: null;
|
||||
},
|
||||
[bundleListingData, subscriptionPlansWithPricingSystems]
|
||||
const otherBundlesFromTheSameAuthorTiles = React.useMemo(
|
||||
() =>
|
||||
getOtherProductsFromSameAuthorTiles({
|
||||
otherProductListingDatasFromSameCreator: bundleListingDatasFromSameCreator,
|
||||
currentProductListingData: bundleListingData,
|
||||
receivedProducts: receivedBundles,
|
||||
onProductOpen: onBundleOpen,
|
||||
}),
|
||||
[
|
||||
bundleListingDatasFromSameCreator,
|
||||
bundleListingData,
|
||||
receivedBundles,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const bundle = await getBundle(bundleListingData.id);
|
||||
const [bundle, profile] = await Promise.all([
|
||||
getBundle(id),
|
||||
getUserPublicProfile(sellerId),
|
||||
]);
|
||||
|
||||
setBundle(bundle);
|
||||
setSellerPublicProfile(profile);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
@@ -246,149 +359,328 @@ const BundleInformationPage = ({
|
||||
<Trans>An error occurred, please try again later.</Trans>
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[bundleListingData.id]
|
||||
[id, sellerId]
|
||||
);
|
||||
|
||||
const customSectionPaperStyle = {
|
||||
// $FlowIgnore
|
||||
...(noPadding
|
||||
? {
|
||||
padding: 0,
|
||||
}
|
||||
: {}),
|
||||
...(bundleListingData.visibleUntil && !noPadding
|
||||
? {
|
||||
backgroundAttachment: 'local',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'top',
|
||||
backgroundSize: isMobile && !isLandscape ? 'contain' : 'auto',
|
||||
backgroundImage: `${
|
||||
paletteType === 'dark'
|
||||
? 'linear-gradient(180deg, #6B1A18 0px, #2D2331 5%, #1d1d2600 10%)'
|
||||
: 'linear-gradient(180deg, #F03F18 0px, #f5f5f700 10%)'
|
||||
}`,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle || isOwningAnotherVariant) return;
|
||||
if (isAlreadyReceived) {
|
||||
onBundleOpen(bundleListingData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorText) {
|
||||
return (
|
||||
<SectionContainer
|
||||
flexBody
|
||||
backAction={onBack}
|
||||
customPaperStyle={customSectionPaperStyle}
|
||||
>
|
||||
<SectionRow expand>
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
try {
|
||||
const price = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
|
||||
if (!bundleListingData || !bundle) {
|
||||
return (
|
||||
<SectionContainer flexBody customPaperStyle={customSectionPaperStyle}>
|
||||
<SectionRow expand>
|
||||
<PlaceholderLoader />
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
sendBundleBuyClicked({
|
||||
bundleId: bundle.id,
|
||||
bundleName: bundle.name,
|
||||
bundleTag: bundle.tag,
|
||||
currency: price ? price.currency : undefined,
|
||||
usageType: 'default',
|
||||
});
|
||||
|
||||
setPurchasingBundleListingData(bundleListingData);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
isAlreadyReceived,
|
||||
isOwningAnotherVariant,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const mediaItems = React.useMemo(
|
||||
() =>
|
||||
getProductMediaItems({
|
||||
product: bundle,
|
||||
productListingData: bundleListingData,
|
||||
shouldSimulateAppStoreProduct: simulateAppStoreProduct,
|
||||
additionalThumbnails: additionalProductThumbnailsIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
simulateAppStoreProduct,
|
||||
additionalProductThumbnailsIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const includedCreditsAmount = React.useMemo(
|
||||
() =>
|
||||
(bundleListingData.includedListableProducts || [])
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
),
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const includedRedemptionCodes = React.useMemo(
|
||||
() => bundleListingData.includedRedemptionCodes || [],
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<SectionContainer
|
||||
backAction={onBack}
|
||||
customPaperStyle={customSectionPaperStyle}
|
||||
>
|
||||
<BundlePageHeader
|
||||
bundleListingData={bundleListingData}
|
||||
bundle={bundle}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<Line noMargin>
|
||||
<Text size="section-title">
|
||||
<Trans>What's included:</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
{courseAndTheirListingDataIncludedInBundle &&
|
||||
courseAndTheirListingDataIncludedInBundle.length > 0 && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{courseAndTheirListingDataIncludedInBundle.map(
|
||||
({ course, courseListingData }) => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onCourseOpen(courseListingData);
|
||||
}}
|
||||
discountedPrice
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</GridList>
|
||||
</Line>
|
||||
)}
|
||||
{productsExceptCoursesIncludedInBundleTiles && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsExceptCoursesIncludedInBundleTiles}
|
||||
</GridList>
|
||||
<>
|
||||
{errorText ? (
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
)}
|
||||
{highestSubscriptionPlanIncludedInBundle && (
|
||||
<ResponsiveLineStackLayout expand noColumnMargin>
|
||||
<Column noMargin justifyContent="center">
|
||||
<Line expand>
|
||||
<SubscriptionPlanPricingSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
highestSubscriptionPlanIncludedInBundle
|
||||
) : isFetching ? (
|
||||
<Column expand alignItems="center" justifyContent="center">
|
||||
<PlaceholderLoader />
|
||||
</Column>
|
||||
) : bundle && sellerPublicProfile ? (
|
||||
<Column noOverflowParent expand noMargin>
|
||||
<ScrollView autoHideScrollbar style={styles.scrollview}>
|
||||
<ResponsiveLineStackLayout
|
||||
noColumnMargin
|
||||
noMargin
|
||||
// Force the columns to wrap on tablets and small screens.
|
||||
forceMobileLayout={isMediumScreen}
|
||||
// Prevent it to wrap when in landscape mode on small screens.
|
||||
noResponsiveLandscape
|
||||
useLargeSpacer
|
||||
>
|
||||
<div
|
||||
style={
|
||||
isMobile
|
||||
? styles.leftColumnContainerMobile
|
||||
: styles.leftColumnContainer
|
||||
}
|
||||
disabled={false}
|
||||
onClickChoosePlan={async () => {}}
|
||||
seatsCount={0}
|
||||
setSeatsCount={() => {}}
|
||||
period={'month'}
|
||||
setPeriod={() => {}}
|
||||
onlyShowDiscountedPrice
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
<Spacer />
|
||||
<Column noMargin>
|
||||
<SubscriptionPlanTableSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
highestSubscriptionPlanIncludedInBundle
|
||||
}
|
||||
hideActions
|
||||
/>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
>
|
||||
<ResponsiveMediaGallery
|
||||
mediaItems={mediaItems}
|
||||
altTextTemplate={`Bundle ${name} preview image {mediaIndex}`}
|
||||
horizontalOuterMarginToEatOnMobile={8}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
isMobile
|
||||
? styles.rightColumnContainerMobile
|
||||
: styles.rightColumnContainer
|
||||
}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text noMargin size="title">
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
{isAlreadyReceived && (
|
||||
<div
|
||||
style={{
|
||||
...styles.ownedTag,
|
||||
backgroundColor:
|
||||
gdevelopTheme.statusIndicator.success,
|
||||
}}
|
||||
>
|
||||
<Text color="inherit" noMargin>
|
||||
<Trans>OWNED</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Avatar
|
||||
src={sellerPublicProfile.iconUrl}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text displayInlineAsSpan size="sub-title">
|
||||
<Link
|
||||
onClick={() =>
|
||||
openUserPublicProfile({
|
||||
userId: sellerPublicProfile.id,
|
||||
callbacks: {
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
},
|
||||
})
|
||||
}
|
||||
href="#"
|
||||
>
|
||||
{sellerPublicProfile.username || ''}
|
||||
</Link>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
{isOwningAnotherVariant ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
You own an older version of this bundle. Browse the
|
||||
store to access it!
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : !isAlreadyReceived ? (
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<Text size="body2" displayInlineAsSpan>
|
||||
<MarkdownText
|
||||
source={selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
allowParagraphs
|
||||
/>
|
||||
</Text>
|
||||
{includedRedemptionCodes.length > 0 && (
|
||||
<ColumnStackLayout noMargin>
|
||||
{includedRedemptionCodes.map(
|
||||
(includedRedemptionCode, index) => (
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
key={`${
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
}-${index}`}
|
||||
>
|
||||
{getPlanIcon({
|
||||
planId:
|
||||
includedRedemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Text>
|
||||
<Trans>
|
||||
{formatDurationOfRedemptionCode(
|
||||
includedRedemptionCode.durationInDays
|
||||
)}{' '}
|
||||
of
|
||||
{getPlanInferredNameFromId(
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)
|
||||
)}
|
||||
{isAlreadyReceived && (
|
||||
<Line noMargin>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>See my codes</Trans>}
|
||||
onClick={() =>
|
||||
setIsRedemptionCodesDialogOpen(true)
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
{includedCreditsAmount > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text>
|
||||
<Trans>
|
||||
{includedCreditsAmount} credits included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
</ResponsiveLineStackLayout>
|
||||
{bundlesContainingBundleTiles &&
|
||||
bundlesContainingBundleTiles.length ? (
|
||||
<>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LargeSpacer />
|
||||
{bundlesContainingBundleTiles}
|
||||
<LargeSpacer />
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : null}
|
||||
{productsIncludedInBundleTiles && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Included in this bundle</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsIncludedInBundleTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
{otherBundlesFromTheSameAuthorTiles &&
|
||||
otherBundlesFromTheSameAuthorTiles.length > 0 && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Similar bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{otherBundlesFromTheSameAuthorTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Column>
|
||||
) : null}
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
</SectionContainer>
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
@@ -1,518 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { type Bundle } from '../../Utils/GDevelopServices/Asset';
|
||||
import { type BundleListingData } from '../../Utils/GDevelopServices/Shop';
|
||||
import Paper from '../../UI/Paper';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import { renderProductPrice } from '../../AssetStore/ProductPriceTag';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../../AssetStore/ProductPageHelper';
|
||||
import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { BundleStoreContext } from '../../AssetStore/Bundles/BundleStoreContext';
|
||||
import { sendBundleBuyClicked } from '../../Utils/Analytics/EventSender';
|
||||
import BundlePurchaseDialog from '../../AssetStore/Bundles/BundlePurchaseDialog';
|
||||
import RedemptionCodesDialog from '../../RedemptionCode/RedemptionCodesDialog';
|
||||
import {
|
||||
getEstimatedSavingsFormatted,
|
||||
renderEstimatedTotalPriceFormatted,
|
||||
} from '../../AssetStore/Bundles/Utils';
|
||||
import { PrivateGameTemplateStoreContext } from '../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { CreditsPackageStoreContext } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import { AssetStoreContext } from '../../AssetStore/AssetStoreContext';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import SecureCheckout from '../../AssetStore/SecureCheckout/SecureCheckout';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import Chip from '../../UI/Chip';
|
||||
import ProductLimitedTimeOffer from '../../AssetStore/ProductLimitedTimeOffer';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import { getSummaryLines } from './Utils';
|
||||
import { SectionRow } from '../../MainFrame/EditorContainers/HomePage/SectionContainer';
|
||||
|
||||
const styles = {
|
||||
title: { overflowWrap: 'anywhere', textWrap: 'wrap' },
|
||||
image: { width: 300, aspectRatio: '16 / 9' },
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
discountChip: {
|
||||
height: 24,
|
||||
backgroundColor: '#F03F18',
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
bundle: Bundle,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
i18n: I18nType,
|
||||
|};
|
||||
|
||||
const BundlePageHeader = ({
|
||||
bundle,
|
||||
bundleListingData,
|
||||
simulateAppStoreProduct,
|
||||
i18n,
|
||||
}: Props) => {
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { creditsPackageListingDatas } = React.useContext(
|
||||
CreditsPackageStoreContext
|
||||
);
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const { receivedBundles, bundlePurchases } = authenticatedUser;
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
const isMobileOrMediumScreen = isMobile || isMediumScreen;
|
||||
const [
|
||||
isRedemptionCodesDialogOpen,
|
||||
setIsRedemptionCodesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: bundleListingData ? bundleListingData.id : null,
|
||||
receivedProducts: receivedBundles,
|
||||
productPurchases: bundlePurchases,
|
||||
allProductListingDatas: bundleListingDatas,
|
||||
}),
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
|
||||
const productListingDatasIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData &&
|
||||
bundleListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
privateAssetPackListingDatas &&
|
||||
listedCourses &&
|
||||
creditsPackageListingDatas
|
||||
? getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...bundleListingDatas,
|
||||
...privateGameTemplateListingDatas,
|
||||
...privateAssetPackListingDatas,
|
||||
...listedCourses,
|
||||
...creditsPackageListingDatas,
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
creditsPackageListingDatas,
|
||||
]
|
||||
);
|
||||
|
||||
const redemptionCodesIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? bundleListingData.includedRedemptionCodes || []
|
||||
: null,
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const summaryLines = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!productListingDatasIncludedInBundle ||
|
||||
!redemptionCodesIncludedInBundle ||
|
||||
!bundleListingData
|
||||
)
|
||||
return isMobile ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
</ColumnStackLayout>
|
||||
) : (
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
);
|
||||
|
||||
if (isAlreadyReceived) {
|
||||
return (
|
||||
<Line noMargin>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>See my subscription codes</Trans>}
|
||||
onClick={() => setIsRedemptionCodesDialogOpen(true)}
|
||||
/>
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
|
||||
const summaryLines = getSummaryLines({
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
});
|
||||
|
||||
if (isMobile) {
|
||||
return summaryLines.mobileLines;
|
||||
}
|
||||
return summaryLines.desktopLines;
|
||||
},
|
||||
[
|
||||
isAlreadyReceived,
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
isMobile,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedTotalPriceFormatted = React.useMemo(
|
||||
() =>
|
||||
renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedSavingsFormatted = React.useMemo(
|
||||
() =>
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const productPrice = React.useMemo(
|
||||
() =>
|
||||
renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
}),
|
||||
[i18n, bundleListingData]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle) return;
|
||||
if (isAlreadyReceived) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const price = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
|
||||
sendBundleBuyClicked({
|
||||
bundleId: bundle.id,
|
||||
bundleName: bundle.name,
|
||||
bundleTag: bundle.tag,
|
||||
usageType: 'default',
|
||||
priceValue: price && price.value,
|
||||
priceCurrency: price && price.currency,
|
||||
});
|
||||
|
||||
setPurchasingBundleListingData(bundleListingData);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[bundle, bundleListingData, isAlreadyReceived]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noOverflowParent noMargin>
|
||||
{!isAlreadyReceived && !isMobileOrMediumScreen && (
|
||||
<SectionRow>
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{!!bundleListingData && (
|
||||
<LineStackLayout noMargin justifyContent="space-between">
|
||||
{bundleListingData.visibleUntil ? (
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
/>
|
||||
) : estimatedSavingsFormatted ? (
|
||||
<Column
|
||||
noMargin
|
||||
alignItems="flex-start"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Chip
|
||||
label={<Trans>Bundle</Trans>}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
<Text color="secondary">
|
||||
<Trans>
|
||||
Get{' '}
|
||||
{estimatedSavingsFormatted.savingsPriceFormatted}{' '}
|
||||
worth of value for less!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
) : (
|
||||
<Skeleton height={24} width={100} />
|
||||
)}
|
||||
{estimatedTotalPriceFormatted &&
|
||||
estimatedSavingsFormatted &&
|
||||
productPrice ? (
|
||||
<LineStackLayout justifyContent="flex-end" noMargin>
|
||||
<Column noMargin alignItems="flex-end">
|
||||
<LineStackLayout>
|
||||
<Text noMargin color="secondary" size="block-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
{bundleListingData.visibleUntil && (
|
||||
<Chip
|
||||
label={
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
</Column>
|
||||
<ColumnStackLayout noMargin alignItems="center">
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
customLabel={
|
||||
<Trans>
|
||||
Buy now and save{' '}
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPriceFormatted
|
||||
}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Column noMargin justifyContent="flex-end">
|
||||
<Skeleton height={24} width={100} />
|
||||
<Skeleton height={24} width={100} />
|
||||
</Column>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!isAlreadyReceived &&
|
||||
isMobileOrMediumScreen &&
|
||||
bundleListingData &&
|
||||
bundleListingData.visibleUntil && (
|
||||
<SectionRow>
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
<SectionRow>
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
expand
|
||||
>
|
||||
<div style={styles.imageContainer}>
|
||||
<img
|
||||
src={bundle.previewImageUrls[0]}
|
||||
style={styles.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ColumnStackLayout
|
||||
expand
|
||||
justifyContent="flex-start"
|
||||
noMargin={isMobile}
|
||||
>
|
||||
<Text size="title" noMargin style={styles.title}>
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<Text noMargin>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
{summaryLines}
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
</SectionRow>
|
||||
{!isAlreadyReceived && isMobileOrMediumScreen && bundleListingData && (
|
||||
<SectionRow>
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{estimatedTotalPriceFormatted &&
|
||||
estimatedSavingsFormatted &&
|
||||
productPrice ? (
|
||||
<ResponsiveLineStackLayout justifyContent="flex-end" noMargin>
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{bundleListingData.visibleUntil && (
|
||||
<Chip
|
||||
label={
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
)}
|
||||
<Text noMargin color="secondary" size="block-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<ColumnStackLayout noMargin alignItems="center">
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
customLabel={
|
||||
<Trans>
|
||||
Buy now and save{' '}
|
||||
{
|
||||
(
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}) || {}
|
||||
).savingsPriceFormatted
|
||||
}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
) : (
|
||||
<Column noMargin expand justifyContent="flex-end">
|
||||
<Skeleton height={isMobile ? 60 : 40} />
|
||||
<Skeleton height={isMobile ? 60 : 40} />
|
||||
</Column>
|
||||
)}
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePageHeader;
|
@@ -2,7 +2,6 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import {
|
||||
@@ -49,12 +48,8 @@ import Hammer from '../../UI/CustomSvgIcons/Hammer';
|
||||
import School from '../../UI/CustomSvgIcons/School';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import Sparkle from '../../UI/CustomSvgIcons/Sparkle';
|
||||
import {
|
||||
getEstimatedSavingsFormatted,
|
||||
renderEstimatedTotalPriceFormatted,
|
||||
} from './Utils';
|
||||
import { renderEstimatedTotalPriceFormatted } from './Utils';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
import ProductLimitedTimeOffer from '../ProductLimitedTimeOffer';
|
||||
|
||||
const highlightColor = '#6CF9F7';
|
||||
|
||||
@@ -67,7 +62,7 @@ const styles = {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
gap: 8,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
bundlePreviewContainer: {
|
||||
@@ -259,11 +254,9 @@ const getColumnsFromWindowSize = (windowSize: WindowSizeType) => {
|
||||
|
||||
type Props = {|
|
||||
onDisplayBundle: (bundleListingData: BundleListingData) => void,
|
||||
i18n: I18nType,
|
||||
category: string,
|
||||
|};
|
||||
|
||||
const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
const { isMobile, isLandscape, windowSize } = useResponsiveWindowSize();
|
||||
const numberOfTilesToDisplay = getColumnsFromWindowSize(windowSize) - 1; // Reserve one tile for the bundle preview.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
@@ -279,34 +272,28 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
const { bundlePurchases, receivedBundles } = authenticatedUser;
|
||||
|
||||
// For the moment, we either display:
|
||||
// - the first bundle of that category if none are owned.
|
||||
// - the first owned bundle of that category (as a listing data if still listed, or as an archived listing data otherwise)
|
||||
// - the first bundle in the list if none are owned.
|
||||
// - the first owned bundle (as a listing data if still listed, or as an archived listing data otherwise)
|
||||
// TODO: improve that logic when we'll have more bundles.
|
||||
const bundleListingData: BundleListingData | null = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingDatas || !receivedBundles) return null;
|
||||
const bundleListingDataOfCategory = bundleListingDatas.filter(bundle =>
|
||||
bundle.categories.includes(category)
|
||||
)[0];
|
||||
const receivedBundleOfCategory = receivedBundles.filter(bundle =>
|
||||
bundle.categories.includes(category)
|
||||
)[0];
|
||||
if (!receivedBundleOfCategory) {
|
||||
// Display the first bundle if none are found with that category.
|
||||
return bundleListingDataOfCategory || bundleListingDatas[0];
|
||||
if (receivedBundles.length === 0) {
|
||||
return bundleListingDatas[0]; // Display the first bundle if none are owned.
|
||||
}
|
||||
const bundleListingDataMatchingOwnedBundle = bundleListingDatas.find(
|
||||
bundleListingData =>
|
||||
bundleListingData.id === receivedBundleOfCategory.id
|
||||
const receivedBundle = receivedBundles[0];
|
||||
const bundleListingData = bundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === receivedBundle.id
|
||||
);
|
||||
if (bundleListingData) {
|
||||
return bundleListingData; // Display the first owned bundle that is still listed.
|
||||
}
|
||||
// If this bundle is not listed anymore, get an archived listing data for that bundle.
|
||||
return (
|
||||
bundleListingDataMatchingOwnedBundle ||
|
||||
getArchivedBundleListingData({
|
||||
bundle: receivedBundleOfCategory,
|
||||
})
|
||||
);
|
||||
return getArchivedBundleListingData({
|
||||
bundle: receivedBundle,
|
||||
});
|
||||
},
|
||||
[bundleListingDatas, receivedBundles, category]
|
||||
[bundleListingDatas, receivedBundles]
|
||||
);
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
@@ -355,51 +342,6 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const estimatedTotalPriceFormatted = React.useMemo(
|
||||
() =>
|
||||
renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedSavingsFormatted = React.useMemo(
|
||||
() =>
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const productPrice = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})
|
||||
: null,
|
||||
[i18n, bundleListingData]
|
||||
);
|
||||
|
||||
const courseTiles = React.useMemo(
|
||||
() => {
|
||||
if (isMobile && !isLandscape) {
|
||||
@@ -491,6 +433,27 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
}}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line noMargin>
|
||||
{bundleListingData ? (
|
||||
<Chip
|
||||
label={
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Owned</Trans>
|
||||
) : (
|
||||
<Trans>Discount</Trans>
|
||||
)
|
||||
}
|
||||
style={
|
||||
isAlreadyReceived
|
||||
? styles.ownedChip
|
||||
: styles.discountChip
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton variant="rect" height={20} />
|
||||
)}
|
||||
</Line>
|
||||
<Spacer />
|
||||
{bundleListingData ? (
|
||||
<Text noMargin size="block-title">
|
||||
{bundleListingData.nameByLocale
|
||||
@@ -515,74 +478,20 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
) : (
|
||||
<Skeleton height={30} />
|
||||
)}
|
||||
{isMobile && (
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
{bundleListingData ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{estimatedSavingsFormatted &&
|
||||
estimatedTotalPriceFormatted &&
|
||||
productPrice !== null ? (
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent={
|
||||
bundleListingData.visibleUntil && !isMobile
|
||||
? 'flex-start'
|
||||
: 'space-between'
|
||||
}
|
||||
noMargin
|
||||
>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
{!isAlreadyReceived && estimatedTotalPriceFormatted && (
|
||||
<Text noMargin color="secondary" size="sub-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
{(isAlreadyReceived ||
|
||||
!bundleListingData.visibleUntil) && (
|
||||
<Chip
|
||||
label={
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Owned</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
style={
|
||||
isAlreadyReceived
|
||||
? styles.ownedChip
|
||||
: styles.discountChip
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
{!isAlreadyReceived && (
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Skeleton variant="rect" height={20} />
|
||||
)}
|
||||
{!isAlreadyReceived && bundleListingData.visibleUntil && (
|
||||
<Line noMargin expand>
|
||||
<Column noMargin expand>
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
hideMinutesAndSeconds
|
||||
alignCenter
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
{!isAlreadyReceived && (
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
@@ -590,7 +499,15 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Browse bundle</Trans>
|
||||
) : (
|
||||
<Trans>Discover this bundle</Trans>
|
||||
<Trans>
|
||||
Buy for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})}
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
onClick={() => onDisplayBundle(bundleListingData)}
|
||||
@@ -603,9 +520,7 @@ const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{courseTiles}
|
||||
{!isMobile && (
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
)}
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
|
@@ -157,15 +157,10 @@ export const BundleStoreStateProvider = ({
|
||||
});
|
||||
|
||||
if (bundleListingData) {
|
||||
const priceForUsageType = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendBundleInformationOpened({
|
||||
bundleName: bundleListingData.name,
|
||||
bundleId: bundleListingData.id,
|
||||
source: 'web-link',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
shopNavigationState.openBundleInformationPage({
|
||||
bundleListingData,
|
||||
|
@@ -8,94 +8,14 @@ import {
|
||||
type CreditsPackageListingData,
|
||||
type IncludedRedemptionCode,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { renderPriceFormatted } from '../ProductPriceTag';
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../Profile/Subscription/PlanCard';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
import School from '../../UI/CustomSvgIcons/School';
|
||||
import Hammer from '../../UI/CustomSvgIcons/Hammer';
|
||||
import Store from '../../UI/CustomSvgIcons/Store';
|
||||
import { Divider } from '@material-ui/core';
|
||||
import { getCreditsAmountFromId } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
|
||||
const styles = {
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
const getEstimatedRedemptionCodePrice = (
|
||||
redemptionCode: IncludedRedemptionCode,
|
||||
currencyCode: 'USD' | 'EUR'
|
||||
): number => {
|
||||
const planId = redemptionCode.givenSubscriptionPlanId;
|
||||
if (!planId) {
|
||||
return 0;
|
||||
}
|
||||
let estimatedAmountInCents = null;
|
||||
if (redemptionCode.estimatedPrices) {
|
||||
const estimatedPrice = redemptionCode.estimatedPrices.find(
|
||||
price => price.currency === currencyCode
|
||||
);
|
||||
if (estimatedPrice) {
|
||||
estimatedAmountInCents = estimatedPrice.value;
|
||||
}
|
||||
}
|
||||
|
||||
// If no estimated price is provided, guess a mostly correct value
|
||||
// for backward compatibility.
|
||||
if (estimatedAmountInCents === null) {
|
||||
const monthlyEstimatedAmountInCents =
|
||||
planId === 'gdevelop_silver'
|
||||
? 599
|
||||
: planId === 'gdevelop_gold'
|
||||
? 1099
|
||||
: planId === 'gdevelop_startup'
|
||||
? 3499
|
||||
: 0;
|
||||
estimatedAmountInCents =
|
||||
monthlyEstimatedAmountInCents *
|
||||
Math.max(1, Math.round(redemptionCode.durationInDays / 30));
|
||||
}
|
||||
|
||||
return estimatedAmountInCents;
|
||||
};
|
||||
|
||||
export const renderEstimatedRedemptionCodePriceFormatted = (
|
||||
bundleListingData: BundleListingData,
|
||||
redemptionCode: IncludedRedemptionCode,
|
||||
i18n: I18nType
|
||||
): string => {
|
||||
const bundlePrice = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
const currencyCode = bundlePrice ? bundlePrice.currency : 'USD';
|
||||
const estimatedPrice = getEstimatedRedemptionCodePrice(
|
||||
redemptionCode,
|
||||
currencyCode
|
||||
);
|
||||
return renderPriceFormatted(estimatedPrice, currencyCode, i18n);
|
||||
};
|
||||
|
||||
const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
export const renderEstimatedTotalPriceFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
}: {
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
@@ -105,18 +25,7 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
filter?:
|
||||
| 'ASSET_PACK'
|
||||
| 'GAME_TEMPLATE'
|
||||
| 'COURSE'
|
||||
| 'BUNDLE'
|
||||
| 'CREDITS_PACKAGE'
|
||||
| 'REDEMPTION_CODE',
|
||||
}): ?{
|
||||
totalPrice: number,
|
||||
bundlePrice: number,
|
||||
currencyCode: 'USD' | 'EUR',
|
||||
} => {
|
||||
}): ?string => {
|
||||
let totalPrice = 0;
|
||||
if (
|
||||
!bundleListingData ||
|
||||
@@ -130,13 +39,10 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
const currencyCode = bundlePrice ? bundlePrice.currency : 'USD';
|
||||
const bundlePriceValue = bundlePrice ? bundlePrice.value : 0;
|
||||
const currencySymbol = currencyCode === 'USD' ? '$' : '€';
|
||||
|
||||
for (const product of bundleListingData.includedListableProducts || []) {
|
||||
if (
|
||||
product.productType === 'ASSET_PACK' &&
|
||||
(!filter || filter === 'ASSET_PACK')
|
||||
) {
|
||||
if (product.productType === 'ASSET_PACK') {
|
||||
const listedAssetPack =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
assetPack => assetPack.id === product.productId
|
||||
@@ -147,10 +53,7 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (
|
||||
product.productType === 'GAME_TEMPLATE' &&
|
||||
(!filter || filter === 'GAME_TEMPLATE')
|
||||
) {
|
||||
} else if (product.productType === 'GAME_TEMPLATE') {
|
||||
const listedGameTemplate =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
gameTemplate => gameTemplate.id === product.productId
|
||||
@@ -161,10 +64,7 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (
|
||||
product.productType === 'COURSE' &&
|
||||
(!filter || filter === 'COURSE')
|
||||
) {
|
||||
} else if (product.productType === 'COURSE') {
|
||||
const listedCourse = productListingDatasIncludedInBundle.find(
|
||||
course => course.id === product.productId
|
||||
);
|
||||
@@ -174,10 +74,7 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (
|
||||
product.productType === 'BUNDLE' &&
|
||||
(!filter || filter === 'BUNDLE')
|
||||
) {
|
||||
} else if (product.productType === 'BUNDLE') {
|
||||
const listedBundle = productListingDatasIncludedInBundle.find(
|
||||
bundle => bundle.id === product.productId
|
||||
);
|
||||
@@ -187,10 +84,7 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (
|
||||
product.productType === 'CREDITS_PACKAGE' &&
|
||||
(!filter || filter === 'CREDITS_PACKAGE')
|
||||
) {
|
||||
} else if (product.productType === 'CREDITS_PACKAGE') {
|
||||
const listedCreditsPackage =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
creditsPackage => creditsPackage.id === product.productId
|
||||
@@ -204,522 +98,45 @@ const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
redemptionCodesIncludedInBundle.length > 0 &&
|
||||
(!filter || filter === 'REDEMPTION_CODE')
|
||||
) {
|
||||
if (redemptionCodesIncludedInBundle.length > 0) {
|
||||
for (const redemptionCode of redemptionCodesIncludedInBundle) {
|
||||
const estimatedAmountInCents = getEstimatedRedemptionCodePrice(
|
||||
redemptionCode,
|
||||
currencyCode
|
||||
);
|
||||
totalPrice += estimatedAmountInCents || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { totalPrice, bundlePrice: bundlePriceValue, currencyCode };
|
||||
};
|
||||
|
||||
export const renderEstimatedTotalPriceFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
filter?:
|
||||
| 'ASSET_PACK'
|
||||
| 'GAME_TEMPLATE'
|
||||
| 'COURSE'
|
||||
| 'BUNDLE'
|
||||
| 'CREDITS_PACKAGE'
|
||||
| 'REDEMPTION_CODE',
|
||||
|}): ?string => {
|
||||
const estimatedPriceAndCode = getEstimatedTotalPriceAndCurrencyCode({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
});
|
||||
if (!estimatedPriceAndCode || !bundleListingData) return null;
|
||||
|
||||
return renderPriceFormatted(
|
||||
estimatedPriceAndCode.totalPrice,
|
||||
estimatedPriceAndCode.currencyCode,
|
||||
i18n
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedSavingsFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
}): ?{ savingsPriceFormatted: string, savingsPercentageFormatted: string } => {
|
||||
const estimatedTotalPriceAndCode = getEstimatedTotalPriceAndCurrencyCode({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
});
|
||||
if (!estimatedTotalPriceAndCode || !bundleListingData) return null;
|
||||
|
||||
const savings =
|
||||
estimatedTotalPriceAndCode.totalPrice -
|
||||
estimatedTotalPriceAndCode.bundlePrice;
|
||||
|
||||
return {
|
||||
savingsPriceFormatted: renderPriceFormatted(
|
||||
savings,
|
||||
estimatedTotalPriceAndCode.currencyCode,
|
||||
i18n
|
||||
),
|
||||
savingsPercentageFormatted: `${Math.round(
|
||||
(savings / estimatedTotalPriceAndCode.totalPrice) * 100
|
||||
)}%`,
|
||||
};
|
||||
};
|
||||
|
||||
const getCoursesEstimatedHoursToComplete = (
|
||||
courseListingDatas: CourseListingData[]
|
||||
) => {
|
||||
// Ideally we'd look at the Course durationInWeeks, but to avoid too
|
||||
// many API calls, we just estimate.
|
||||
const totalHours = courseListingDatas.reduce((acc, course) => {
|
||||
const estimatedHours = 4 * 5; // Estimate 4h per day, 5 days a week.
|
||||
return acc + Math.round(estimatedHours);
|
||||
}, 0);
|
||||
return Math.round(totalHours);
|
||||
};
|
||||
|
||||
const CoursesLineSummary = ({
|
||||
courseListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
courseListingDatasIncludedInBundle: CourseListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<School />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length} Course
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length} Courses
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
{getCoursesEstimatedHoursToComplete(
|
||||
courseListingDatasIncludedInBundle
|
||||
)}{' '}
|
||||
hours of material
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'COURSE',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const AssetPacksLineSummary = ({
|
||||
assetPackListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
assetPackListingDatasIncludedInBundle: PrivateAssetPackListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<Store />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length} Asset pack
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length} Asset packs
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>Commercial License</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'ASSET_PACK',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const GameTemplatesLineSummary = ({
|
||||
gameTemplateListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
gameTemplateListingDatasIncludedInBundle: PrivateGameTemplateListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<Hammer />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length} Game
|
||||
template
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length} Game
|
||||
templates
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>Lifetime access</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'GAME_TEMPLATE',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const RedemptionCodeLineSummary = ({
|
||||
includedRedemptionCode,
|
||||
bundleListingData,
|
||||
}: {|
|
||||
includedRedemptionCode: IncludedRedemptionCode,
|
||||
bundleListingData: BundleListingData,
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
{getPlanIcon({
|
||||
planId: includedRedemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{getPlanInferredNameFromId(
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
{formatDurationOfRedemptionCode(
|
||||
includedRedemptionCode.durationInDays
|
||||
)}
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedRedemptionCodePriceFormatted(
|
||||
bundleListingData,
|
||||
includedRedemptionCode,
|
||||
i18n
|
||||
)}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const CreditsLineSummary = ({
|
||||
includedCreditsAmount,
|
||||
}: {
|
||||
includedCreditsAmount: number,
|
||||
}) => (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>{includedCreditsAmount} credits</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
||||
export const getSummaryLines = ({
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
}: {|
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
|}) => {
|
||||
const includedListableProducts =
|
||||
bundleListingData.includedListableProducts || [];
|
||||
const summaryLineItems = [];
|
||||
|
||||
const includedCourseListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'COURSE'
|
||||
);
|
||||
|
||||
const courseListingDatasIncludedInBundle = includedCourseListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's a course because of the filter.
|
||||
const courseListingData: ?CourseListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
return courseListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (courseListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<CoursesLineSummary
|
||||
courseListingDatasIncludedInBundle={courseListingDatasIncludedInBundle}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
const planId = redemptionCode.givenSubscriptionPlanId;
|
||||
if (planId) {
|
||||
let estimatedAmountInCents = null;
|
||||
if (redemptionCode.estimatedPrices) {
|
||||
const estimatedPrice = redemptionCode.estimatedPrices.find(
|
||||
price => price.currency === currencyCode
|
||||
);
|
||||
if (estimatedPrice) {
|
||||
estimatedAmountInCents = estimatedPrice.value;
|
||||
}
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const includedAssetPackListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'ASSET_PACK'
|
||||
);
|
||||
|
||||
const assetPackListingDatasIncludedInBundle = includedAssetPackListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's an asset pack because of the filter.
|
||||
const assetPackListingData: ?PrivateAssetPackListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'ASSET_PACK'
|
||||
);
|
||||
|
||||
return assetPackListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (assetPackListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<AssetPacksLineSummary
|
||||
assetPackListingDatasIncludedInBundle={
|
||||
assetPackListingDatasIncludedInBundle
|
||||
// If no estimated price is provided, guess a mostly correct value
|
||||
// for backward compatibility.
|
||||
if (estimatedAmountInCents === null) {
|
||||
const monthlyEstimatedAmountInCents =
|
||||
planId === 'gdevelop_silver'
|
||||
? 599
|
||||
: planId === 'gdevelop_gold'
|
||||
? 1099
|
||||
: planId === 'gdevelop_startup'
|
||||
? 3499
|
||||
: 0;
|
||||
estimatedAmountInCents =
|
||||
monthlyEstimatedAmountInCents *
|
||||
Math.max(1, Math.round(redemptionCode.durationInDays / 30));
|
||||
}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const includedGameTemplateListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'GAME_TEMPLATE'
|
||||
);
|
||||
|
||||
const gameTemplateListingDatasIncludedInBundle = includedGameTemplateListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's a game template because of the filter.
|
||||
const gameTemplateListingData: ?PrivateGameTemplateListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'GAME_TEMPLATE'
|
||||
);
|
||||
|
||||
return gameTemplateListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (gameTemplateListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<GameTemplatesLineSummary
|
||||
gameTemplateListingDatasIncludedInBundle={
|
||||
gameTemplateListingDatasIncludedInBundle
|
||||
}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (redemptionCodesIncludedInBundle.length) {
|
||||
redemptionCodesIncludedInBundle.forEach(code => {
|
||||
summaryLineItems.push(
|
||||
<RedemptionCodeLineSummary
|
||||
includedRedemptionCode={code}
|
||||
bundleListingData={bundleListingData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const includedCreditsAmount = includedListableProducts
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
);
|
||||
if (includedCreditsAmount) {
|
||||
summaryLineItems.push(
|
||||
<CreditsLineSummary includedCreditsAmount={includedCreditsAmount} />
|
||||
);
|
||||
}
|
||||
|
||||
const mobileLineItems = [];
|
||||
const desktopLineItems = [];
|
||||
summaryLineItems.forEach((item, index) => {
|
||||
if (index !== 0) {
|
||||
desktopLineItems.push(
|
||||
<Line noMargin key={`divider-${index}`}>
|
||||
<Spacer />
|
||||
<Column>
|
||||
<Divider orientation="vertical" />
|
||||
</Column>
|
||||
<Spacer />
|
||||
</Line>
|
||||
);
|
||||
if (index % 2 === 1) {
|
||||
mobileLineItems.push(
|
||||
<Column key={`divider-${index}`}>
|
||||
<Divider orientation="vertical" />
|
||||
</Column>
|
||||
);
|
||||
totalPrice += estimatedAmountInCents || 0;
|
||||
}
|
||||
}
|
||||
desktopLineItems.push(item);
|
||||
mobileLineItems.push(
|
||||
<Column expand noMargin key={`item-${index}`}>
|
||||
{item}
|
||||
</Column>
|
||||
);
|
||||
});
|
||||
|
||||
// Desktop, everything on one line.
|
||||
const desktopLines = [];
|
||||
desktopLines.push(
|
||||
<ResponsiveLineStackLayout expand key="desktop-line">
|
||||
{desktopLineItems}
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
// Mobile, 3 items by line (2 + 1 divider)
|
||||
const mobileLines = [];
|
||||
for (let i = 0; i < mobileLineItems.length; i += 3) {
|
||||
mobileLines.push(
|
||||
<LineStackLayout key={`mobile-line-${i}`} expand>
|
||||
{mobileLineItems.slice(i, i + 3)}
|
||||
</LineStackLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
mobileLines,
|
||||
desktopLines,
|
||||
};
|
||||
return `${currencySymbol} ${i18n
|
||||
.number(totalPrice / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
};
|
||||
|
@@ -186,15 +186,10 @@ const ExampleStore = ({
|
||||
)
|
||||
: [],
|
||||
onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => {
|
||||
const priceForUsageType = privateGameTemplateListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendGameTemplateInformationOpened({
|
||||
gameTemplateName: privateGameTemplateListingData.name,
|
||||
gameTemplateId: privateGameTemplateListingData.id,
|
||||
source: 'examples-list',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
onSelectPrivateGameTemplateListingData(
|
||||
privateGameTemplateListingData
|
||||
|
@@ -20,7 +20,6 @@ import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMea
|
||||
import Download from '../../UI/CustomSvgIcons/Download';
|
||||
import Add from '../../UI/CustomSvgIcons/Add';
|
||||
import ErrorBoundary from '../../UI/ErrorBoundary';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -55,7 +54,6 @@ const ExtensionsSearchDialog = ({
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
installExtension
|
||||
);
|
||||
const { showConfirmation, showAlert } = useAlertDialog();
|
||||
|
||||
const installOrImportExtension = async (
|
||||
i18n: I18nType,
|
||||
@@ -80,9 +78,7 @@ const ExtensionsSearchDialog = ({
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
onInstallExtension,
|
||||
showConfirmation,
|
||||
showAlert
|
||||
onInstallExtension
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -9,14 +9,8 @@ import {
|
||||
import { addSerializedExtensionsToProject } from '../InstallAsset';
|
||||
import { type EventsFunctionsExtensionsState } from '../../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { t } from '@lingui/macro';
|
||||
import Window from '../../Utils/Window';
|
||||
import { retryIfFailed } from '../../Utils/RetryIfFailed';
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
import {
|
||||
type ShowAlertDialogOptions,
|
||||
type ShowConfirmDialogOptions,
|
||||
} from '../../UI/Alert/AlertContext';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
/**
|
||||
* Download and add the extension in the project.
|
||||
@@ -57,9 +51,7 @@ export const importExtension = async (
|
||||
i18n: I18nType,
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
onWillInstallExtension: (extensionName: string) => void,
|
||||
showConfirmation: ShowConfirmDialogOptions => Promise<boolean>,
|
||||
showAlert: ShowAlertDialogOptions => Promise<void>
|
||||
onWillInstallExtension: (extensionName: string) => void
|
||||
): Promise<string | null> => {
|
||||
const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener();
|
||||
if (!eventsFunctionsExtensionOpener) return null;
|
||||
@@ -73,29 +65,12 @@ export const importExtension = async (
|
||||
);
|
||||
|
||||
if (project.hasEventsFunctionsExtensionNamed(serializedExtension.name)) {
|
||||
const answer = await showConfirmation({
|
||||
title: t`Replace existing extension`,
|
||||
message: t`An extension with this name already exists in the project. Importing this extension will replace it.`,
|
||||
confirmButtonLabel: `Replace`,
|
||||
});
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`An extension with this name already exists in the project. Importing this extension will replace it: are you sure you want to continue?`
|
||||
)
|
||||
);
|
||||
if (!answer) return null;
|
||||
} else {
|
||||
let hasConflictWithBuiltInExtension = false;
|
||||
const allExtensions = gd
|
||||
.asPlatform(gd.JsPlatform.get())
|
||||
.getAllPlatformExtensions();
|
||||
mapVector(allExtensions, extension => {
|
||||
if (extension.getName() === serializedExtension.name) {
|
||||
hasConflictWithBuiltInExtension = true;
|
||||
}
|
||||
});
|
||||
if (hasConflictWithBuiltInExtension) {
|
||||
await showAlert({
|
||||
title: t`Invalid name`,
|
||||
message: t`The extension can't be imported because it has the same name as a built-in extension.`,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onWillInstallExtension(serializedExtension.name);
|
||||
|
@@ -230,7 +230,7 @@ const getMergedInstalledWithDefaultEnumeratedObjectMetadataByCategory = ({
|
||||
iconFilename:
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMjMiIGN5PSI3IiByPSIzIi8+DQo8bGluZSBjbGFzcz0ic3QwIiB4MT0iMyIgeTE9IjciIHgyPSIyMCIgeTI9IjciLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjciIHgyPSIyNiIgeTI9IjciLz4NCjxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjEyIiBjeT0iMTYiIHI9IjMiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzIiB5MT0iMTYiIHgyPSI5IiB5Mj0iMTYiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjE2IiB4Mj0iMTUiIHkyPSIxNiIvPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMjMiIGN5PSIyNSIgcj0iMyIvPg0KPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMiIHkxPSIyNSIgeDI9IjIwIiB5Mj0iMjUiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjI1IiB4Mj0iMjYiIHkyPSIyNSIvPg0KPC9zdmc+DQo=',
|
||||
isRenderedIn3D: false,
|
||||
assetStorePackTag: 'slider',
|
||||
assetStorePackTag: 'settings ui',
|
||||
requiredExtensions: [
|
||||
{
|
||||
extensionName: 'PanelSpriteSlider',
|
||||
@@ -245,7 +245,7 @@ const getMergedInstalledWithDefaultEnumeratedObjectMetadataByCategory = ({
|
||||
iconFilename:
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIzLDIzSDljLTMuOSwwLTctMy4xLTctN3YwYzAtMy45LDMuMS03LDctN2gxNGMzLjksMCw3LDMuMSw3LDd2MEMzMCwxOS45LDI2LjksMjMsMjMsMjN6Ii8+DQo8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSI5IiBjeT0iMTYiIHI9IjQiLz4NCjwvc3ZnPg0K',
|
||||
isRenderedIn3D: false,
|
||||
assetStorePackTag: 'toggle switch',
|
||||
assetStorePackTag: 'settings ui',
|
||||
requiredExtensions: [
|
||||
{
|
||||
extensionName: 'SpriteToggleSwitch',
|
||||
|
@@ -455,8 +455,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
assetPackTag: assetPack.tag,
|
||||
assetPackKind: 'private',
|
||||
usageType: selectedUsageType,
|
||||
priceValue: price && price.value,
|
||||
priceCurrency: price && price.currency,
|
||||
currency: price ? price.currency : undefined,
|
||||
});
|
||||
|
||||
setPurchasingPrivateAssetPackListingData(privateAssetPackListingData);
|
||||
@@ -488,6 +487,15 @@ const PrivateAssetPackInformationPage = ({
|
||||
return;
|
||||
}
|
||||
|
||||
sendAssetPackBuyClicked({
|
||||
assetPackId: assetPack.id,
|
||||
assetPackName: assetPack.name,
|
||||
assetPackTag: assetPack.tag,
|
||||
assetPackKind: 'private',
|
||||
currency: 'CREDITS',
|
||||
usageType: selectedUsageType,
|
||||
});
|
||||
|
||||
const currentCreditsAmount = limits.credits.userBalance.amount;
|
||||
const assetPackPriceForUsageType = privateAssetPackListingData.creditPrices.find(
|
||||
price => price.usageType === selectedUsageType
|
||||
@@ -500,17 +508,6 @@ const PrivateAssetPackInformationPage = ({
|
||||
return;
|
||||
}
|
||||
const assetPackCreditsAmount = assetPackPriceForUsageType.amount;
|
||||
|
||||
sendAssetPackBuyClicked({
|
||||
assetPackId: assetPack.id,
|
||||
assetPackName: assetPack.name,
|
||||
assetPackTag: assetPack.tag,
|
||||
assetPackKind: 'private',
|
||||
priceValue: assetPackCreditsAmount,
|
||||
priceCurrency: 'CREDITS',
|
||||
usageType: selectedUsageType,
|
||||
});
|
||||
|
||||
if (currentCreditsAmount < assetPackCreditsAmount) {
|
||||
openCreditsPackageDialog({
|
||||
missingCredits: assetPackCreditsAmount - currentCreditsAmount,
|
||||
@@ -739,7 +736,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
label={<Trans>Browse assets</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<ColumnStackLayout noMargin>
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
@@ -752,10 +752,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
|
@@ -355,9 +355,8 @@ const PrivateGameTemplateInformationPage = ({
|
||||
gameTemplateId: gameTemplate.id,
|
||||
gameTemplateName: gameTemplate.name,
|
||||
gameTemplateTag: gameTemplate.tag,
|
||||
currency: price ? price.currency : undefined,
|
||||
usageType: selectedUsageType,
|
||||
priceValue: price && price.value,
|
||||
priceCurrency: price && price.currency,
|
||||
});
|
||||
|
||||
setPurchasingPrivateGameTemplateListingData(
|
||||
@@ -393,6 +392,14 @@ const PrivateGameTemplateInformationPage = ({
|
||||
return;
|
||||
}
|
||||
|
||||
sendGameTemplateBuyClicked({
|
||||
gameTemplateId: gameTemplate.id,
|
||||
gameTemplateName: gameTemplate.name,
|
||||
gameTemplateTag: gameTemplate.tag,
|
||||
usageType: selectedUsageType,
|
||||
currency: 'CREDITS',
|
||||
});
|
||||
|
||||
const currentCreditsAmount = limits.credits.userBalance.amount;
|
||||
const gameTemplatePriceForUsageType = privateGameTemplateListingData.creditPrices.find(
|
||||
price => price.usageType === selectedUsageType
|
||||
@@ -405,16 +412,6 @@ const PrivateGameTemplateInformationPage = ({
|
||||
return;
|
||||
}
|
||||
const gameTemplateCreditsAmount = gameTemplatePriceForUsageType.amount;
|
||||
|
||||
sendGameTemplateBuyClicked({
|
||||
gameTemplateId: gameTemplate.id,
|
||||
gameTemplateName: gameTemplate.name,
|
||||
gameTemplateTag: gameTemplate.tag,
|
||||
usageType: selectedUsageType,
|
||||
priceValue: gameTemplateCreditsAmount,
|
||||
priceCurrency: 'CREDITS',
|
||||
});
|
||||
|
||||
if (currentCreditsAmount < gameTemplateCreditsAmount) {
|
||||
openCreditsPackageDialog({
|
||||
missingCredits: gameTemplateCreditsAmount - currentCreditsAmount,
|
||||
@@ -599,7 +596,10 @@ const PrivateGameTemplateInformationPage = ({
|
||||
/>
|
||||
<Spacer />
|
||||
{!isAlreadyReceived ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
@@ -614,10 +614,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : onCreateWithGameTemplate ? (
|
||||
<OpenProductButton
|
||||
productListingData={privateGameTemplateListingData}
|
||||
|
@@ -214,15 +214,10 @@ export const PrivateGameTemplateStoreStateProvider = ({
|
||||
);
|
||||
|
||||
if (privateGameTemplateListingData) {
|
||||
const priceForUsageType = privateGameTemplateListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendGameTemplateInformationOpened({
|
||||
gameTemplateName: privateGameTemplateListingData.name,
|
||||
gameTemplateId: privateGameTemplateListingData.id,
|
||||
source: 'web-link',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
shopNavigationState.openPrivateGameTemplateInformationPage({
|
||||
privateGameTemplateListingData,
|
||||
|
@@ -1,133 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Text from '../UI/Text';
|
||||
import { Column, Spacer } from '../UI/Grid';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
|
||||
const styles = {
|
||||
limitedTimeContainer: {
|
||||
display: 'flex',
|
||||
backgroundColor: '#F03F18',
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
padding: '8px 0',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
visibleUntil: string,
|
||||
hideMinutesAndSeconds?: boolean,
|
||||
alignCenter?: boolean,
|
||||
|};
|
||||
|
||||
const ProductLimitedTimeOffer = ({
|
||||
visibleUntil,
|
||||
hideMinutesAndSeconds,
|
||||
alignCenter,
|
||||
}: Props) => {
|
||||
const [timeLeft, setTimeLeft] = React.useState<{|
|
||||
days: number,
|
||||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
|}>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const updateCountdown = () => {
|
||||
const now = new Date().getTime();
|
||||
const targetTime = new Date(visibleUntil).getTime();
|
||||
const difference = targetTime - now;
|
||||
|
||||
if (difference > 0) {
|
||||
setTimeLeft({
|
||||
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor(
|
||||
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
),
|
||||
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((difference % (1000 * 60)) / 1000),
|
||||
});
|
||||
} else {
|
||||
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
},
|
||||
[visibleUntil]
|
||||
);
|
||||
|
||||
if (!timeLeft) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.limitedTimeContainer}>
|
||||
<Column
|
||||
justifyContent="center"
|
||||
expand
|
||||
alignItems={alignCenter ? 'center' : undefined}
|
||||
>
|
||||
<Text>
|
||||
<Trans>Limited time offer:</Trans>
|
||||
</Text>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<Text noMargin size="block-title">
|
||||
{String(timeLeft.days).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Days</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.hours).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Hours</Trans>
|
||||
</Text>
|
||||
{!hideMinutesAndSeconds && (
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.minutes).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Minutes</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.seconds).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Seconds</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductLimitedTimeOffer;
|
@@ -319,7 +319,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
onPrivateGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
product: ?PrivateAssetPack | PrivateGameTemplate | Bundle | Course,
|
||||
productListingDatas: ?Array<
|
||||
@@ -344,7 +343,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
) => void,
|
||||
onBundleOpen?: (bundleListingData: BundleListingData) => void,
|
||||
onCourseOpen?: (courseListingData: CourseListingData) => void,
|
||||
discountedPrice?: boolean,
|
||||
|}): ?Array<React.Node> => {
|
||||
if (!product || !productListingDatas) return null;
|
||||
|
||||
@@ -380,7 +378,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
onPrivateGameTemplateOpen(includedProductListingData)
|
||||
}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -398,7 +395,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onPrivateAssetPackOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -416,7 +412,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onBundleOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -434,7 +429,6 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onCourseOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -535,7 +529,6 @@ export const PurchaseProductButtons = <
|
||||
isAlreadyReceived,
|
||||
onClickBuy,
|
||||
onClickBuyWithCredits,
|
||||
customLabel,
|
||||
}: {|
|
||||
productListingData: T,
|
||||
selectedUsageType: string,
|
||||
@@ -545,7 +538,6 @@ export const PurchaseProductButtons = <
|
||||
isAlreadyReceived: boolean,
|
||||
onClickBuy: () => void | Promise<void>,
|
||||
onClickBuyWithCredits?: () => void | Promise<void>,
|
||||
customLabel?: React.Node,
|
||||
|}) => {
|
||||
const { authenticated } = React.useContext(AuthenticatedUserContext);
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
@@ -586,13 +578,10 @@ export const PurchaseProductButtons = <
|
||||
<LineStackLayout>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
customLabel || <Trans>Buy for {formattedProductPriceText}</Trans>
|
||||
}
|
||||
label={<Trans>Buy for {formattedProductPriceText}</Trans>}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
icon={<Coin fontSize="small" />}
|
||||
size="medium"
|
||||
/>
|
||||
{!isAlreadyReceived && !authenticated && (
|
||||
<Text size="body-small">
|
||||
@@ -603,27 +592,21 @@ export const PurchaseProductButtons = <
|
||||
)}
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<LineStackLayout noMargin>
|
||||
<LineStackLayout>
|
||||
{creditPrice && (
|
||||
<FlatButton
|
||||
primary
|
||||
label={
|
||||
customLabel || <Trans>Buy for {creditPrice.amount} credits</Trans>
|
||||
}
|
||||
label={<Trans>Buy for {creditPrice.amount} credits</Trans>}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
leftIcon={<Coin fontSize="small" />}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
customLabel || <Trans>Buy for {formattedProductPriceText}</Trans>
|
||||
}
|
||||
label={<Trans>Buy for {formattedProductPriceText}</Trans>}
|
||||
onClick={onClickBuy}
|
||||
id={`buy-${productType}`}
|
||||
size="medium"
|
||||
/>
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
@@ -38,22 +38,6 @@ const styles = {
|
||||
marginTop: -3,
|
||||
marginBottom: -1,
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
};
|
||||
|
||||
export const renderPriceFormatted = (
|
||||
priceInCents: number,
|
||||
currencyCode: 'USD' | 'EUR',
|
||||
i18n: I18nType
|
||||
): string => {
|
||||
const currencySymbol = currencyCode === 'USD' ? '$' : '€';
|
||||
// Use a non-breaking space to ensure the currency stays next to the price.
|
||||
return `${currencySymbol}\u00A0${i18n
|
||||
.number(priceInCents / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
};
|
||||
|
||||
type FormatProps = {|
|
||||
@@ -67,7 +51,6 @@ type FormatProps = {|
|
||||
usageType?: string,
|
||||
plainText?: boolean,
|
||||
showBothPrices?: 'column' | 'line', // If defined, will show both the credits price and the product price.
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
export const renderProductPrice = ({
|
||||
@@ -76,7 +59,6 @@ export const renderProductPrice = ({
|
||||
usageType,
|
||||
plainText,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
}: FormatProps): React.Node => {
|
||||
// For Credits packages & Bundles, on mobile, only show the app store product price.
|
||||
if (
|
||||
@@ -97,9 +79,8 @@ export const renderProductPrice = ({
|
||||
? creditPrices[0]
|
||||
: null;
|
||||
|
||||
// If we're on mobile, only show credits prices for other products,
|
||||
// except if we're showing the discounted price.
|
||||
if (shouldUseAppStoreProduct() && !discountedPrice) {
|
||||
// If we're on mobile, only show credits prices for other packages.
|
||||
if (shouldUseAppStoreProduct()) {
|
||||
if (!creditPrice) return '';
|
||||
return plainText ? (
|
||||
i18n._(t`${creditPrice.amount} credits`)
|
||||
@@ -122,15 +103,17 @@ export const renderProductPrice = ({
|
||||
: null;
|
||||
if (!price) return '';
|
||||
|
||||
const formattedPrice = renderPriceFormatted(
|
||||
price.value,
|
||||
price.currency,
|
||||
i18n
|
||||
);
|
||||
const currencyCode = price.currency === 'USD' ? '$' : '€';
|
||||
const formattedPrice = `${currencyCode} ${i18n
|
||||
.number(price.value / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
|
||||
return plainText ? (
|
||||
formattedPrice
|
||||
) : showBothPrices && creditPrice && !discountedPrice ? (
|
||||
) : showBothPrices && creditPrice ? (
|
||||
showBothPrices === 'column' ? (
|
||||
<Column alignItems="flex-end">
|
||||
<div style={styles.creditPriceContainer}>
|
||||
@@ -166,11 +149,7 @@ export const renderProductPrice = ({
|
||||
)
|
||||
) : (
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{discountedPrice ? (
|
||||
<span style={styles.discountedPrice}>{formattedPrice}</span>
|
||||
) : (
|
||||
formattedPrice
|
||||
)}
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -186,7 +165,6 @@ type ProductPriceOrOwnedProps = {|
|
||||
usageType?: string,
|
||||
owned?: boolean,
|
||||
showBothPrices?: 'column' | 'line',
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
export const OwnedLabel = () => {
|
||||
@@ -211,18 +189,11 @@ export const getProductPriceOrOwnedLabel = ({
|
||||
usageType,
|
||||
owned,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
}: ProductPriceOrOwnedProps): React.Node => {
|
||||
return owned ? (
|
||||
<OwnedLabel />
|
||||
) : (
|
||||
renderProductPrice({
|
||||
i18n,
|
||||
productListingData,
|
||||
usageType,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
})
|
||||
renderProductPrice({ i18n, productListingData, usageType, showBothPrices })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -240,7 +211,6 @@ type ProductPriceTagProps = {|
|
||||
*/
|
||||
withOverlay?: boolean,
|
||||
owned?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
const ProductPriceTag = ({
|
||||
@@ -248,7 +218,6 @@ const ProductPriceTag = ({
|
||||
usageType,
|
||||
withOverlay,
|
||||
owned,
|
||||
discountedPrice,
|
||||
}: ProductPriceTagProps) => {
|
||||
return (
|
||||
<I18n>
|
||||
@@ -258,7 +227,6 @@ const ProductPriceTag = ({
|
||||
productListingData,
|
||||
usageType,
|
||||
owned,
|
||||
discountedPrice,
|
||||
});
|
||||
|
||||
return <PriceTag withOverlay={withOverlay} label={label} />;
|
||||
|
@@ -1,23 +1,52 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { LineStackLayout } from '../../UI/Layout';
|
||||
import Visa from '../../UI/CustomSvgIcons/Visa';
|
||||
import MasterCard from '../../UI/CustomSvgIcons/MasterCard';
|
||||
import Paypal from '../../UI/CustomSvgIcons/Paypal';
|
||||
import Text from '../../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import ShieldChecked from '../../UI/CustomSvgIcons/ShieldChecked';
|
||||
|
||||
const SecureCheckout = () => {
|
||||
const styles = {
|
||||
logoContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 8,
|
||||
},
|
||||
};
|
||||
|
||||
const LogoContainer = ({ children }) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<ShieldChecked style={{ color: gdevelopTheme.message.valid }} />
|
||||
<Text color="secondary">
|
||||
<Trans>Paypal secure</Trans>
|
||||
</Text>
|
||||
<ShieldChecked style={{ color: gdevelopTheme.message.valid }} />
|
||||
<Text color="secondary">
|
||||
<Trans>Stripe secure</Trans>
|
||||
<div
|
||||
style={{
|
||||
...styles.logoContainer,
|
||||
background: gdevelopTheme.palette.secondary,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SecureCheckout = () => {
|
||||
return (
|
||||
<LineStackLayout>
|
||||
<Text>
|
||||
<Trans>Secure Checkout:</Trans>
|
||||
</Text>
|
||||
<LogoContainer>
|
||||
<Paypal />
|
||||
</LogoContainer>
|
||||
<LogoContainer>
|
||||
<Visa />
|
||||
</LogoContainer>
|
||||
<LogoContainer>
|
||||
<MasterCard />
|
||||
</LogoContainer>
|
||||
</LineStackLayout>
|
||||
);
|
||||
};
|
||||
|
@@ -244,7 +244,6 @@ export const PrivateAssetPackTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
assetPackListingData: PrivateAssetPackListingData,
|
||||
onSelect: () => void,
|
||||
@@ -252,7 +251,6 @@ export const PrivateAssetPackTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
@@ -293,7 +291,6 @@ export const PrivateAssetPackTile = ({
|
||||
productListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -467,7 +464,6 @@ export const PrivateGameTemplateTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
onSelect: () => void,
|
||||
@@ -475,7 +471,6 @@ export const PrivateGameTemplateTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -505,7 +500,6 @@ export const PrivateGameTemplateTile = ({
|
||||
productListingData={privateGameTemplateListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -528,7 +522,6 @@ export const CourseTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
courseListingData: CourseListingData,
|
||||
onSelect: () => void,
|
||||
@@ -536,7 +529,6 @@ export const CourseTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -564,7 +556,6 @@ export const CourseTile = ({
|
||||
productListingData={courseListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -587,7 +578,6 @@ export const BundleTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
bundleListingData: BundleListingData,
|
||||
onSelect: () => void,
|
||||
@@ -595,7 +585,6 @@ export const BundleTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -627,7 +616,6 @@ export const BundleTile = ({
|
||||
productListingData={bundleListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
) : (
|
||||
<OwnedLabel />
|
||||
|
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
|
||||
import Tune from '../UI/CustomSvgIcons/Tune';
|
||||
@@ -69,8 +68,6 @@ import { AssetSwappingAssetStoreSearchFilter } from './AssetStoreSearchFilter';
|
||||
import { delay } from '../Utils/Delay';
|
||||
import { BundleStoreContext } from './Bundles/BundleStoreContext';
|
||||
import BundleInformationPage from './Bundles/BundleInformationPage';
|
||||
import { type CourseCompletion } from '../MainFrame/EditorContainers/HomePage/UseCourses';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
type Props = {|
|
||||
onlyShowAssets?: boolean, // TODO: if we add more options, use an array instead.
|
||||
@@ -79,11 +76,8 @@ type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onOpenProfile?: () => void,
|
||||
courses?: ?Array<Course>,
|
||||
receivedCourses?: ?Array<Course>,
|
||||
onCourseOpen?: (courseId: string) => void,
|
||||
getSubscriptionPlansWithPricingSystems?: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
getCourseCompletion?: (courseId: string) => CourseCompletion | null,
|
||||
assetSwappedObject?: ?gdObject,
|
||||
minimalUI?: boolean,
|
||||
|};
|
||||
@@ -124,11 +118,8 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
displayPromotions,
|
||||
onOpenPrivateGameTemplateListingData,
|
||||
onOpenProfile,
|
||||
courses,
|
||||
receivedCourses,
|
||||
onCourseOpen,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
getCourseCompletion,
|
||||
assetSwappedObject,
|
||||
minimalUI,
|
||||
}: Props,
|
||||
@@ -435,16 +426,10 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
|
||||
if (!receivedAssetPack || (options && options.forceProductPage)) {
|
||||
// The user has not received the pack, open the page to buy it.
|
||||
const priceForUsageType = privateAssetPackListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
|
||||
sendAssetPackInformationOpened({
|
||||
assetPackName: privateAssetPackListingData.name,
|
||||
assetPackId: privateAssetPackListingData.id,
|
||||
assetPackKind: 'private',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
saveScrollPosition();
|
||||
shopNavigationState.openPrivateAssetPackInformationPage({
|
||||
@@ -495,15 +480,10 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
|
||||
const selectPrivateGameTemplate = React.useCallback(
|
||||
(privateGameTemplateListingData: PrivateGameTemplateListingData) => {
|
||||
const priceForUsageType = privateGameTemplateListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendGameTemplateInformationOpened({
|
||||
gameTemplateName: privateGameTemplateListingData.name,
|
||||
gameTemplateId: privateGameTemplateListingData.id,
|
||||
source: 'store',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
saveScrollPosition();
|
||||
shopNavigationState.openPrivateGameTemplateInformationPage({
|
||||
@@ -517,15 +497,10 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
|
||||
const selectBundle = React.useCallback(
|
||||
(bundleListingData: BundleListingData) => {
|
||||
const priceForUsageType = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendBundleInformationOpened({
|
||||
bundleName: bundleListingData.name,
|
||||
bundleId: bundleListingData.id,
|
||||
source: 'store',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
saveScrollPosition();
|
||||
shopNavigationState.openBundleInformationPage({
|
||||
@@ -539,15 +514,10 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
|
||||
const selectCourse = React.useCallback(
|
||||
(courseListingData: CourseListingData) => {
|
||||
const priceForUsageType = courseListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendCourseInformationOpened({
|
||||
courseName: courseListingData.name,
|
||||
courseId: courseListingData.id,
|
||||
source: 'store',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
if (onCourseOpen) onCourseOpen(courseListingData.id);
|
||||
},
|
||||
@@ -690,323 +660,291 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const onBack = React.useCallback(
|
||||
async () => {
|
||||
const page = shopNavigationState.backToPreviousPage();
|
||||
const isUpdatingSearchtext = reApplySearchTextIfNeeded(page);
|
||||
if (isUpdatingSearchtext) {
|
||||
// Updating the search is not instant, so we cannot apply the scroll position
|
||||
// right away. We force a wait as there's no easy way to know when results are completely updated.
|
||||
await delay(500);
|
||||
setScrollUpdateIsNeeded(page);
|
||||
applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again.
|
||||
} else {
|
||||
setScrollUpdateIsNeeded(page);
|
||||
}
|
||||
},
|
||||
[
|
||||
shopNavigationState,
|
||||
reApplySearchTextIfNeeded,
|
||||
setScrollUpdateIsNeeded,
|
||||
applyBackScrollPosition,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column
|
||||
expand
|
||||
noMargin
|
||||
useFullHeight
|
||||
noOverflowParent
|
||||
id="asset-store"
|
||||
>
|
||||
<>
|
||||
<LineStackLayout>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
id="home-button"
|
||||
key="back-discover"
|
||||
tooltip={t`Back to discover`}
|
||||
onClick={() => {
|
||||
setSearchText('');
|
||||
const page = assetSwappedObject
|
||||
? shopNavigationState.openAssetSwapping()
|
||||
: shopNavigationState.openHome();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
clearAllAssetStoreFilters();
|
||||
setIsFiltersPanelOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Home />
|
||||
</IconButton>
|
||||
)}
|
||||
<Column expand useFullHeight noMargin>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
onlyShowAssets ? t`Search assets` : t`Search the shop`
|
||||
<Column expand noMargin useFullHeight noOverflowParent id="asset-store">
|
||||
<>
|
||||
<LineStackLayout>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
id="home-button"
|
||||
key="back-discover"
|
||||
tooltip={t`Back to discover`}
|
||||
onClick={() => {
|
||||
setSearchText('');
|
||||
const page = assetSwappedObject
|
||||
? shopNavigationState.openAssetSwapping()
|
||||
: shopNavigationState.openHome();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
clearAllAssetStoreFilters();
|
||||
setIsFiltersPanelOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Home />
|
||||
</IconButton>
|
||||
)}
|
||||
<Column expand useFullHeight noMargin>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
onlyShowAssets ? t`Search assets` : t`Search the shop`
|
||||
}
|
||||
value={searchText}
|
||||
onChange={(newValue: string) => {
|
||||
if (searchText === newValue || newValue.length === 1) {
|
||||
return;
|
||||
}
|
||||
setSearchText(newValue);
|
||||
if (isOnSearchResultPage) {
|
||||
// An existing search is already being done: just move to the
|
||||
// top search results.
|
||||
shopNavigationState.openSearchResultPage();
|
||||
const assetsListInterface = assetsList.current;
|
||||
if (assetsListInterface) {
|
||||
assetsListInterface.scrollToPosition(0);
|
||||
assetsListInterface.setPageBreakIndex(0);
|
||||
}
|
||||
value={searchText}
|
||||
onChange={(newValue: string) => {
|
||||
if (searchText === newValue || newValue.length === 1) {
|
||||
return;
|
||||
}
|
||||
setSearchText(newValue);
|
||||
if (isOnSearchResultPage) {
|
||||
// An existing search is already being done: just move to the
|
||||
// top search results.
|
||||
shopNavigationState.openSearchResultPage();
|
||||
const assetsListInterface = assetsList.current;
|
||||
if (assetsListInterface) {
|
||||
assetsListInterface.scrollToPosition(0);
|
||||
assetsListInterface.setPageBreakIndex(0);
|
||||
}
|
||||
} else {
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onRequestSearch={() => {}}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
</Column>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
onClick={() => setIsFiltersPanelOpen(!isFiltersPanelOpen)}
|
||||
disabled={!canShowFiltersPanel}
|
||||
selected={canShowFiltersPanel && isFiltersPanelOpen}
|
||||
size="small"
|
||||
>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
</>
|
||||
<Column noMargin>
|
||||
<Line justifyContent="space-between" noMargin alignItems="center">
|
||||
{(!isOnHomePage || !!openedShopCategory) &&
|
||||
!(assetSwappedObject && minimalUI) && (
|
||||
} else {
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onRequestSearch={() => {}}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
</Column>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
onClick={() => setIsFiltersPanelOpen(!isFiltersPanelOpen)}
|
||||
disabled={!canShowFiltersPanel}
|
||||
selected={canShowFiltersPanel && isFiltersPanelOpen}
|
||||
size="small"
|
||||
>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
</>
|
||||
<Column noMargin>
|
||||
<Line justifyContent="space-between" noMargin alignItems="center">
|
||||
{(!isOnHomePage || !!openedShopCategory) &&
|
||||
!(assetSwappedObject && minimalUI) && (
|
||||
<>
|
||||
{shopNavigationState.isRootPage ? null : (
|
||||
<Column expand alignItems="flex-start" noMargin>
|
||||
<TextButton
|
||||
icon={<ChevronArrowLeft />}
|
||||
label={<Trans>Back</Trans>}
|
||||
onClick={async () => {
|
||||
const page = shopNavigationState.backToPreviousPage();
|
||||
const isUpdatingSearchtext = reApplySearchTextIfNeeded(
|
||||
page
|
||||
);
|
||||
if (isUpdatingSearchtext) {
|
||||
// Updating the search is not instant, so we cannot apply the scroll position
|
||||
// right away. We force a wait as there's no easy way to know when results are completely updated.
|
||||
await delay(500);
|
||||
setScrollUpdateIsNeeded(page);
|
||||
applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again.
|
||||
} else {
|
||||
setScrollUpdateIsNeeded(page);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
{(openedAssetPack ||
|
||||
openedPrivateAssetPackListingData ||
|
||||
filtersState.chosenCategory) && (
|
||||
<>
|
||||
{shopNavigationState.isRootPage ||
|
||||
// Don't show back action on bundle pages, as it's handled by the page itself.
|
||||
openedBundleListingData ? null : (
|
||||
<Column expand alignItems="flex-start" noMargin>
|
||||
<TextButton
|
||||
icon={<ChevronArrowLeft />}
|
||||
label={<Trans>Back</Trans>}
|
||||
onClick={onBack}
|
||||
/>
|
||||
{!openedAssetPack && !openedPrivateAssetPackListingData && (
|
||||
// Only show the category name if we're not on an asset pack page.
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{filtersState.chosenCategory
|
||||
? capitalize(
|
||||
filtersState.chosenCategory.node.name
|
||||
)
|
||||
: ''}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
{(openedAssetPack ||
|
||||
openedPrivateAssetPackListingData ||
|
||||
filtersState.chosenCategory) && (
|
||||
<>
|
||||
{!openedAssetPack &&
|
||||
!openedPrivateAssetPackListingData && (
|
||||
// Only show the category name if we're not on an asset pack page.
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{filtersState.chosenCategory
|
||||
? capitalize(
|
||||
filtersState.chosenCategory.node.name
|
||||
)
|
||||
: ''}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
<Column
|
||||
expand
|
||||
alignItems="flex-end"
|
||||
noMargin
|
||||
justifyContent="center"
|
||||
>
|
||||
{openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
doesAssetPackContainAudio(openedAssetPack) &&
|
||||
!isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
<Column
|
||||
expand
|
||||
alignItems="flex-end"
|
||||
noMargin
|
||||
justifyContent="center"
|
||||
>
|
||||
{openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
doesAssetPackContainAudio(openedAssetPack) &&
|
||||
!isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
<Line
|
||||
expand
|
||||
noMargin
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
{isOnHomePage ? (
|
||||
storeError ? (
|
||||
<PlaceholderError onRetry={fetchAssetsAndGameTemplates}>
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
An error occurred when fetching the store content.
|
||||
Please try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</PlaceholderError>
|
||||
) : publicAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
bundleListingDatas ? (
|
||||
<AssetsHome
|
||||
ref={assetsHome}
|
||||
publicAssetPacks={publicAssetPacks}
|
||||
privateAssetPackListingDatas={privateAssetPackListingDatas}
|
||||
privateGameTemplateListingDatas={
|
||||
privateGameTemplateListingDatas
|
||||
}
|
||||
bundleListingDatas={bundleListingDatas}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onCategorySelection={selectShopCategory}
|
||||
openedShopCategory={openedShopCategory}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
displayPromotions={displayPromotions}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)
|
||||
) : isOnSearchResultPage ? (
|
||||
<AssetsList
|
||||
publicAssetPacks={
|
||||
assetSwappedObject ? [] : publicAssetPacksSearchResults
|
||||
}
|
||||
privateAssetPackListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateAssetPackListingDatasSearchResults
|
||||
}
|
||||
privateGameTemplateListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateGameTemplateListingDatasSearchResults
|
||||
}
|
||||
bundleListingDatas={
|
||||
assetSwappedObject ? [] : bundleListingDatasSearchResults
|
||||
}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
ref={assetsList}
|
||||
error={storeError}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onFolderSelection={selectFolder}
|
||||
onGoBackToFolderIndex={goBackToFolderIndex}
|
||||
currentPage={shopNavigationState.getCurrentPage()}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
hideDetails={!!assetSwappedObject && !!minimalUI}
|
||||
/>
|
||||
) : // Do not show the asset details if we're swapping an asset.
|
||||
openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? (
|
||||
<AssetDetails
|
||||
ref={assetDetails}
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={
|
||||
openedPrivateAssetPackListingData
|
||||
}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateAssetPackListingDatasFromSameCreator={
|
||||
privateAssetPackListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedPrivateGameTemplateListingData ? (
|
||||
<PrivateGameTemplateInformationPage
|
||||
privateGameTemplateListingData={
|
||||
openedPrivateGameTemplateListingData
|
||||
}
|
||||
onCreateWithGameTemplate={() => {
|
||||
onOpenPrivateGameTemplateListingData &&
|
||||
onOpenPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedBundleListingData &&
|
||||
getSubscriptionPlansWithPricingSystems &&
|
||||
getCourseCompletion ? (
|
||||
<BundleInformationPage
|
||||
bundleListingData={openedBundleListingData}
|
||||
noPadding
|
||||
onBack={onBack}
|
||||
onBundleOpen={selectBundle}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onCourseOpen={selectCourse}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
courses={courses}
|
||||
receivedCourses={receivedCourses}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
/>
|
||||
) : null}
|
||||
{canShowFiltersPanel && (
|
||||
<ResponsivePaperOrDrawer
|
||||
onClose={() => setIsFiltersPanelOpen(false)}
|
||||
open={isFiltersPanelOpen}
|
||||
>
|
||||
<ScrollView>
|
||||
<Column>
|
||||
<Column noMargin>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetSwappedObject={assetSwappedObject}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</ResponsivePaperOrDrawer>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
</Line>
|
||||
</Column>
|
||||
<Line
|
||||
expand
|
||||
noMargin
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
{isOnHomePage ? (
|
||||
storeError ? (
|
||||
<PlaceholderError onRetry={fetchAssetsAndGameTemplates}>
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
An error occurred when fetching the store content. Please
|
||||
try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</PlaceholderError>
|
||||
) : publicAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
bundleListingDatas ? (
|
||||
<AssetsHome
|
||||
ref={assetsHome}
|
||||
publicAssetPacks={publicAssetPacks}
|
||||
privateAssetPackListingDatas={privateAssetPackListingDatas}
|
||||
privateGameTemplateListingDatas={
|
||||
privateGameTemplateListingDatas
|
||||
}
|
||||
bundleListingDatas={bundleListingDatas}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onCategorySelection={selectShopCategory}
|
||||
openedShopCategory={openedShopCategory}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
displayPromotions={displayPromotions}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)
|
||||
) : isOnSearchResultPage ? (
|
||||
<AssetsList
|
||||
publicAssetPacks={
|
||||
assetSwappedObject ? [] : publicAssetPacksSearchResults
|
||||
}
|
||||
privateAssetPackListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateAssetPackListingDatasSearchResults
|
||||
}
|
||||
privateGameTemplateListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateGameTemplateListingDatasSearchResults
|
||||
}
|
||||
bundleListingDatas={
|
||||
assetSwappedObject ? [] : bundleListingDatasSearchResults
|
||||
}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
ref={assetsList}
|
||||
error={storeError}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onFolderSelection={selectFolder}
|
||||
onGoBackToFolderIndex={goBackToFolderIndex}
|
||||
currentPage={shopNavigationState.getCurrentPage()}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
hideDetails={!!assetSwappedObject && !!minimalUI}
|
||||
/>
|
||||
) : // Do not show the asset details if we're swapping an asset.
|
||||
openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? (
|
||||
<AssetDetails
|
||||
ref={assetDetails}
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={openedPrivateAssetPackListingData}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateAssetPackListingDatasFromSameCreator={
|
||||
privateAssetPackListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedPrivateGameTemplateListingData ? (
|
||||
<PrivateGameTemplateInformationPage
|
||||
privateGameTemplateListingData={
|
||||
openedPrivateGameTemplateListingData
|
||||
}
|
||||
onCreateWithGameTemplate={() => {
|
||||
onOpenPrivateGameTemplateListingData &&
|
||||
onOpenPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedBundleListingData ? (
|
||||
<BundleInformationPage
|
||||
bundleListingData={openedBundleListingData}
|
||||
receivedCourses={receivedCourses}
|
||||
onBundleOpen={selectBundle}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onCourseOpen={selectCourse}
|
||||
/>
|
||||
) : null}
|
||||
{canShowFiltersPanel && (
|
||||
<ResponsivePaperOrDrawer
|
||||
onClose={() => setIsFiltersPanelOpen(false)}
|
||||
open={isFiltersPanelOpen}
|
||||
>
|
||||
<ScrollView>
|
||||
<Column>
|
||||
<Column noMargin>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetSwappedObject={assetSwappedObject}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</ResponsivePaperOrDrawer>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -29,7 +29,6 @@ type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onChoose: (type: string, defaultName: string) => void,
|
||||
@@ -44,7 +43,6 @@ export default function NewBehaviorDialog({
|
||||
onChoose,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
onExtensionInstalled,
|
||||
}: Props) {
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
@@ -226,7 +224,6 @@ export default function NewBehaviorDialog({
|
||||
project={project}
|
||||
objectType={objectType}
|
||||
objectBehaviorsTypes={objectBehaviorsTypes}
|
||||
isChildObject={isChildObject}
|
||||
isInstalling={isInstalling}
|
||||
onInstall={async shortHeader =>
|
||||
onInstallExtension(i18n, shortHeader)
|
||||
|
@@ -71,7 +71,6 @@ type BehaviorConfigurationEditorProps = {|
|
||||
project: gdProject,
|
||||
object: gdObject,
|
||||
behavior: gdBehavior,
|
||||
isChildObject: boolean,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
onBehaviorsUpdated: () => void,
|
||||
onChangeBehaviorName: (behavior: gdBehavior, newName: string) => void,
|
||||
@@ -95,7 +94,6 @@ const BehaviorConfigurationEditor = React.forwardRef<
|
||||
project,
|
||||
object,
|
||||
behavior,
|
||||
isChildObject,
|
||||
resourceManagementProps,
|
||||
onBehaviorsUpdated,
|
||||
onChangeBehaviorName,
|
||||
@@ -313,7 +311,6 @@ type UseManageBehaviorsState = {|
|
||||
export const useManageObjectBehaviors = ({
|
||||
project,
|
||||
object,
|
||||
isChildObject,
|
||||
eventsFunctionsExtension,
|
||||
onUpdate,
|
||||
onSizeUpdated,
|
||||
@@ -323,7 +320,6 @@ export const useManageObjectBehaviors = ({
|
||||
}: {
|
||||
project: gdProject,
|
||||
object: gdObject,
|
||||
isChildObject: boolean,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
onUpdate: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
@@ -590,7 +586,6 @@ export const useManageObjectBehaviors = ({
|
||||
open
|
||||
objectType={object.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(object)}
|
||||
isChildObject={isChildObject}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
project={project}
|
||||
@@ -620,7 +615,6 @@ type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
object: gdObject,
|
||||
isChildObject: boolean,
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
@@ -642,7 +636,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
|
||||
const {
|
||||
object,
|
||||
isChildObject,
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
onSizeUpdated,
|
||||
@@ -672,7 +665,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
} = useManageObjectBehaviors({
|
||||
project,
|
||||
object,
|
||||
isChildObject,
|
||||
eventsFunctionsExtension,
|
||||
onUpdate: forceUpdate,
|
||||
onSizeUpdated,
|
||||
@@ -793,7 +785,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
key={behaviorName}
|
||||
project={project}
|
||||
object={object}
|
||||
isChildObject={isChildObject}
|
||||
behavior={behavior}
|
||||
copyBehavior={copyBehavior}
|
||||
onRemoveBehavior={removeBehavior}
|
||||
|
@@ -1,102 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../UI/Layout';
|
||||
import { Line } from '../UI/Grid';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import Trash from '../UI/CustomSvgIcons/Trash';
|
||||
|
||||
export type Choice = {|
|
||||
value: string,
|
||||
label: string,
|
||||
|};
|
||||
|
||||
type ChoicesEditorProps = {|
|
||||
choices: Array<Choice>,
|
||||
setChoices: (Array<Choice>) => void,
|
||||
disabled?: boolean,
|
||||
hideLabels?: boolean,
|
||||
|};
|
||||
|
||||
const ChoicesEditor = ({
|
||||
choices,
|
||||
setChoices,
|
||||
disabled,
|
||||
hideLabels,
|
||||
}: ChoicesEditorProps) => {
|
||||
const updateChoices = () => setChoices(choices);
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<ColumnStackLayout justifyContent="flex-end" expand noMargin>
|
||||
{choices.map((item, index) => (
|
||||
<LineStackLayout
|
||||
key={index}
|
||||
justifyContent="flex-end"
|
||||
expand
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Value</Trans>}
|
||||
value={item.value}
|
||||
onChange={text => {
|
||||
choices[index].value = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{!hideLabels && (
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Label displayed in editor</Trans>}
|
||||
value={item.label}
|
||||
onChange={text => {
|
||||
choices[index].label = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
tooltip={t`Delete option`}
|
||||
onClick={() => {
|
||||
choices.splice(index, 1);
|
||||
updateChoices();
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
))}
|
||||
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
disabled={disabled}
|
||||
primary
|
||||
onClick={() => {
|
||||
choices.push({ value: 'New Option', label: '' });
|
||||
updateChoices();
|
||||
}}
|
||||
label={<Trans>Add a new option</Trans>}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChoicesEditor;
|
@@ -96,48 +96,6 @@ const parseVariablePath = (
|
||||
return segments;
|
||||
};
|
||||
|
||||
const parseValueAsObjectOrArray = (value: string): Array<any> | {} | null => {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
|
||||
if (Array.isArray(parsedValue) || typeof parsedValue === 'object') {
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
// Value is a primitive, not an object or array.
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Not even a JSON string (probably a primitive or a string).
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const convertJsObjectToVariable = (value: any, variable: gdVariable) => {
|
||||
if (value === null) {
|
||||
variable.setString('null');
|
||||
} else if (value === undefined) {
|
||||
variable.setString('undefined');
|
||||
} else if (typeof value === 'number') {
|
||||
variable.setValue(value || 0);
|
||||
} else if (typeof value === 'string') {
|
||||
variable.setString(value);
|
||||
} else if (typeof value === 'boolean') {
|
||||
variable.setBool(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
variable.castTo('Array');
|
||||
variable.clearChildren();
|
||||
for (const item of value) {
|
||||
convertJsObjectToVariable(item, variable.pushNew());
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
variable.castTo('Structure');
|
||||
variable.clearChildren();
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
convertJsObjectToVariable(item, variable.getChild(key));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const applyVariableChange = ({
|
||||
variablePath,
|
||||
forcedVariableType,
|
||||
@@ -196,35 +154,19 @@ export const applyVariableChange = ({
|
||||
}
|
||||
}
|
||||
|
||||
const arrayOrObjectValue = parseValueAsObjectOrArray(value);
|
||||
const variableType = readOrInferVariableType(forcedVariableType, value);
|
||||
|
||||
if (arrayOrObjectValue) {
|
||||
// Value is an object or array.
|
||||
convertJsObjectToVariable(arrayOrObjectValue, variable);
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType:
|
||||
variable.getType() === gd.Variable.Array ? 'Array' : 'Structure',
|
||||
addedNewVariable,
|
||||
};
|
||||
} else {
|
||||
// Value is a primitive, or not a valid Object/Array in JSON:
|
||||
|
||||
const variableType = readOrInferVariableType(forcedVariableType, value);
|
||||
|
||||
if (variableType === 'String') {
|
||||
variable.setString(value);
|
||||
} else if (variableType === 'Number') {
|
||||
variable.setValue(parseFloat(value));
|
||||
} else if (variableType === 'Boolean') {
|
||||
variable.setBool(value.toLowerCase() === 'true');
|
||||
}
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType,
|
||||
addedNewVariable,
|
||||
};
|
||||
if (variableType === 'String') {
|
||||
variable.setString(value);
|
||||
} else if (variableType === 'Number') {
|
||||
variable.setValue(parseFloat(value));
|
||||
} else if (variableType === 'Boolean') {
|
||||
variable.setBool(value.toLowerCase() === 'true');
|
||||
}
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType,
|
||||
addedNewVariable,
|
||||
};
|
||||
};
|
||||
|
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { serializeToJSObject } from '../Utils/Serializer';
|
||||
import { applyVariableChange } from './ApplyVariableChange';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -396,142 +395,5 @@ describe('applyVariableChange', () => {
|
||||
expect(variable.getType()).toBe(gd.Variable.String);
|
||||
expect(variable.getString()).toBe('123abc');
|
||||
});
|
||||
|
||||
it('should recognize JSON and parse it (object => structure)', () => {
|
||||
const result = applyVariableChange({
|
||||
variablePath: 'testString',
|
||||
forcedVariableType: null,
|
||||
variablesContainer,
|
||||
value:
|
||||
'{"a": 1, "b": "2", "c": [3, 4], "d": {"e": 5, "f": null, "g": true, "h": false}}',
|
||||
});
|
||||
expect(result.variableType).toBe('Structure');
|
||||
|
||||
const variable = variablesContainer.get('testString');
|
||||
expect(variable.getType()).toBe(gd.Variable.Structure);
|
||||
expect(serializeToJSObject(variable)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "a",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"name": "b",
|
||||
"type": "string",
|
||||
"value": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 4,
|
||||
},
|
||||
],
|
||||
"name": "c",
|
||||
"type": "array",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "e",
|
||||
"type": "number",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"name": "f",
|
||||
"type": "string",
|
||||
"value": "null",
|
||||
},
|
||||
Object {
|
||||
"name": "g",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"name": "h",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"name": "d",
|
||||
"type": "structure",
|
||||
},
|
||||
],
|
||||
"type": "structure",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should recognize JSON and parse it (object => structure)', () => {
|
||||
const result = applyVariableChange({
|
||||
variablePath: 'testString',
|
||||
forcedVariableType: null,
|
||||
variablesContainer,
|
||||
value: '[1, "2", [3, 4], {"e": 5, "f": null, "g": true, "h": false}]',
|
||||
});
|
||||
expect(result.variableType).toBe('Array');
|
||||
|
||||
const variable = variablesContainer.get('testString');
|
||||
expect(variable.getType()).toBe(gd.Variable.Array);
|
||||
expect(serializeToJSObject(variable)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"type": "string",
|
||||
"value": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 4,
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "e",
|
||||
"type": "number",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"name": "f",
|
||||
"type": "string",
|
||||
"value": "null",
|
||||
},
|
||||
Object {
|
||||
"name": "g",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"name": "h",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"type": "structure",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,9 +2,7 @@
|
||||
import { type EventsGenerationResult } from '.';
|
||||
import {
|
||||
editorFunctions,
|
||||
editorFunctionsWithoutProject,
|
||||
type EditorFunction,
|
||||
type EditorFunctionWithoutProject,
|
||||
type EditorCallbacks,
|
||||
type EditorFunctionCall,
|
||||
type EditorFunctionGenericOutput,
|
||||
@@ -32,7 +30,7 @@ export type EditorFunctionCallResult =
|
||||
|};
|
||||
|
||||
export type ProcessEditorFunctionCallsOptions = {|
|
||||
project: gdProject | null,
|
||||
project: gdProject,
|
||||
functionCalls: Array<EditorFunctionCall>,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
ignore: boolean,
|
||||
@@ -81,17 +79,6 @@ export const processEditorFunctionCalls = async ({
|
||||
}
|
||||
|
||||
const name = functionCall.name;
|
||||
if (!project && name !== 'initialize_project') {
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
success: false,
|
||||
output: {
|
||||
message: 'No project opened.',
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let args;
|
||||
try {
|
||||
try {
|
||||
@@ -135,9 +122,7 @@ export const processEditorFunctionCalls = async ({
|
||||
// Check if the function exists
|
||||
const editorFunction: EditorFunction | null =
|
||||
editorFunctions[name] || null;
|
||||
const editorFunctionWithoutProject: EditorFunctionWithoutProject | null =
|
||||
editorFunctionsWithoutProject[name] || null;
|
||||
if (!editorFunction && !editorFunctionWithoutProject) {
|
||||
if (!editorFunction) {
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
@@ -149,42 +134,19 @@ export const processEditorFunctionCalls = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const argumentsWithoutProject = {
|
||||
args,
|
||||
editorCallbacks,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
};
|
||||
|
||||
// Execute the function
|
||||
let result: EditorFunctionGenericOutput;
|
||||
if (editorFunction) {
|
||||
if (project) {
|
||||
result = await editorFunction.launchFunction({
|
||||
...argumentsWithoutProject,
|
||||
project,
|
||||
});
|
||||
} else {
|
||||
result = ({
|
||||
success: false,
|
||||
message: `Function ${name} requires a project to be opened before being used.`,
|
||||
}: EditorFunctionGenericOutput);
|
||||
const result: EditorFunctionGenericOutput = await editorFunction.launchFunction(
|
||||
{
|
||||
project,
|
||||
args,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
}
|
||||
} else if (editorFunctionWithoutProject) {
|
||||
result = await editorFunctionWithoutProject.launchFunction(
|
||||
argumentsWithoutProject
|
||||
);
|
||||
} else {
|
||||
result = ({
|
||||
success: false,
|
||||
message: `Unknown function with name: ${name}. Please use something else as this seems not supported or existing.`,
|
||||
}: EditorFunctionGenericOutput);
|
||||
}
|
||||
|
||||
const { success, meta, ...output } = result;
|
||||
);
|
||||
const { success, ...output } = result;
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
@@ -192,8 +154,10 @@ export const processEditorFunctionCalls = async ({
|
||||
output,
|
||||
});
|
||||
|
||||
if (meta && meta.newSceneNames) {
|
||||
createdSceneNames.push(...meta.newSceneNames);
|
||||
if (success && args) {
|
||||
if (name === 'create_scene' && typeof args.scene_name === 'string') {
|
||||
createdSceneNames.push(args.scene_name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
|
@@ -104,7 +104,8 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
const isCollection = isCollectionVariable(variable);
|
||||
|
||||
if (isCollection) {
|
||||
if (variable.getType() === gd.Variable.Structure) {
|
||||
// Don't diplay children of arrays, and only display the first level of children of structures.
|
||||
if (variable.getType() === gd.Variable.Structure && depth === 0) {
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
@@ -116,20 +117,11 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
return getSimplifiedVariable(childName, childVariable, depth + 1);
|
||||
}),
|
||||
};
|
||||
} else if (variable.getType() === gd.Variable.Array) {
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
variableChildren: mapFor(0, variable.getChildrenCount(), index => {
|
||||
const childVariable = variable.getAtIndex(index);
|
||||
return getSimplifiedVariable(
|
||||
index.toString(),
|
||||
childVariable,
|
||||
depth + 1
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -142,7 +134,7 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
const getSimplifiedVariablesContainerJson = (
|
||||
container: gdVariablesContainer
|
||||
): Array<SimplifiedVariable> => {
|
||||
return mapFor(0, container.count(), (index: number) => {
|
||||
return mapFor(0, Math.min(container.count(), 20), (index: number) => {
|
||||
const name = container.getNameAt(index);
|
||||
const variable = container.getAt(index);
|
||||
return getSimplifiedVariable(name, variable);
|
||||
|
@@ -135,13 +135,6 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "856.5",
|
||||
"variableName": "0",
|
||||
},
|
||||
],
|
||||
"variableName": "ObjectChild4",
|
||||
},
|
||||
],
|
||||
@@ -467,13 +460,6 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "856.5",
|
||||
"variableName": "0",
|
||||
},
|
||||
],
|
||||
"variableName": "ObjectChild4",
|
||||
},
|
||||
],
|
||||
@@ -826,15 +812,6 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Structure",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "Hello
|
||||
Multiline
|
||||
World",
|
||||
"variableName": "SubChild1",
|
||||
},
|
||||
],
|
||||
"variableName": "FoldedChild",
|
||||
},
|
||||
],
|
||||
@@ -842,45 +819,10 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "String value
|
||||
with Multiline",
|
||||
"variableName": "0",
|
||||
},
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "4539.42",
|
||||
"variableName": "1",
|
||||
},
|
||||
Object {
|
||||
"type": "Boolean",
|
||||
"value": "True",
|
||||
"variableName": "2",
|
||||
},
|
||||
],
|
||||
"variableName": "FoldedArray",
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "PlayerName",
|
||||
"variableName": "0",
|
||||
},
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "25",
|
||||
"variableName": "1",
|
||||
},
|
||||
Object {
|
||||
"type": "Boolean",
|
||||
"value": "False",
|
||||
"variableName": "2",
|
||||
},
|
||||
],
|
||||
"variableName": "OtherArray",
|
||||
},
|
||||
],
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import BehaviorTypeSelector from '../BehaviorTypeSelector';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
@@ -124,13 +124,9 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
};
|
||||
|
||||
export default function EventsBasedBehaviorPropertiesEditor({
|
||||
@@ -401,18 +397,17 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
[properties, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoices = React.useCallback(
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -959,12 +954,12 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getChoicesArray(property).map(
|
||||
{getExtraInfoArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice.value}
|
||||
label={choice.value}
|
||||
value={choice}
|
||||
label={choice}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -972,9 +967,11 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
@@ -119,13 +119,9 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
};
|
||||
|
||||
export default function EventsBasedObjectPropertiesEditor({
|
||||
@@ -408,18 +404,17 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
[eventsBasedObject, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoices = React.useCallback(
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -882,12 +877,12 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getChoicesArray(property).map(
|
||||
{getExtraInfoArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice.value}
|
||||
label={choice.label}
|
||||
value={choice}
|
||||
label={choice}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -911,9 +906,11 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -113,12 +113,11 @@ export default function EventsBasedObjectEditor({
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
label={<Trans>Has animations</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
@@ -130,11 +129,12 @@ export default function EventsBasedObjectEditor({
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Has animations (JavaScript only)</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
{isDev && (
|
||||
|
@@ -553,20 +553,12 @@ export const EventsFunctionPropertiesEditor = ({
|
||||
}}
|
||||
/>
|
||||
{eventsFunction.isAsync() && (
|
||||
<AlertMessage
|
||||
kind="info"
|
||||
renderRightButton={() => (
|
||||
<HelpButton
|
||||
helpPagePath={'/functions/asynchronous-functions'}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
This is an asynchronous action, meaning that the actions
|
||||
and sub-events following it will wait for it to end. You
|
||||
should use other async actions like "wait" to schedule
|
||||
your actions and don't forget to use the action "End
|
||||
asynchronous function" to mark the end of the action.
|
||||
and sub-events following it will wait for it to end.
|
||||
Don't forget to use the action "End asynchronous
|
||||
function" to mark the end of the action.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
|
@@ -9,7 +9,7 @@ import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import ObjectTypeSelector from '../../ObjectTypeSelector';
|
||||
import BehaviorTypeSelector from '../../BehaviorTypeSelector';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import ChoicesEditor, { type Choice } from '../../ChoicesEditor';
|
||||
import StringArrayEditor from '../../StringArrayEditor';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import ResourceTypeSelectField from './ResourceTypeSelectField';
|
||||
|
||||
@@ -295,18 +295,11 @@ export default function ValueTypeEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{valueTypeMetadata.getName() === 'stringWithSelector' && (
|
||||
<ChoicesEditor
|
||||
<StringArrayEditor
|
||||
disabled={disabled}
|
||||
choices={getExtraInfoArray(valueTypeMetadata).map(value => ({
|
||||
value,
|
||||
label: '',
|
||||
}))}
|
||||
hideLabels
|
||||
setChoices={(choices: Array<Choice>) => {
|
||||
// TODO Handle labels (and search "choice label")
|
||||
valueTypeMetadata.setExtraInfo(
|
||||
JSON.stringify(choices.map(choice => choice.value))
|
||||
);
|
||||
extraInfo={getExtraInfoArray(valueTypeMetadata)}
|
||||
setExtraInfo={(newExtraInfo: Array<string>) => {
|
||||
valueTypeMetadata.setExtraInfo(JSON.stringify(newExtraInfo));
|
||||
forceUpdate();
|
||||
onTypeUpdated();
|
||||
}}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TextField, { type TextFieldInterface } from '../../../UI/TextField';
|
||||
import { rgbToHex } from '../../../Utils/ColorTransformer';
|
||||
import {
|
||||
largeSelectedArea,
|
||||
@@ -19,15 +22,6 @@ const gd: libGDevelop = global.gd;
|
||||
|
||||
const commentTextStyle = {
|
||||
width: '100%',
|
||||
fontSize: 'inherit',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
|
||||
// Big enough to have an empty text be the same size as an empty textarea.
|
||||
lineHeight: '1.5em',
|
||||
};
|
||||
|
||||
const styles = {
|
||||
@@ -36,9 +30,9 @@ const styles = {
|
||||
flexWrap: 'wrap',
|
||||
padding: 5,
|
||||
overflow: 'hidden',
|
||||
minHeight: '2.4em',
|
||||
minHeight: '2.1em',
|
||||
},
|
||||
commentTextField: { ...commentTextStyle, minHeight: '0', resize: 'none' },
|
||||
commentTextField: { ...commentTextStyle, fontSize: 'inherit' },
|
||||
commentSpan: {
|
||||
...commentTextStyle,
|
||||
alignItems: 'center',
|
||||
@@ -61,7 +55,8 @@ export default class CommentEvent extends React.Component<
|
||||
editingPreviousValue: null,
|
||||
};
|
||||
|
||||
_textField: ?HTMLTextAreaElement;
|
||||
_selectable: ?HTMLSpanElement;
|
||||
_textField: ?TextFieldInterface;
|
||||
|
||||
edit = () => {
|
||||
if (this.state.editing) return;
|
||||
@@ -72,11 +67,8 @@ export default class CommentEvent extends React.Component<
|
||||
editingPreviousValue: commentEvent.getComment(),
|
||||
},
|
||||
() => {
|
||||
const textField = this._textField;
|
||||
if (textField) {
|
||||
textField.focus();
|
||||
textField.selectionStart = textField.value.length;
|
||||
textField.selectionEnd = textField.value.length;
|
||||
if (this._textField) {
|
||||
this._textField.focus({ caretPosition: 'end' });
|
||||
}
|
||||
// Wait for the change to be applied on the DOM before calling onUpdate,
|
||||
// so that the height of the event is updated.
|
||||
@@ -85,12 +77,15 @@ export default class CommentEvent extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
onChange = (e: any) => {
|
||||
onChange = (e: any, text: string) => {
|
||||
const commentEvent = gd.asCommentEvent(this.props.event);
|
||||
commentEvent.setComment(e.target.value);
|
||||
commentEvent.setComment(text);
|
||||
|
||||
this._autoResizeTextArea();
|
||||
this.forceUpdate();
|
||||
this.forceUpdate(() => {
|
||||
// Wait for the change to be applied on the DOM before calling onUpdate,
|
||||
// so that the height of the event is updated.
|
||||
this.props.onUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
endEditing = () => {
|
||||
@@ -119,22 +114,6 @@ export default class CommentEvent extends React.Component<
|
||||
.replace(/\n/g, '<br>');
|
||||
};
|
||||
|
||||
_autoResizeTextArea = () => {
|
||||
if (this._textField) {
|
||||
const previousHeight = this._textField.style.height;
|
||||
this._textField.style.height = 'auto';
|
||||
this._textField.style.height = this._textField.scrollHeight + 'px';
|
||||
|
||||
if (previousHeight !== this._textField.style.height) {
|
||||
this.props.onUpdate(); // Notify the parent that the height has changed.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
this._autoResizeTextArea();
|
||||
}
|
||||
|
||||
render() {
|
||||
const commentEvent = gd.asCommentEvent(this.props.event);
|
||||
|
||||
@@ -170,27 +149,31 @@ export default class CommentEvent extends React.Component<
|
||||
id={`${this.props.idPrefix}-comment`}
|
||||
>
|
||||
{this.state.editing ? (
|
||||
<textarea
|
||||
<TextField
|
||||
multiline
|
||||
margin="none"
|
||||
ref={textField => (this._textField = textField)}
|
||||
value={commentEvent.getComment()}
|
||||
placeholder="..."
|
||||
translatableHintText={t`<Enter comment>`}
|
||||
onBlur={this.endEditing}
|
||||
onChange={this.onChange}
|
||||
style={{ ...styles.commentTextField, color: textColor }}
|
||||
style={styles.commentTextField}
|
||||
inputStyle={{
|
||||
color: textColor,
|
||||
padding: 0,
|
||||
}}
|
||||
fullWidth
|
||||
id="comment-title"
|
||||
onKeyDown={event => {
|
||||
if (shouldCloseOrCancel(event) || shouldSubmit(event)) {
|
||||
this.endEditing();
|
||||
}
|
||||
}}
|
||||
rows={
|
||||
/* Ensure the textarea resize down to 1 line when no text or just a single line is entered. */
|
||||
1
|
||||
}
|
||||
spellCheck="false"
|
||||
underlineShow={false}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
ref={selectable => (this._selectable = selectable)}
|
||||
className={classNames({
|
||||
[selectableArea]: true,
|
||||
[disabledText]: this.props.disabled,
|
||||
|
@@ -428,7 +428,6 @@ const InstructionEditorDialog = ({
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={chosenObject.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
|
||||
isChildObject={!scope.layout}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
onExtensionInstalled={extensionName => {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import Window from '../Utils/Window';
|
||||
import { getIDEVersion } from '../Version';
|
||||
|
||||
type FileSet =
|
||||
@@ -49,9 +48,10 @@ export const findGDJS = (
|
||||
// run `newIDE/web-app/scripts/deploy-GDJS-Runtime` script.
|
||||
let gdjsRoot = `https://resources.gdevelop-app.com/GDJS-${getIDEVersion()}`;
|
||||
|
||||
if (Window.isDev()) {
|
||||
gdjsRoot = `http://localhost:5002`;
|
||||
}
|
||||
// If you want to test your local changes to the game engine on the local web-app,
|
||||
// run `npx serve --cors -p 5001` (or another CORS enabled http server on port 5001)
|
||||
// in `newIDE/app/resources/GDJS` and uncomment this line:
|
||||
// gdjsRoot = `http://localhost:5001`;
|
||||
|
||||
return Promise.all(
|
||||
filesToDownload[fileSet].map(relativeFilePath => {
|
||||
|
@@ -113,7 +113,7 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
|
||||
if (type === 'TextObject::Text::SetFontSize') {
|
||||
return {
|
||||
kind: 'warning',
|
||||
message: t`This action will create a new texture and re-render the text each time it is called, which is expensive and can reduce performance. Avoid changing the character size of text frequently.`,
|
||||
message: t`This action will create a new texture and re-render the text each time it is called, which is expensive and can reduce performances. Prefer to avoid changing a lot the character size of a text.`,
|
||||
};
|
||||
}
|
||||
if (type === 'PlayMusicCanal' || type === 'PlayMusic') {
|
||||
|
@@ -17,7 +17,6 @@ import { type CourseChapter } from '../../Utils/GDevelopServices/Asset';
|
||||
import { type GamesList } from '../../GameDashboard/UseGamesList';
|
||||
import { type GamesPlatformFrameTools } from './HomePage/PlaySection/UseGamesPlatformFrame';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { type CreateProjectResult } from '../../Utils/UseCreateProject';
|
||||
|
||||
export type EditorContainerExtraProps = {|
|
||||
// Events function extension editor
|
||||
@@ -160,10 +159,8 @@ export type RenderEditorContainerProps = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<CreateProjectResult>,
|
||||
onCreateEmptyProject: (
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onOpenTemplateFromTutorial: (tutorialId: string) => Promise<void>,
|
||||
onOpenTemplateFromCourseChapter: (
|
||||
CourseChapter,
|
||||
|
@@ -50,7 +50,6 @@ import {
|
||||
import { useProjectsListFor } from './utils';
|
||||
import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project';
|
||||
import { getDefaultRegisterGameProperties } from '../../../../Utils/UseGameAndBuildsManager';
|
||||
import { type CreateProjectResult } from '../../../../Utils/UseCreateProject';
|
||||
|
||||
const getExampleItemsColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
@@ -84,7 +83,7 @@ type Props = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
onSelectPrivateGameTemplateListingData: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
|
@@ -0,0 +1,303 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import SectionContainer, { SectionRow } from '../SectionContainer';
|
||||
import { Column, Line } from '../../../../UI/Grid';
|
||||
import BundlePageHeader from './BundlePageHeader';
|
||||
import { BundleStoreContext } from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import type { CourseCompletion } from '../UseCourses';
|
||||
import {
|
||||
getBundle,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../../../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../../../Utils/GDevelopServices/Errors';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import AlertMessage from '../../../../UI/AlertMessage';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
} from '../../../../AssetStore/ProductPageHelper';
|
||||
import { PrivateGameTemplateStoreContext } from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../../../../AssetStore/AssetStoreContext';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { GridList, GridListTile } from '@material-ui/core';
|
||||
import { LARGE_WIDGET_SIZE } from '../CardWidget';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import Text from '../../../../UI/Text';
|
||||
import CourseStoreContext from '../../../../Course/CourseStoreContext';
|
||||
import CourseCard from './CourseCard';
|
||||
|
||||
const getColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
switch (windowSize) {
|
||||
case 'small':
|
||||
return isLandscape ? 4 : 2;
|
||||
case 'medium':
|
||||
return 3;
|
||||
case 'large':
|
||||
return 4;
|
||||
case 'xlarge':
|
||||
return 6;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
|
||||
const cellSpacing = 10;
|
||||
const MAX_COLUMNS = getColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
onBack: () => void,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
onBundleOpen: BundleListingData => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onCourseOpen: CourseListingData => void,
|
||||
courses: ?Array<Course>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
|};
|
||||
|
||||
const BundlePage = ({
|
||||
bundleListingData,
|
||||
onBack,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
courses,
|
||||
receivedCourses,
|
||||
getCourseCompletion,
|
||||
}: Props) => {
|
||||
const { windowSize, isLandscape } = useResponsiveWindowSize();
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext); // If archived, should use the one passed.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const {
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [bundle, setBundle] = React.useState<?Bundle>(null);
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
|
||||
const courseAndTheirListingDataIncludedInBundle = React.useMemo(
|
||||
(): Array<{|
|
||||
course: Course,
|
||||
courseListingData: CourseListingData,
|
||||
|}> | null => {
|
||||
if (!bundle || !bundleListingData || !courses) return null;
|
||||
const productListingDatasInBundle = getProductsIncludedInBundle({
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: [...(listedCourses || [])],
|
||||
});
|
||||
|
||||
if (!productListingDatasInBundle) return null;
|
||||
// $FlowIgnore - Flow doesn't understand that we have filtered the products to only include courses.
|
||||
const courseListingDatasInBundle: CourseListingData[] = productListingDatasInBundle.filter(
|
||||
productListingData => productListingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
return (courseListingDatasInBundle || [])
|
||||
.map(courseListingData => {
|
||||
const course = courses.find(
|
||||
course => course.id === courseListingData.id
|
||||
);
|
||||
if (!course) return null;
|
||||
return {
|
||||
course,
|
||||
courseListingData,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
[bundle, bundleListingData, listedCourses, courses]
|
||||
);
|
||||
|
||||
const productsExceptCoursesIncludedInBundleTiles = React.useMemo(
|
||||
() =>
|
||||
bundle && bundleListingData
|
||||
? getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundle,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
bundleListingData,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
try {
|
||||
const bundle = await getBundle(bundleListingData.id);
|
||||
|
||||
setBundle(bundle);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
);
|
||||
if (extractedStatusAndCode && extractedStatusAndCode.status === 404) {
|
||||
setErrorText(
|
||||
<Trans>
|
||||
Bundle not found - An error occurred, please try again later.
|
||||
</Trans>
|
||||
);
|
||||
} else {
|
||||
setErrorText(
|
||||
<Trans>An error occurred, please try again later.</Trans>
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
},
|
||||
[bundleListingData.id]
|
||||
);
|
||||
|
||||
if (errorText) {
|
||||
return (
|
||||
<SectionContainer flexBody backAction={onBack}>
|
||||
<SectionRow expand>
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!bundleListingData || !bundle) {
|
||||
return (
|
||||
<SectionContainer flexBody>
|
||||
<SectionRow expand>
|
||||
<PlaceholderLoader />
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<SectionContainer
|
||||
applyTopSpacingAsMarginOnChildrenContainer
|
||||
backAction={onBack}
|
||||
>
|
||||
<Column noOverflowParent noMargin>
|
||||
<BundlePageHeader
|
||||
bundleListingData={bundleListingData}
|
||||
bundle={bundle}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
{courseAndTheirListingDataIncludedInBundle &&
|
||||
courseAndTheirListingDataIncludedInBundle.length > 0 && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{courseAndTheirListingDataIncludedInBundle.map(
|
||||
({ course, courseListingData }) => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onCourseOpen(courseListingData);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</GridList>
|
||||
</Line>
|
||||
)}
|
||||
{productsExceptCoursesIncludedInBundleTiles && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Also included in this bundle</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsExceptCoursesIncludedInBundleTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePage;
|
@@ -0,0 +1,388 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type Bundle } from '../../../../Utils/GDevelopServices/Asset';
|
||||
import { type BundleListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { SectionRow } from '../SectionContainer';
|
||||
import Paper from '../../../../UI/Paper';
|
||||
import Text from '../../../../UI/Text';
|
||||
import { Column, Line } from '../../../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../../UI/Layout';
|
||||
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
|
||||
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
import { renderProductPrice } from '../../../../AssetStore/ProductPriceTag';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../../../../AssetStore/ProductPageHelper';
|
||||
import { shouldUseAppStoreProduct } from '../../../../Utils/AppStorePurchases';
|
||||
import { Divider } from '@material-ui/core';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { BundleStoreContext } from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import { sendBundleBuyClicked } from '../../../../Utils/Analytics/EventSender';
|
||||
import BundlePurchaseDialog from '../../../../AssetStore/Bundles/BundlePurchaseDialog';
|
||||
import RedemptionCodesDialog from '../../../../RedemptionCode/RedemptionCodesDialog';
|
||||
import { renderEstimatedTotalPriceFormatted } from '../../../../AssetStore/Bundles/Utils';
|
||||
import { PrivateGameTemplateStoreContext } from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import {
|
||||
CreditsPackageStoreContext,
|
||||
getCreditsAmountFromId,
|
||||
} from '../../../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import { AssetStoreContext } from '../../../../AssetStore/AssetStoreContext';
|
||||
import CourseStoreContext from '../../../../Course/CourseStoreContext';
|
||||
import SecureCheckout from '../../../../AssetStore/SecureCheckout/SecureCheckout';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../../../Profile/Subscription/PlanCard';
|
||||
import FlatButton from '../../../../UI/FlatButton';
|
||||
import Coin from '../../../../Credits/Icons/Coin';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { formatDurationOfRedemptionCode } from '../../../../RedemptionCode/Utils';
|
||||
|
||||
const styles = {
|
||||
title: { overflowWrap: 'anywhere', textWrap: 'wrap' },
|
||||
image: { width: 300, aspectRatio: '16 / 9' },
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
const ResponsiveDivider = () => {
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
return isMobile || isMediumScreen ? (
|
||||
<Column noMargin>
|
||||
<Divider orientation="horizontal" />
|
||||
</Column>
|
||||
) : (
|
||||
<Line noMargin>
|
||||
<Divider orientation="vertical" />
|
||||
</Line>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
bundle: Bundle,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const BundlePageHeader = ({
|
||||
bundle,
|
||||
bundleListingData,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { creditsPackageListingDatas } = React.useContext(
|
||||
CreditsPackageStoreContext
|
||||
);
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const { receivedBundles, bundlePurchases } = authenticatedUser;
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
const [
|
||||
isRedemptionCodesDialogOpen,
|
||||
setIsRedemptionCodesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: bundleListingData ? bundleListingData.id : null,
|
||||
receivedProducts: receivedBundles,
|
||||
productPurchases: bundlePurchases,
|
||||
allProductListingDatas: bundleListingDatas,
|
||||
}),
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
|
||||
const productListingDatasIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData &&
|
||||
bundleListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
privateAssetPackListingDatas &&
|
||||
listedCourses &&
|
||||
creditsPackageListingDatas
|
||||
? getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...bundleListingDatas,
|
||||
...privateGameTemplateListingDatas,
|
||||
...privateAssetPackListingDatas,
|
||||
...listedCourses,
|
||||
...creditsPackageListingDatas,
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
creditsPackageListingDatas,
|
||||
]
|
||||
);
|
||||
|
||||
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
|
||||
|
||||
const redemptionCodesIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? bundleListingData.includedRedemptionCodes || []
|
||||
: null,
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const includedCreditsAmount = React.useMemo(
|
||||
() =>
|
||||
(bundleListingData.includedListableProducts || [])
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
),
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle) return;
|
||||
if (isAlreadyReceived) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const price = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
|
||||
sendBundleBuyClicked({
|
||||
bundleId: bundle.id,
|
||||
bundleName: bundle.name,
|
||||
bundleTag: bundle.tag,
|
||||
currency: price ? price.currency : undefined,
|
||||
usageType: 'default',
|
||||
});
|
||||
|
||||
setPurchasingBundleListingData(bundleListingData);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[bundle, bundleListingData, isAlreadyReceived]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<>
|
||||
<SectionRow>
|
||||
<Paper background="dark" variant="outlined" style={{ padding: 16 }}>
|
||||
<ColumnStackLayout noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
expand
|
||||
>
|
||||
<div style={styles.imageContainer}>
|
||||
<img
|
||||
src={bundle.previewImageUrls[0]}
|
||||
style={styles.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ColumnStackLayout expand justifyContent="flex-start">
|
||||
<Text size="title" noMargin style={styles.title}>
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<Text noMargin>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout
|
||||
expand
|
||||
justifyContent="space-between"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{redemptionCodesIncludedInBundle &&
|
||||
redemptionCodesIncludedInBundle.length > 0 && (
|
||||
<ColumnStackLayout noMargin expand>
|
||||
{redemptionCodesIncludedInBundle.map(
|
||||
(includedRedemptionCode, index) => (
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
key={`${
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
}-${index}`}
|
||||
>
|
||||
{getPlanIcon({
|
||||
planId:
|
||||
includedRedemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Text>
|
||||
<Trans>
|
||||
{formatDurationOfRedemptionCode(
|
||||
includedRedemptionCode.durationInDays
|
||||
)}{' '}
|
||||
of
|
||||
{getPlanInferredNameFromId(
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)
|
||||
)}
|
||||
{isAlreadyReceived && (
|
||||
<Line noMargin>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>See my codes</Trans>}
|
||||
onClick={() =>
|
||||
setIsRedemptionCodesDialogOpen(true)
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
{includedCreditsAmount > 0 && (
|
||||
<Column justifyContent="center" expand noMargin>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text>
|
||||
<Trans>
|
||||
{includedCreditsAmount} credits included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
)}
|
||||
<ResponsiveDivider />
|
||||
</ResponsiveLineStackLayout>
|
||||
{!isAlreadyReceived && (
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{!!bundleListingData && (
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noMargin
|
||||
>
|
||||
{!isMobile && !isMediumScreen && (
|
||||
<Column noMargin justifyContent="center">
|
||||
<LineStackLayout noMargin>
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
subscriptionPlansWithPricingSystems,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
color: gdevelopTheme.text.color.secondary,
|
||||
}}
|
||||
>
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
})}
|
||||
</div>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
)}
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePageHeader;
|
@@ -131,7 +131,6 @@ type Props = {|
|
||||
course: ?Course,
|
||||
courseListingData: ?CourseListingData,
|
||||
onClick?: () => void,
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
const CourseCard = ({
|
||||
@@ -139,7 +138,6 @@ const CourseCard = ({
|
||||
course,
|
||||
courseListingData,
|
||||
onClick,
|
||||
discountedPrice,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const specializationConfig = getSpecializationConfig(
|
||||
@@ -257,7 +255,6 @@ const CourseCard = ({
|
||||
usageType: 'default',
|
||||
showBothPrices: 'column',
|
||||
owned: !course.isLocked,
|
||||
discountedPrice,
|
||||
})}
|
||||
</Line>
|
||||
</div>
|
||||
|
@@ -39,7 +39,6 @@ import CoursePageHeader from './CoursePageHeader';
|
||||
import Window from '../../../../Utils/Window';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { RatingBanner } from './RatingBanner';
|
||||
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
|
||||
const styles = {
|
||||
desktopContainer: { display: 'flex', gap: 16 },
|
||||
@@ -354,20 +353,8 @@ const CoursePage = ({
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
)}
|
||||
{course.introByLocale && (
|
||||
<Line>
|
||||
<AlertMessage kind="info" background="light">
|
||||
{selectMessageByLocale(i18n, course.introByLocale)}
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
)}
|
||||
{courseChapters.map((chapter: CourseChapter, index) => (
|
||||
<ColumnStackLayout
|
||||
expand
|
||||
noOverflowParent
|
||||
noMargin
|
||||
key={chapter.id}
|
||||
>
|
||||
<ColumnStackLayout expand noOverflowParent noMargin>
|
||||
{chapter.videoUrl ? (
|
||||
<VideoBasedCourseChapterView
|
||||
chapterIndex={index}
|
||||
|
@@ -276,6 +276,9 @@ const CoursePageHeader = ({
|
||||
noMargin
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={courseListingData}
|
||||
@@ -292,9 +295,6 @@ const CoursePageHeader = ({
|
||||
onWillBuyWithCredits(i18n)
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
|
@@ -7,10 +7,7 @@ import {
|
||||
type Course,
|
||||
type CourseChapter,
|
||||
} from '../../../../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
} from '../../../../Utils/GDevelopServices/Shop';
|
||||
import type { BundleListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import CoursePreviewBanner from '../../../../Course/CoursePreviewBanner';
|
||||
import type { CourseCompletion, CourseChapterCompletion } from '../UseCourses';
|
||||
import { Line } from '../../../../UI/Grid';
|
||||
@@ -39,7 +36,7 @@ const styles = {
|
||||
type Props = {|
|
||||
onBack: () => void,
|
||||
courses: ?Array<Course>,
|
||||
onSelectCourse: (courseListingData: CourseListingData) => void,
|
||||
onSelectCourse: (courseId: string) => void,
|
||||
onSelectBundle: (bundleListingData: BundleListingData) => void,
|
||||
previewedCourse: ?Course,
|
||||
getCourseChapters: (courseId: string) => ?Array<CourseChapter>,
|
||||
@@ -86,12 +83,8 @@ const CoursesPage = ({
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
getCourseChapterCompletion={getCourseChapterCompletion}
|
||||
onDisplayCourse={() => {
|
||||
if (!previewedCourse || !listedCourses) return;
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === previewedCourse.id
|
||||
);
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
if (!previewedCourse) return;
|
||||
onSelectCourse(previewedCourse.id);
|
||||
}}
|
||||
/>
|
||||
</SectionRow>
|
||||
@@ -116,8 +109,7 @@ const CoursesPage = ({
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
@@ -137,11 +129,7 @@ const CoursesPage = ({
|
||||
</SectionRow>
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="starter"
|
||||
i18n={i18n}
|
||||
/>
|
||||
<BundlePreviewBanner onDisplayBundle={onSelectBundle} />
|
||||
</SectionRow>
|
||||
)}
|
||||
{courses && listedCourses && courses.length > numberOfItemsOnOneRow && (
|
||||
@@ -153,74 +141,28 @@ const CoursesPage = ({
|
||||
cellHeight="auto"
|
||||
spacing={ITEMS_SPACING * 2}
|
||||
>
|
||||
{courses
|
||||
.slice(numberOfItemsOnOneRow, 2 * numberOfItemsOnOneRow)
|
||||
.map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
{courses.slice(numberOfItemsOnOneRow).map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
</GridList>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="premium"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
{courses &&
|
||||
listedCourses &&
|
||||
courses.length > 2 * numberOfItemsOnOneRow && (
|
||||
<SectionRow>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={numberOfItemsOnOneRow}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={ITEMS_SPACING * 2}
|
||||
>
|
||||
{courses.slice(2 * numberOfItemsOnOneRow).map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
</GridList>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
)}
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -34,7 +34,6 @@ import ExampleStore from '../../../../AssetStore/ExampleStore';
|
||||
import {
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
} from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
|
||||
import Carousel from '../../../../UI/Carousel';
|
||||
@@ -63,7 +62,7 @@ type Props = {|
|
||||
onSelectCategory: (category: LearnCategory) => void,
|
||||
selectInAppTutorial: (tutorialId: string) => void,
|
||||
courses: ?(Course[]),
|
||||
onSelectCourse: (courseListingData: CourseListingData) => void,
|
||||
onSelectCourse: (courseId: string) => void,
|
||||
onSelectBundle: (bundleListingData: BundleListingData) => void,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
getCourseChapterCompletion: (
|
||||
@@ -217,8 +216,7 @@ const MainPage = ({
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
@@ -242,11 +240,7 @@ const MainPage = ({
|
||||
</SectionRow>
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="starter"
|
||||
i18n={i18n}
|
||||
/>
|
||||
<BundlePreviewBanner onDisplayBundle={onSelectBundle} />
|
||||
</SectionRow>
|
||||
)}
|
||||
<SectionRow>
|
||||
|
@@ -21,8 +21,6 @@ import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
import Text from '../../../../UI/Text';
|
||||
import { TutorialContext } from '../../../../Tutorial/TutorialContext';
|
||||
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import CourseStoreContext from '../../../../Course/CourseStoreContext';
|
||||
import { type CourseListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
|
||||
const styles = {
|
||||
educationCurriculumTutorialContainer: {
|
||||
@@ -145,7 +143,7 @@ type Props = {|
|
||||
onBack: () => void,
|
||||
category: TutorialCategory,
|
||||
onOpenTemplateFromTutorial: string => Promise<void>,
|
||||
onSelectCourse: (courseListingData: CourseListingData) => void,
|
||||
onSelectCourse: (courseId: string) => void,
|
||||
|};
|
||||
|
||||
const TutorialsCategoryPage = ({
|
||||
@@ -154,7 +152,6 @@ const TutorialsCategoryPage = ({
|
||||
onOpenTemplateFromTutorial,
|
||||
onSelectCourse,
|
||||
}: Props) => {
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
const texts = TUTORIAL_CATEGORY_TEXTS[category];
|
||||
@@ -190,14 +187,7 @@ const TutorialsCategoryPage = ({
|
||||
<EducationCurriculum
|
||||
tutorials={filteredTutorials}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
onSelectCourse={courseId => {
|
||||
if (!listedCourses) return;
|
||||
const courseListingData = listedCourses.find(
|
||||
course => course.id === courseId
|
||||
);
|
||||
if (!courseListingData) return;
|
||||
onSelectCourse(courseListingData);
|
||||
}}
|
||||
onSelectCourse={onSelectCourse}
|
||||
i18n={i18n}
|
||||
limits={limits}
|
||||
onOpenTemplateFromTutorial={onOpenTemplateFromTutorial}
|
||||
|
@@ -26,13 +26,13 @@ import CoursesPage from './CoursesPage';
|
||||
import { type LearnCategory } from './Utils';
|
||||
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import BundlePage from './BundlePage';
|
||||
import RouterContext from '../../../RouterContext';
|
||||
import {
|
||||
sendBundleInformationOpened,
|
||||
sendCourseInformationOpened,
|
||||
} from '../../../../Utils/Analytics/EventSender';
|
||||
import { BundleStoreContext } from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import BundleInformationPage from '../../../../AssetStore/Bundles/BundleInformationPage';
|
||||
|
||||
type Props = {|
|
||||
selectInAppTutorial: (tutorialId: string) => void,
|
||||
@@ -119,15 +119,10 @@ const LearnSection = ({
|
||||
|
||||
const onOpenBundle = React.useCallback(
|
||||
(bundleListingData: BundleListingData) => {
|
||||
const priceForUsageType = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
sendBundleInformationOpened({
|
||||
bundleName: bundleListingData.name,
|
||||
bundleId: bundleListingData.id,
|
||||
source: 'learn',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
setSelectedBundleListingData(bundleListingData);
|
||||
},
|
||||
@@ -135,27 +130,18 @@ const LearnSection = ({
|
||||
);
|
||||
|
||||
const onOpenCourse = React.useCallback(
|
||||
(courseListingData: CourseListingData) => {
|
||||
const courseId = courseListingData.id;
|
||||
if (!courses) {
|
||||
return;
|
||||
(courseId: string | null) => {
|
||||
if (courseId && courses) {
|
||||
const course = courses.find(c => c.id === courseId);
|
||||
if (course && course.isLocked) {
|
||||
// Only send the event if the course is not owned.
|
||||
sendCourseInformationOpened({
|
||||
courseName: course.titleByLocale['en'],
|
||||
courseId: courseId,
|
||||
source: 'learn',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const course = courses.find(c => c.id === courseId);
|
||||
if (course && course.isLocked) {
|
||||
const priceForUsageType = courseListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
// Only send the event if the course is not owned.
|
||||
sendCourseInformationOpened({
|
||||
courseName: courseListingData.name,
|
||||
courseId: courseListingData.id,
|
||||
source: 'learn',
|
||||
priceValue: priceForUsageType && priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType && priceForUsageType.currency,
|
||||
});
|
||||
}
|
||||
|
||||
onSelectCourse(courseId);
|
||||
},
|
||||
[onSelectCourse, courses]
|
||||
@@ -209,7 +195,7 @@ const LearnSection = ({
|
||||
|
||||
if (selectedBundleListingData) {
|
||||
return (
|
||||
<BundleInformationPage
|
||||
<BundlePage
|
||||
bundleListingData={selectedBundleListingData}
|
||||
onBack={() => setSelectedBundleListingData(null)}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
@@ -224,7 +210,9 @@ const LearnSection = ({
|
||||
}}
|
||||
onGameTemplateOpen={onSelectPrivateGameTemplateListingData}
|
||||
onBundleOpen={onOpenBundle}
|
||||
onCourseOpen={onOpenCourse}
|
||||
onCourseOpen={courseListingData => {
|
||||
onOpenCourse(courseListingData.id);
|
||||
}}
|
||||
courses={courses}
|
||||
receivedCourses={receivedCourses}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
|
@@ -65,7 +65,7 @@ type Props = {|
|
||||
subtitleText?: React.Node,
|
||||
customPaperStyle?: Object,
|
||||
renderSubtitle?: () => React.Node,
|
||||
backAction?: () => void | Promise<void>,
|
||||
backAction?: () => void,
|
||||
flexBody?: boolean,
|
||||
renderFooter?: () => React.Node,
|
||||
noScroll?: boolean,
|
||||
|
@@ -11,11 +11,9 @@ import AssetPackInstallDialog from '../../../../AssetStore/AssetPackInstallDialo
|
||||
import { enumerateAssetStoreIds } from '../../../../AssetStore/EnumerateAssetStoreIds';
|
||||
import { type PrivateGameTemplateListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { type Course } from '../../../../Utils/GDevelopServices/Asset';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import ErrorBoundary from '../../../../UI/ErrorBoundary';
|
||||
import { getAssetShortHeadersToDisplay } from '../../../../AssetStore/AssetsList';
|
||||
import { AssetStoreNavigatorContext } from '../../../../AssetStore/AssetStoreNavigator';
|
||||
import { type CourseCompletion } from '../UseCourses';
|
||||
|
||||
type Props = {|
|
||||
project: ?gdProject,
|
||||
@@ -25,11 +23,8 @@ type Props = {|
|
||||
) => void,
|
||||
onOpenProfile: () => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
onCourseOpen: (courseId: string) => void,
|
||||
receivedCourses?: ?Array<Course>,
|
||||
courses?: ?Array<Course>,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
|};
|
||||
|
||||
const StoreSection = ({
|
||||
@@ -39,10 +34,7 @@ const StoreSection = ({
|
||||
onOpenProfile,
|
||||
onExtensionInstalled,
|
||||
onCourseOpen,
|
||||
courses,
|
||||
receivedCourses,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
getCourseCompletion,
|
||||
}: Props) => {
|
||||
const [
|
||||
isAssetPackDialogInstallOpen,
|
||||
@@ -97,13 +89,8 @@ const StoreSection = ({
|
||||
}
|
||||
displayPromotions
|
||||
onOpenProfile={onOpenProfile}
|
||||
courses={courses}
|
||||
receivedCourses={receivedCourses}
|
||||
onCourseOpen={onCourseOpen}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
/>
|
||||
{(openedAssetPack || openedAssetShortHeader) && (
|
||||
<Line justifyContent="flex-end">
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import FlatButton from '../../../../../UI/FlatButton';
|
||||
import Dialog, { DialogPrimaryButton } from '../../../../../UI/Dialog';
|
||||
import { ColumnStackLayout } from '../../../../../UI/Layout';
|
||||
|
@@ -405,9 +405,8 @@ const useCourses = () => {
|
||||
sendCourseBuyClicked({
|
||||
courseId: course.id,
|
||||
courseName: course.titleByLocale.en,
|
||||
currency: priceForUsageType ? priceForUsageType.currency : undefined,
|
||||
usageType: 'default',
|
||||
priceValue: priceForUsageType.value,
|
||||
priceCurrency: priceForUsageType.currency,
|
||||
});
|
||||
|
||||
setPurchasingCourseListingData(listedCourse);
|
||||
@@ -434,6 +433,13 @@ const useCourses = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
sendCourseBuyClicked({
|
||||
courseId: course.id,
|
||||
courseName: course.titleByLocale.en,
|
||||
currency: 'CREDITS',
|
||||
usageType: 'default',
|
||||
});
|
||||
|
||||
const currentCreditsAmount = limits.credits.userBalance.amount;
|
||||
const listedCourse = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
@@ -452,15 +458,6 @@ const useCourses = () => {
|
||||
return;
|
||||
}
|
||||
const creditsAmount = priceForUsageType.amount;
|
||||
|
||||
sendCourseBuyClicked({
|
||||
courseId: course.id,
|
||||
courseName: course.titleByLocale.en,
|
||||
usageType: 'default',
|
||||
priceValue: creditsAmount,
|
||||
priceCurrency: 'CREDITS',
|
||||
});
|
||||
|
||||
if (currentCreditsAmount < creditsAmount) {
|
||||
openCreditsPackageDialog({
|
||||
missingCredits: creditsAmount - currentCreditsAmount,
|
||||
|
@@ -44,7 +44,6 @@ import useCourses from './UseCourses';
|
||||
import PreferencesContext from '../../Preferences/PreferencesContext';
|
||||
import useSubscriptionPlans from '../../../Utils/UseSubscriptionPlans';
|
||||
import { BundleStoreContext } from '../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import { type CreateProjectResult } from '../../../Utils/UseCreateProject';
|
||||
|
||||
const getRequestedTab = (routeArguments: RouteArguments): HomeTab | null => {
|
||||
if (
|
||||
@@ -150,7 +149,7 @@ type Props = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
onOpenTemplateFromTutorial: (tutorialId: string) => Promise<void>,
|
||||
onOpenTemplateFromCourseChapter: (
|
||||
CourseChapter,
|
||||
@@ -641,11 +640,6 @@ export const HomePage = React.memo<Props>(
|
||||
? courses.filter(course => !course.isLocked)
|
||||
: undefined
|
||||
}
|
||||
courses={courses}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'team-view' &&
|
||||
|
@@ -52,7 +52,6 @@ import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import useOnResize from '../Utils/UseOnResize';
|
||||
import DrawerTopBar from '../UI/DrawerTopBar';
|
||||
import { type FloatingPaneState } from './PanesContainer';
|
||||
import { type CreateProjectResult } from '../Utils/UseCreateProject';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -165,15 +164,13 @@ export type EditorTabsPaneCommonProps = {|
|
||||
privateGameTemplateListingData: ?PrivateGameTemplateListingData,
|
||||
preventBackHome?: boolean,
|
||||
|}) => void,
|
||||
createEmptyProject: (
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
createEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
createProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
onOpenProfileDialog: () => void,
|
||||
openLanguageDialog: (open: boolean) => void,
|
||||
openPreferencesDialog: (open: boolean) => void,
|
||||
|
@@ -15,22 +15,21 @@ import NewProjectSetupDialog, {
|
||||
} from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { type StorageProvider } from '../ProjectsStorage';
|
||||
import RouterContext from './RouterContext';
|
||||
import { type CreateProjectResult } from '../Utils/UseCreateProject';
|
||||
|
||||
type Props = {|
|
||||
isProjectOpening: boolean,
|
||||
newProjectSetupDialogOpen: boolean,
|
||||
setNewProjectSetupDialogOpen: boolean => void,
|
||||
createEmptyProject: NewProjectSetup => Promise<CreateProjectResult>,
|
||||
createEmptyProject: NewProjectSetup => Promise<void>,
|
||||
createProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
createProjectFromPrivateGameTemplate: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
) => Promise<void>,
|
||||
openAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|
@@ -366,7 +366,6 @@ export const CompactObjectPropertiesEditor = ({
|
||||
} = useManageObjectBehaviors({
|
||||
project,
|
||||
object,
|
||||
isChildObject: !layout,
|
||||
eventsFunctionsExtension,
|
||||
onUpdate: forceUpdate,
|
||||
onBehaviorsUpdated: forceUpdate,
|
||||
|
@@ -326,7 +326,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
{currentTab === 'behaviors' && (
|
||||
<BehaviorsEditor
|
||||
object={object}
|
||||
isChildObject={!!eventsBasedObject}
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
|
@@ -226,25 +226,6 @@ export const getPlanInferredNameFromId = (planId: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
export const planIdSortingFunction = (
|
||||
planIdA: string,
|
||||
planIdB: string
|
||||
): number => {
|
||||
const planOrder = [
|
||||
'gdevelop_free',
|
||||
'gdevelop_indie',
|
||||
'gdevelop_silver',
|
||||
'gdevelop_gold',
|
||||
'gdevelop_pro',
|
||||
'gdevelop_education',
|
||||
'gdevelop_startup',
|
||||
'gdevelop_enterprise',
|
||||
];
|
||||
const indexA = planOrder.indexOf(planIdA);
|
||||
const indexB = planOrder.indexOf(planIdB);
|
||||
return indexA - indexB;
|
||||
};
|
||||
|
||||
export const getPlanIcon = ({
|
||||
planId,
|
||||
logoSize,
|
||||
|
@@ -2,24 +2,39 @@
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Text from '../../../UI/Text';
|
||||
import { Column, LargeSpacer, Line, Spacer } from '../../../UI/Grid';
|
||||
import {
|
||||
type SubscriptionPlanWithPricingSystems,
|
||||
type SubscriptionPlanPricingSystem,
|
||||
EDUCATION_PLAN_MAX_SEATS,
|
||||
EDUCATION_PLAN_MIN_SEATS,
|
||||
getSummarizedSubscriptionPlanFeatures,
|
||||
} from '../../../Utils/GDevelopServices/Usage';
|
||||
import { selectMessageByLocale } from '../../../Utils/i18n/MessageByLocale';
|
||||
import { getPlanIcon } from '../PlanCard';
|
||||
import RedemptionCodeIcon from '../../../UI/CustomSvgIcons/RedemptionCode';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../UI/Layout';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import Link from '../../../UI/Link';
|
||||
import Window from '../../../Utils/Window';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext';
|
||||
import CheckCircleFilled from '../../../UI/CustomSvgIcons/CheckCircleFilled';
|
||||
import ShieldChecked from '../../../UI/CustomSvgIcons/ShieldChecked';
|
||||
import classes from './PromotionSubscriptionPlan.module.css';
|
||||
import Paper from '../../../UI/Paper';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import LeftLoader from '../../../UI/LeftLoader';
|
||||
import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core';
|
||||
import DiscountFlame from '../../../UI/HotMessage/DiscountFlame';
|
||||
import ThumbsUp from '../../../UI/CustomSvgIcons/ThumbsUp';
|
||||
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import SubscriptionPlanTableSummary from './SubscriptionPlanTableSummary';
|
||||
import SubscriptionPlanPricingSummary from './SubscriptionPlanPricingSummary';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import CircledClose from '../../../UI/CustomSvgIcons/CircledClose';
|
||||
|
||||
const styles = {
|
||||
simpleSizeContainer: {
|
||||
@@ -34,6 +49,65 @@ const styles = {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
summarizeFeatureRow: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
tableRightItemContainer: {
|
||||
width: 120,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bulletText: { flex: 1 },
|
||||
bulletIcon: { width: 20, height: 20 },
|
||||
paper: { flex: 1, zIndex: 2, padding: 16 },
|
||||
descriptionContainer: { minHeight: 70 }, // Keep height the same for 1 or 2 lines.
|
||||
discountContainer: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
radioGroup: { flex: 1 },
|
||||
formControlLabel: {
|
||||
borderRadius: 4,
|
||||
// Override MUI margins
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
cursor: 'default',
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through' },
|
||||
unlimitedContainer: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'black',
|
||||
},
|
||||
};
|
||||
|
||||
const formatPriceWithCurrency = (amountInCents: number, currency: string) => {
|
||||
if (currency === 'USD') {
|
||||
return `$${amountInCents / 100}`;
|
||||
}
|
||||
return `${amountInCents / 100}${currency === 'EUR' ? '€' : currency}`;
|
||||
};
|
||||
|
||||
const getYearlyDiscountDisplayText = (
|
||||
monthlyPricingSystem: SubscriptionPlanPricingSystem,
|
||||
yearlyPricingSystem: SubscriptionPlanPricingSystem
|
||||
): string | null => {
|
||||
return (
|
||||
'-' +
|
||||
((
|
||||
100 -
|
||||
(yearlyPricingSystem.amountInCents /
|
||||
(monthlyPricingSystem.amountInCents * 12)) *
|
||||
100
|
||||
).toFixed(0) +
|
||||
'%')
|
||||
);
|
||||
};
|
||||
|
||||
const PromotionSubscriptionPlan = ({
|
||||
@@ -58,6 +132,39 @@ const PromotionSubscriptionPlan = ({
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const [period, setPeriod] = React.useState<'year' | 'month'>('year');
|
||||
|
||||
const planIcon = getPlanIcon({
|
||||
planId: subscriptionPlanWithPricingSystems.id,
|
||||
logoSize: 12,
|
||||
});
|
||||
|
||||
const selectedPricingSystem = subscriptionPlanWithPricingSystems.pricingSystems.find(
|
||||
pricingSystem => pricingSystem.period === period
|
||||
);
|
||||
const yearlyPlanPrice = subscriptionPlanWithPricingSystems.pricingSystems.find(
|
||||
price => price.period === 'year'
|
||||
);
|
||||
const monthlyPlanPrice = subscriptionPlanWithPricingSystems.pricingSystems.find(
|
||||
price => price.period === 'month'
|
||||
);
|
||||
|
||||
if (!selectedPricingSystem || !yearlyPlanPrice || !monthlyPlanPrice) {
|
||||
console.error(
|
||||
'No pricing system found for period',
|
||||
period,
|
||||
'in',
|
||||
subscriptionPlanWithPricingSystems
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const yearlyPriceInCentsWithMonthlyPlan = Math.floor(
|
||||
monthlyPlanPrice.amountInCents * 12
|
||||
);
|
||||
const yearlyDiscountDisplayText = getYearlyDiscountDisplayText(
|
||||
monthlyPlanPrice,
|
||||
yearlyPlanPrice
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -69,25 +176,292 @@ const PromotionSubscriptionPlan = ({
|
||||
: styles.simpleSizeContainer
|
||||
}
|
||||
>
|
||||
<SubscriptionPlanTableSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
<Column justifyContent="center" noMargin>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
>
|
||||
<Line alignItems="center" justifyContent="flex-end" noMargin>
|
||||
<div style={styles.tableRightItemContainer}>
|
||||
<Text size="sub-title">
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
subscriptionPlanWithPricingSystems.nameByLocale
|
||||
)}{' '}
|
||||
</Text>
|
||||
</div>
|
||||
</Line>
|
||||
</div>
|
||||
{getSummarizedSubscriptionPlanFeatures(
|
||||
i18n,
|
||||
subscriptionPlanWithPricingSystems
|
||||
}
|
||||
/>
|
||||
).map((summarizedFeature, index) => (
|
||||
<Column key={index} noMargin>
|
||||
<div
|
||||
style={{
|
||||
...styles.summarizeFeatureRow,
|
||||
borderTop:
|
||||
index !== 0 &&
|
||||
`1px solid ${gdevelopTheme.listItem.separatorColor}`,
|
||||
}}
|
||||
>
|
||||
<Line
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text style={styles.bulletText}>
|
||||
{summarizedFeature.displayedFeatureName}
|
||||
</Text>
|
||||
<div style={styles.tableRightItemContainer}>
|
||||
{summarizedFeature.enabled === 'yes' ? (
|
||||
<CheckCircleFilled
|
||||
style={{
|
||||
...styles.bulletIcon,
|
||||
color: gdevelopTheme.message.valid,
|
||||
}}
|
||||
/>
|
||||
) : summarizedFeature.enabled === 'no' ? (
|
||||
<CircledClose style={styles.bulletIcon} />
|
||||
) : summarizedFeature.unlimited ? (
|
||||
<div
|
||||
style={{
|
||||
...styles.unlimitedContainer,
|
||||
backgroundColor: gdevelopTheme.message.valid,
|
||||
}}
|
||||
>
|
||||
<Text noMargin color="inherit">
|
||||
∞ <Trans>Unlimited</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Text>{summarizedFeature.description}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Line>
|
||||
</div>
|
||||
</Column>
|
||||
))}
|
||||
<LargeSpacer />
|
||||
<Line noMargin justifyContent="center">
|
||||
<Text size="body" color="secondary">
|
||||
<Trans>
|
||||
Compare all the advantages of the different plans in this{' '}
|
||||
<Link
|
||||
href="https://gdevelop.io/pricing#feature-comparison"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://gdevelop.io/pricing#feature-comparison'
|
||||
)
|
||||
}
|
||||
>
|
||||
big feature comparison table
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
<div style={styles.simpleSizeContainer}>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<SubscriptionPlanPricingSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
subscriptionPlanWithPricingSystems
|
||||
}
|
||||
disabled={disabled}
|
||||
onClickChoosePlan={onClickChoosePlan}
|
||||
seatsCount={seatsCount}
|
||||
setSeatsCount={setSeatsCount}
|
||||
period={period}
|
||||
setPeriod={setPeriod}
|
||||
/>
|
||||
<div className={classes.choosePlanContainer}>
|
||||
<Paper background="dark" style={styles.paper}>
|
||||
<ColumnStackLayout>
|
||||
<Line alignItems="center" justifyContent="center">
|
||||
{planIcon}
|
||||
<Text size="block-title">
|
||||
<span style={{ textTransform: 'uppercase' }}>
|
||||
<b>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
subscriptionPlanWithPricingSystems.nameByLocale
|
||||
)}
|
||||
</b>
|
||||
</span>
|
||||
</Text>
|
||||
</Line>
|
||||
<Text size="section-title" noMargin align="center">
|
||||
<div style={styles.descriptionContainer}>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
subscriptionPlanWithPricingSystems.descriptionByLocale
|
||||
)}
|
||||
</div>
|
||||
</Text>
|
||||
{subscriptionPlanWithPricingSystems.id ===
|
||||
'gdevelop_education' && (
|
||||
<ColumnStackLayout key="options" expand noMargin>
|
||||
<SemiControlledTextField
|
||||
value={seatsCount.toString()}
|
||||
floatingLabelFixed
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Number of seats</Trans>}
|
||||
commitOnBlur
|
||||
type="number"
|
||||
onChange={value => {
|
||||
const newValue = parseInt(value);
|
||||
setSeatsCount(
|
||||
Math.min(
|
||||
EDUCATION_PLAN_MAX_SEATS,
|
||||
Math.max(
|
||||
Number.isNaN(newValue)
|
||||
? EDUCATION_PLAN_MIN_SEATS
|
||||
: newValue,
|
||||
EDUCATION_PLAN_MIN_SEATS
|
||||
)
|
||||
)
|
||||
);
|
||||
}}
|
||||
min={EDUCATION_PLAN_MIN_SEATS}
|
||||
max={EDUCATION_PLAN_MAX_SEATS}
|
||||
step={1}
|
||||
helperMarkdownText={i18n._(
|
||||
t`As a teacher, you will use one seat in the plan so make sure to include yourself!`
|
||||
)}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
<Line noMargin expand>
|
||||
<RadioGroup
|
||||
value={period}
|
||||
onChange={event => {
|
||||
setPeriod(event.target.value);
|
||||
}}
|
||||
style={styles.radioGroup}
|
||||
>
|
||||
<FormControlLabel
|
||||
style={{
|
||||
...styles.formControlLabel,
|
||||
backgroundColor:
|
||||
period === 'year'
|
||||
? gdevelopTheme.paper.backgroundColor.light
|
||||
: gdevelopTheme.paper.backgroundColor.medium,
|
||||
}}
|
||||
value="year"
|
||||
control={
|
||||
<Radio color="secondary" disabled={disabled} />
|
||||
}
|
||||
label={
|
||||
<Line>
|
||||
<Column>
|
||||
<Text noMargin color="inherit" size="sub-title">
|
||||
{!yearlyPlanPrice.isPerUser ? (
|
||||
<Trans>
|
||||
Yearly,
|
||||
{formatPriceWithCurrency(
|
||||
yearlyPlanPrice.amountInCents,
|
||||
yearlyPlanPrice.currency
|
||||
)}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Yearly,
|
||||
{formatPriceWithCurrency(
|
||||
yearlyPlanPrice.amountInCents,
|
||||
yearlyPlanPrice.currency
|
||||
)}{' '}
|
||||
per seat
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
Instead of{' '}
|
||||
<span style={styles.discountedPrice}>
|
||||
{formatPriceWithCurrency(
|
||||
yearlyPriceInCentsWithMonthlyPlan,
|
||||
yearlyPlanPrice.currency
|
||||
)}
|
||||
</span>
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
<Column
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...styles.discountContainer,
|
||||
backgroundColor:
|
||||
gdevelopTheme.message.hot.backgroundColor,
|
||||
color: gdevelopTheme.message.hot.color,
|
||||
}}
|
||||
>
|
||||
<DiscountFlame fontSize="small" />
|
||||
<Spacer />
|
||||
<Text color="inherit" noMargin>
|
||||
{yearlyDiscountDisplayText}
|
||||
</Text>
|
||||
</span>
|
||||
</Column>
|
||||
</Line>
|
||||
}
|
||||
/>
|
||||
<Spacer />
|
||||
<FormControlLabel
|
||||
style={{
|
||||
...styles.formControlLabel,
|
||||
backgroundColor:
|
||||
period === 'month'
|
||||
? gdevelopTheme.paper.backgroundColor.light
|
||||
: gdevelopTheme.paper.backgroundColor.medium,
|
||||
}}
|
||||
value="month"
|
||||
control={
|
||||
<Radio color="secondary" disabled={disabled} />
|
||||
}
|
||||
label={
|
||||
<Line>
|
||||
<Column>
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{!monthlyPlanPrice.isPerUser ? (
|
||||
<Trans>
|
||||
Monthly,
|
||||
{formatPriceWithCurrency(
|
||||
monthlyPlanPrice.amountInCents,
|
||||
monthlyPlanPrice.currency
|
||||
)}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Monthly,
|
||||
{formatPriceWithCurrency(
|
||||
monthlyPlanPrice.amountInCents,
|
||||
monthlyPlanPrice.currency
|
||||
)}{' '}
|
||||
per seat
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Line>
|
||||
<Line alignItems="center" justifyContent="center">
|
||||
<RaisedButton
|
||||
color="premium"
|
||||
key="upgrade"
|
||||
disabled={disabled}
|
||||
label={
|
||||
<LeftLoader isLoading={disabled}>
|
||||
<Text size="block-title" color="inherit">
|
||||
<Trans>Choose this plan</Trans>
|
||||
</Text>
|
||||
</LeftLoader>
|
||||
}
|
||||
onClick={() => onClickChoosePlan(selectedPricingSystem)}
|
||||
size="large"
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</Paper>
|
||||
</div>
|
||||
<LineStackLayout justifyContent="center" alignItems="center">
|
||||
<ShieldChecked style={{ color: gdevelopTheme.message.valid }} />
|
||||
<Text color="secondary">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user