Commit 7c2c4f39 authored by Alexander Lobas's avatar Alexander Lobas Committed by intellij-monorepo-bot
Browse files

IDEA-220109 Plugins management: paid plugins

GitOrigin-RevId: 5292293976118e667ad3c2a2cf83f3bcca180494
parent 48c83bb9
Branches unavailable Tags unavailable
No related merge requests found
Showing with 280 additions and 30 deletions
+280 -30
......@@ -998,7 +998,7 @@ public class PluginManagerConfigurable
@Override
protected void setEmptyText() {
myPanel.getEmptyText().setText("Nothing found.");
myPanel.getEmptyText().appendSecondaryText("Search in marketplace", SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
myPanel.getEmptyText().appendSecondaryText("Search in Marketplace", SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
e -> myTabHeaderComponent.setSelectionWithEvents(MARKETPLACE_TAB));
}
......@@ -1611,6 +1611,7 @@ public class PluginManagerConfigurable
}
myPluginUpdatesService.dispose();
PluginPriceService.cancel();
if (myShutdownCallback != null) {
myShutdownCallback.run();
......
......@@ -10,7 +10,9 @@ import com.intellij.util.Urls;
import com.intellij.util.io.HttpRequests;
import com.intellij.util.io.URLUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.io.JsonReaderEx;
import org.jetbrains.io.JsonUtil;
import java.io.IOException;
import java.net.HttpURLConnection;
......@@ -90,4 +92,22 @@ public class PluginRepositoryRequests {
offsetUrl = baseUrl.addParameters(offsetParameters);
}
}
@Nullable
public static Object getPluginPricesJsonObject() throws IOException {
ApplicationInfoEx instance = ApplicationInfoImpl.getShadowInstance();
Url url = Urls.newFromEncoded(instance.getPluginManagerUrl() + "/geo/files/prices");
return HttpRequests.request(url).throwStatusCodeException(false).productNameAsUserAgent().connect(request -> {
URLConnection connection = request.getConnection();
if (connection instanceof HttpURLConnection && ((HttpURLConnection)connection).getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
try (JsonReaderEx json = new JsonReaderEx(FileUtil.loadTextAndClose(request.getReader()))) {
return JsonUtil.nextAny(json);
}
});
}
}
......@@ -80,35 +80,53 @@ public class LicensePanel extends NonOpaquePanel {
}
public void setTextFromStamp(@NotNull String stamp) {
if (stamp.startsWith("server:")) {
setText("License is active.", false, false);
if (stamp.startsWith("eval:")) {
long[] expTime = parseExpTime(StringUtil.substringAfter(stamp, ":"));
setTextFromStamp(true, expTime[0], expTime[1]);
}
else {
String timeValue = StringUtil.substringAfter(stamp, ":");
else if (stamp.startsWith("key:")) {
setTextFromStamp(false, 0, 0);
// XXX: get exp time
}
else if (stamp.startsWith("stamp:")) {
setTextFromStamp(false, 0, 0);
// XXX: get exp time
}
}
@NotNull
private static long[] parseExpTime(@Nullable String timeValue) {
try {
long time = StringUtil.isEmpty(timeValue) ? 0 : Long.parseLong(timeValue);
long days = time == 0 ? 0 : (time - System.currentTimeMillis()) / DateFormatUtil.DAY_FACTOR;
return new long[]{time, days};
}
catch (NumberFormatException e) {
return new long[]{0, 0};
}
}
if (stamp.startsWith("eval:")) {
if (days == 0) {
setText("Trial expired.", false, true);
}
else {
setText("Trial expires in " + days + " days.", days < 11, false);
}
}
else if (time == 0) {
setText("License is active.", false, false);
}
else if (days > 30) {
setText("License is active until " + PluginManagerConfigurable.DATE_FORMAT.format(new Date(time)) + ".", false, false);
}
else if (days == 0) {
setText("License expired.", false, true);
private void setTextFromStamp(boolean trial, long time, long days) {
if (trial) {
if (days <= 0) {
setText("Trial expired.", false, true);
}
else {
setText("License expires in " + days + " days.", days < 11, false);
setText("Trial expires in " + days + " days.", days < 11, false);
}
}
else if (time == 0) {
setText("License is active.", false, false);
}
else if (days > 30) {
setText("License is active until " + PluginManagerConfigurable.DATE_FORMAT.format(new Date(time)) + ".", false, false);
}
else if (days <= 0) {
setText("License expired.", false, true);
}
else {
setText("License expires in " + days + " days.", days < 11, false);
}
}
public void setLink(@NotNull String text, @NotNull Runnable action, boolean external) {
......@@ -120,6 +138,13 @@ public class LicensePanel extends NonOpaquePanel {
myPanel.setVisible(true);
}
public void updateLink(@NotNull String text, boolean async) {
myLink.setText(text);
if (async) {
myPanel.doLayout();
}
}
@Override
public void setVisible(boolean aFlag) {
super.setVisible(aFlag);
......
......@@ -289,8 +289,7 @@ public class ListPluginComponent extends JPanel {
if (licensePanel.isNotification()) {
licensePanel.setBorder(JBUI.Borders.emptyTop(3));
// todo
licensePanel.setLink("Manage licenses", () -> {/*LicensingFacade.getInstance().register()*/}, false);
//licensePanel.setLink("Manage licenses", () -> { XXX }, false);
myLayout.addLineComponent(licensePanel);
}
}
......
......@@ -34,6 +34,7 @@ import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.function.Supplier;
/**
* @author Alexander Lobas
......@@ -400,8 +401,7 @@ public class PluginDetailsPageComponent extends MultiPanel {
if (productCode == null) {
if (myUpdateDescriptor != null && myUpdateDescriptor.getProductCode() != null) {
myLicensePanel.setText("Next plugin version is paid.\nThe 30-day trial is available.", true, false);
myLicensePanel.setLink("Buy plugin", () ->
BrowserUtil.browse("https://plugins.jetbrains.com/purchase-link/" + myUpdateDescriptor.getProductCode()), true);
showBuyPlugin(() -> myUpdateDescriptor);
}
else {
myLicensePanel.setVisible(false);
......@@ -409,8 +409,7 @@ public class PluginDetailsPageComponent extends MultiPanel {
}
else if (myMarketplace) {
myLicensePanel.setText("The 30-day trial is available.", false, false);
myLicensePanel.setLink("Buy plugin", () ->
BrowserUtil.browse("https://plugins.jetbrains.com/purchase-link/" + myPlugin.getProductCode()), true);
showBuyPlugin(() -> myPlugin);
myLicensePanel.setVisible(true);
}
else {
......@@ -426,8 +425,7 @@ public class PluginDetailsPageComponent extends MultiPanel {
else {
myLicensePanel.setTextFromStamp(stamp);
}
// todo
myLicensePanel.setLink("Manage licenses", () -> {/*LicensingFacade.getInstance().register()*/}, false);
//myLicensePanel.setLink("Manage licenses", () -> { XXX }, false);
}
myLicensePanel.setVisible(true);
......@@ -465,6 +463,19 @@ public class PluginDetailsPageComponent extends MultiPanel {
}
}
private void showBuyPlugin(@NotNull Supplier<IdeaPluginDescriptor> getPlugin) {
IdeaPluginDescriptor plugin = getPlugin.get();
myLicensePanel.setLink("Buy plugin", () ->
BrowserUtil.browse("https://plugins.jetbrains.com/purchase-link/" + plugin.getProductCode()), true);
PluginPriceService.getPrice(plugin, price -> myLicensePanel.updateLink("Buy plugin from " + price, false), price -> {
if (plugin == getPlugin.get()) {
myLicensePanel.updateLink("Buy plugin from " + price, true);
}
});
}
public void updateButtons() {
boolean installedWithoutRestart = InstalledPluginsState.getInstance().wasInstalledWithoutRestart(myPlugin.getPluginId());
if (myMarketplace) {
......
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.plugins.newui;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginRepositoryRequests;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.Map.Entry;
/**
* @author Alexander Lobas
*/
@SuppressWarnings("ALL")
public class PluginPriceService {
private static final Logger LOG = Logger.getInstance(PluginPriceService.class);
private static final DecimalFormat FORMAT = new DecimalFormat("###.#");
private static final Map<String, Object> myPriceTable = new HashMap<>();
private static boolean myPrepared;
private static boolean myPreparing;
public static void getPrice(@NotNull IdeaPluginDescriptor descriptor,
@NotNull Consumer<String> callback,
@NotNull Consumer<String> asyncCallback) {
checkAccess();
String code = descriptor.getProductCode();
if (myPrepared) {
Object value = myPriceTable.get(code);
if (value instanceof String) {
callback.consume((String)value);
}
}
else {
myPriceTable.put(code, asyncCallback);
if (!myPreparing) {
myPreparing = true;
loadPrice();
}
}
}
public static void cancel() {
checkAccess();
clear();
}
private static void clear() {
for (Iterator<Entry<String, Object>> I = myPriceTable.entrySet().iterator(); I.hasNext(); ) {
Entry<String, Object> entry = I.next();
if (entry.getValue() instanceof Consumer) {
I.remove();
}
}
}
private static void loadPrice() {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
try {
Object priceJson = PluginRepositoryRequests.getPluginPricesJsonObject();
if (priceJson instanceof Map) {
Map<String, String> result = parsePrices((Map)priceJson);
ApplicationManager.getApplication().invokeLater(() -> {
checkAccess();
for (Entry<String, String> entry : result.entrySet()) {
Object callback = myPriceTable.put(entry.getKey(), entry.getValue());
if (callback instanceof Consumer) {
((Consumer<String>)callback).consume(entry.getValue());
}
}
clear();
myPrepared = true;
myPreparing = false;
}, ModalityState.any());
}
}
catch (IOException e) {
e.printStackTrace();
LOG.debug(e);
}
});
}
@NotNull
private static Map<String, String> parsePrices(@NotNull Map<String, Object> jsonObject) {
Map<String, String> result = new HashMap<>();
Object plugins = jsonObject.get("plugins");
if (plugins instanceof List) {
String currency = parseCurrency(jsonObject);
for (Map<String, Object> plugin : (List<Map<String, Object>>)plugins) {
Object code = plugin.get("code");
if (!(code instanceof String)) {
continue;
}
Double price = parsePrice(plugin);
if (price != null) {
result.put((String)code, currency + FORMAT.format(price));
}
}
}
return result;
}
@NotNull
private static String parseCurrency(@NotNull Map<String, Object> jsonObject) {
Object iso = jsonObject.get("iso");
if (iso instanceof String) {
Currency currency = Currency.getInstance((String)iso);
if (currency != null) {
return currency.getSymbol(Locale.ENGLISH);
}
}
return "";
}
@Nullable
private static Double parsePrice(@NotNull Map<String, Object> plugin) {
double[] personal = parsePrice(plugin, "personal");
double[] commercial = parsePrice(plugin, "commercial");
if (personal == null && commercial == null) {
return null;
}
if (personal == null || commercial == null) {
for (double value : personal == null ? commercial : personal) {
if (value > 0) {
return value;
}
}
}
else {
for (int i = 0; i < 2; i++) {
if (personal[i] > 0 && commercial[i] > 0) {
return Math.min(personal[i], commercial[i]);
}
}
}
return null;
}
@Nullable
private static double[] parsePrice(@NotNull Map<String, Object> jsonObject, @NotNull String key) {
Object value = jsonObject.get(key);
if (value instanceof Map) {
Object subscription = ((Map)value).get("subscription");
if (subscription instanceof Map) {
return new double[]{parsePriceValue((Map)subscription, "monthly"), parsePriceValue((Map)subscription, "annual")};
}
}
return null;
}
private static double parsePriceValue(@NotNull Map<String, Object> jsonObject, @NotNull String key) {
Object value = jsonObject.get(key);
if (value instanceof Double) {
return (double)value;
}
if (value instanceof String) {
try {
return Double.parseDouble((String)value);
}
catch (NumberFormatException ignore) {
}
}
return 0;
}
private static void checkAccess() {
assert SwingUtilities.isEventDispatchThread();
}
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment