Implemented new subscription system and basic stripe support
This commit is contained in:
@@ -43,6 +43,13 @@ public class ConfigV1
|
||||
[JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
|
||||
|
||||
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
|
||||
|
||||
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
|
||||
}
|
||||
|
||||
public class StripeData
|
||||
{
|
||||
[JsonProperty("ApiKey")] public string ApiKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public class CleanupData
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Models.Misc;
|
||||
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
|
||||
public class Subscription
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public Currency Currency { get; set; } = Currency.USD;
|
||||
public double Price { get; set; }
|
||||
public string StripeProductId { get; set; } = "";
|
||||
public string StripePriceId { get; set; } = "";
|
||||
public string LimitsJson { get; set; } = "";
|
||||
public int Duration { get; set; } = 30;
|
||||
}
|
||||
@@ -51,8 +51,8 @@ public class User
|
||||
// Subscriptions
|
||||
|
||||
public Subscription? CurrentSubscription { get; set; } = null;
|
||||
public DateTime SubscriptionSince { get; set; } = DateTime.Now;
|
||||
public int SubscriptionDuration { get; set; }
|
||||
public DateTime SubscriptionSince { get; set; } = DateTime.UtcNow;
|
||||
public DateTime SubscriptionExpires { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Ip logs
|
||||
public string RegisterIp { get; set; } = "";
|
||||
|
||||
1105
Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs
generated
Normal file
1105
Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedStripeIntegration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SubscriptionDuration",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "SubscriptionExpires",
|
||||
table: "Users",
|
||||
type: "datetime(6)",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Currency",
|
||||
table: "Subscriptions",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Duration",
|
||||
table: "Subscriptions",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "Price",
|
||||
table: "Subscriptions",
|
||||
type: "double",
|
||||
nullable: false,
|
||||
defaultValue: 0.0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "StripePriceId",
|
||||
table: "Subscriptions",
|
||||
type: "longtext",
|
||||
nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "StripeProductId",
|
||||
table: "Subscriptions",
|
||||
type: "longtext",
|
||||
nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SubscriptionExpires",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Currency",
|
||||
table: "Subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Duration",
|
||||
table: "Subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Price",
|
||||
table: "Subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StripePriceId",
|
||||
table: "Subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StripeProductId",
|
||||
table: "Subscriptions");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SubscriptionDuration",
|
||||
table: "Users",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,10 +663,16 @@ namespace Moonlight.App.Database.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Currency")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("LimitsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
@@ -675,6 +681,17 @@ namespace Moonlight.App.Database.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<string>("StripePriceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("StripeProductId")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Subscriptions");
|
||||
@@ -802,8 +819,8 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.Property<bool>("StreamerMode")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int>("SubscriptionDuration")
|
||||
.HasColumnType("int");
|
||||
b.Property<DateTime>("SubscriptionExpires")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("SubscriptionSince")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.App.Services;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
using Stripe;
|
||||
using Stripe.Checkout;
|
||||
|
||||
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/moonlight/billing")]
|
||||
public class BillingController : Controller
|
||||
{
|
||||
private readonly IdentityService IdentityService;
|
||||
private readonly BillingService BillingService;
|
||||
|
||||
public BillingController(
|
||||
IdentityService identityService,
|
||||
BillingService billingService)
|
||||
{
|
||||
IdentityService = identityService;
|
||||
BillingService = billingService;
|
||||
}
|
||||
|
||||
[HttpGet("success")]
|
||||
public async Task<ActionResult> Success()
|
||||
{
|
||||
var user = await IdentityService.Get();
|
||||
|
||||
if (user == null)
|
||||
return Redirect("/login");
|
||||
|
||||
await BillingService.CompleteCheckout(user);
|
||||
|
||||
return Redirect("/profile/subscriptions/close");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Moonlight.App.Models.Misc;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
|
||||
@@ -10,4 +11,8 @@ public class SubscriptionDataModel
|
||||
|
||||
[Required(ErrorMessage = "You need to enter a description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
public double Price { get; set; } = 0;
|
||||
public Currency Currency { get; set; } = Currency.USD;
|
||||
public int Duration { get; set; } = 30;
|
||||
}
|
||||
7
Moonlight/App/Models/Misc/Currency.cs
Normal file
7
Moonlight/App/Models/Misc/Currency.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.App.Models.Misc;
|
||||
|
||||
public enum Currency
|
||||
{
|
||||
USD = 1,
|
||||
EUR = 2
|
||||
}
|
||||
108
Moonlight/App/Services/BillingService.cs
Normal file
108
Moonlight/App/Services/BillingService.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
using Stripe.Checkout;
|
||||
using Subscription = Moonlight.App.Database.Entities.Subscription;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class BillingService
|
||||
{
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly SubscriptionService SubscriptionService;
|
||||
private readonly Repository<Subscription> SubscriptionRepository;
|
||||
private readonly SessionServerService SessionServerService;
|
||||
private readonly EventSystem Event;
|
||||
|
||||
public BillingService(
|
||||
ConfigService configService,
|
||||
SubscriptionService subscriptionService,
|
||||
Repository<Subscription> subscriptionRepository,
|
||||
EventSystem eventSystem,
|
||||
SessionServerService sessionServerService)
|
||||
{
|
||||
ConfigService = configService;
|
||||
SubscriptionService = subscriptionService;
|
||||
SubscriptionRepository = subscriptionRepository;
|
||||
Event = eventSystem;
|
||||
SessionServerService = sessionServerService;
|
||||
}
|
||||
|
||||
public async Task<string> StartCheckout(User user, Subscription subscription)
|
||||
{
|
||||
var appUrl = ConfigService.Get().Moonlight.AppUrl;
|
||||
var controllerUrl = appUrl + "/api/moonlight/billing";
|
||||
|
||||
var options = new SessionCreateOptions()
|
||||
{
|
||||
LineItems = new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Price = subscription.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
},
|
||||
Mode = "payment",
|
||||
SuccessUrl = controllerUrl + "/success",
|
||||
CancelUrl = controllerUrl + "/cancel",
|
||||
AutomaticTax = new SessionAutomaticTaxOptions()
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
CustomerEmail = user.Email.ToLower(),
|
||||
Metadata = new()
|
||||
{
|
||||
{
|
||||
"productId",
|
||||
subscription.StripeProductId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var service = new SessionService();
|
||||
|
||||
var session = await service.CreateAsync(options);
|
||||
|
||||
return session.Url;
|
||||
}
|
||||
public async Task CompleteCheckout(User user)
|
||||
{
|
||||
var sessionService = new SessionService();
|
||||
|
||||
var sessionsPerUser = await sessionService.ListAsync(new SessionListOptions()
|
||||
{
|
||||
CustomerDetails = new()
|
||||
{
|
||||
Email = user.Email
|
||||
}
|
||||
});
|
||||
|
||||
var latestCompletedSession = sessionsPerUser
|
||||
.Where(x => x.Status == "complete")
|
||||
.Where(x => x.PaymentStatus == "paid")
|
||||
.MaxBy(x => x.Created);
|
||||
|
||||
if (latestCompletedSession == null)
|
||||
throw new DisplayException("No completed session found");
|
||||
|
||||
var productId = latestCompletedSession.Metadata["productId"];
|
||||
|
||||
var subscription = SubscriptionRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.StripeProductId == productId);
|
||||
|
||||
if (subscription == null)
|
||||
throw new DisplayException("No subscription for this product found");
|
||||
|
||||
if (await SubscriptionService.GetActiveSubscription(user) != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SubscriptionService.SetActiveSubscription(user, subscription);
|
||||
await SessionServerService.ReloadUserSessions(user);
|
||||
}
|
||||
}
|
||||
18
Moonlight/App/Services/Interop/PopupService.cs
Normal file
18
Moonlight/App/Services/Interop/PopupService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.App.Services.Interop;
|
||||
|
||||
public class PopupService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public PopupService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task ShowCentered(string url, string title, int width = 500, int height = 500)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.popup.showCentered", url, title, width, height);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Models.Misc;
|
||||
using Moonlight.App.Repositories;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class SubscriptionAdminService
|
||||
{
|
||||
private readonly SubscriptionRepository SubscriptionRepository;
|
||||
private readonly OneTimeJwtService OneTimeJwtService;
|
||||
|
||||
public SubscriptionAdminService(OneTimeJwtService oneTimeJwtService, SubscriptionRepository subscriptionRepository)
|
||||
{
|
||||
OneTimeJwtService = oneTimeJwtService;
|
||||
SubscriptionRepository = subscriptionRepository;
|
||||
}
|
||||
|
||||
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
|
||||
{
|
||||
return Task.FromResult(
|
||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
|
||||
?? Array.Empty<SubscriptionLimit>()
|
||||
);
|
||||
}
|
||||
|
||||
public Task SaveLimits(Subscription subscription, SubscriptionLimit[] limits)
|
||||
{
|
||||
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
|
||||
SubscriptionRepository.Update(subscription);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> GenerateCode(Subscription subscription, int duration)
|
||||
{
|
||||
return Task.FromResult(
|
||||
OneTimeJwtService.Generate(data =>
|
||||
{
|
||||
data.Add("subscription", subscription.Id.ToString());
|
||||
data.Add("duration", duration.ToString());
|
||||
}, TimeSpan.FromDays(10324))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,200 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Models.Misc;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
using Newtonsoft.Json;
|
||||
using Stripe;
|
||||
using File = System.IO.File;
|
||||
using Subscription = Moonlight.App.Database.Entities.Subscription;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class SubscriptionService
|
||||
{
|
||||
private readonly SubscriptionRepository SubscriptionRepository;
|
||||
private readonly OneTimeJwtService OneTimeJwtService;
|
||||
private readonly IdentityService IdentityService;
|
||||
private readonly UserRepository UserRepository;
|
||||
private readonly Repository<Subscription> SubscriptionRepository;
|
||||
private readonly Repository<User> UserRepository;
|
||||
|
||||
public SubscriptionService(
|
||||
SubscriptionRepository subscriptionRepository,
|
||||
OneTimeJwtService oneTimeJwtService,
|
||||
IdentityService identityService,
|
||||
UserRepository userRepository
|
||||
)
|
||||
Repository<Subscription> subscriptionRepository,
|
||||
Repository<User> userRepository)
|
||||
{
|
||||
SubscriptionRepository = subscriptionRepository;
|
||||
OneTimeJwtService = oneTimeJwtService;
|
||||
IdentityService = identityService;
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Subscription?> GetCurrent()
|
||||
public async Task<Subscription> Create(string name, string description, Currency currency, double price, int duration)
|
||||
{
|
||||
var user = await GetCurrentUser();
|
||||
|
||||
if (user == null || user.CurrentSubscription == null)
|
||||
return null;
|
||||
|
||||
var subscriptionEnd = user.SubscriptionSince.ToUniversalTime().AddDays(user.SubscriptionDuration);
|
||||
|
||||
if (subscriptionEnd > DateTime.UtcNow)
|
||||
var optionsProduct = new ProductCreateOptions
|
||||
{
|
||||
return user.CurrentSubscription;
|
||||
}
|
||||
Name = name,
|
||||
Description = description,
|
||||
DefaultPriceData = new()
|
||||
{
|
||||
UnitAmount = (long)(price * 100),
|
||||
Currency = currency.ToString().ToLower()
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
var productService = new ProductService();
|
||||
var product = await productService.CreateAsync(optionsProduct);
|
||||
|
||||
var subscription = new Subscription()
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Currency = currency,
|
||||
Price = price,
|
||||
Duration = duration,
|
||||
LimitsJson = "[]",
|
||||
StripeProductId = product.Id,
|
||||
StripePriceId = product.DefaultPriceId
|
||||
};
|
||||
|
||||
return SubscriptionRepository.Add(subscription);
|
||||
}
|
||||
public async Task Update(Subscription subscription)
|
||||
{
|
||||
// Create the new price object
|
||||
|
||||
var optionsPrice = new PriceCreateOptions
|
||||
{
|
||||
UnitAmount = (long)(subscription.Price * 100),
|
||||
Currency = subscription.Currency.ToString().ToLower(),
|
||||
Product = subscription.StripeProductId
|
||||
};
|
||||
|
||||
var servicePrice = new PriceService();
|
||||
var price = await servicePrice.CreateAsync(optionsPrice);
|
||||
|
||||
// Update the product
|
||||
|
||||
var productService = new ProductService();
|
||||
var product = await productService.UpdateAsync(subscription.StripeProductId, new()
|
||||
{
|
||||
Name = subscription.Name,
|
||||
Description = subscription.Description,
|
||||
DefaultPrice = price.Id
|
||||
});
|
||||
|
||||
// Disable old price
|
||||
await servicePrice.UpdateAsync(subscription.StripePriceId, new()
|
||||
{
|
||||
Active = false
|
||||
});
|
||||
|
||||
// Update the model
|
||||
|
||||
subscription.StripeProductId = product.Id;
|
||||
subscription.StripePriceId = product.DefaultPriceId;
|
||||
|
||||
SubscriptionRepository.Update(subscription);
|
||||
}
|
||||
public async Task Delete(Subscription subscription)
|
||||
{
|
||||
var productService = new ProductService();
|
||||
await productService.DeleteAsync(subscription.StripeProductId);
|
||||
|
||||
SubscriptionRepository.Delete(subscription);
|
||||
}
|
||||
|
||||
public async Task ApplyCode(string code)
|
||||
public Task UpdateLimits(Subscription subscription, SubscriptionLimit[] limits)
|
||||
{
|
||||
var data = await OneTimeJwtService.Validate(code);
|
||||
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
|
||||
SubscriptionRepository.Update(subscription);
|
||||
|
||||
if (data == null)
|
||||
throw new DisplayException("Invalid or expired subscription code");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
|
||||
{
|
||||
var limits =
|
||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson) ?? Array.Empty<SubscriptionLimit>();
|
||||
return Task.FromResult(limits);
|
||||
}
|
||||
|
||||
var id = int.Parse(data["subscription"]);
|
||||
var duration = int.Parse(data["duration"]);
|
||||
public async Task<Subscription?> GetActiveSubscription(User u)
|
||||
{
|
||||
var user = await EnsureData(u);
|
||||
|
||||
var subscription = SubscriptionRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == id);
|
||||
if (user.CurrentSubscription != null)
|
||||
{
|
||||
if (user.SubscriptionExpires < DateTime.UtcNow)
|
||||
{
|
||||
user.CurrentSubscription = null;
|
||||
UserRepository.Update(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription == null)
|
||||
throw new DisplayException("The subscription the code is associated with does not exist");
|
||||
return user.CurrentSubscription;
|
||||
}
|
||||
public async Task CancelSubscription(User u)
|
||||
{
|
||||
var user = await EnsureData(u);
|
||||
|
||||
var user = await GetCurrentUser();
|
||||
user.CurrentSubscription = null;
|
||||
UserRepository.Update(user);
|
||||
}
|
||||
public async Task SetActiveSubscription(User u, Subscription subscription)
|
||||
{
|
||||
var user = await EnsureData(u);
|
||||
|
||||
if (user == null)
|
||||
throw new DisplayException("Unable to determine current user");
|
||||
|
||||
user.CurrentSubscription = subscription;
|
||||
user.SubscriptionDuration = duration;
|
||||
user.SubscriptionSince = DateTime.UtcNow;
|
||||
user.SubscriptionExpires = DateTime.UtcNow.AddDays(subscription.Duration);
|
||||
user.CurrentSubscription = subscription;
|
||||
|
||||
UserRepository.Update(user);
|
||||
|
||||
await OneTimeJwtService.Revoke(code);
|
||||
}
|
||||
|
||||
public async Task Cancel()
|
||||
public async Task<SubscriptionLimit[]> GetDefaultLimits()
|
||||
{
|
||||
if (await GetCurrent() != null)
|
||||
var defaultSubscriptionJson = "[]";
|
||||
var path = PathBuilder.File("storage", "configs", "default_subscription.json");
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var user = await GetCurrentUser();
|
||||
|
||||
user.CurrentSubscription = null;
|
||||
|
||||
UserRepository.Update(user);
|
||||
defaultSubscriptionJson =
|
||||
await File.ReadAllTextAsync(path);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SubscriptionLimit> GetLimit(string identifier) // Cache, optimize sql code
|
||||
return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson)
|
||||
?? Array.Empty<SubscriptionLimit>();
|
||||
}
|
||||
public async Task<SubscriptionLimit> GetLimit(User u, string identifier)
|
||||
{
|
||||
var subscription = await GetCurrent();
|
||||
var subscription = await GetActiveSubscription(u);
|
||||
var defaultLimits = await GetDefaultLimits();
|
||||
|
||||
if (subscription == null)
|
||||
if (subscription != null) // User has a active subscriptions
|
||||
{
|
||||
// If the default subscription limit with identifier is found, return it. if not, return empty
|
||||
return defaultLimits.FirstOrDefault(x => x.Identifier == identifier) ?? new()
|
||||
{
|
||||
Identifier = identifier,
|
||||
Amount = 0
|
||||
};
|
||||
}
|
||||
var subscriptionLimits = await GetLimits(subscription);
|
||||
|
||||
var subscriptionLimits =
|
||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
|
||||
?? Array.Empty<SubscriptionLimit>();
|
||||
var subscriptionLimit = subscriptionLimits
|
||||
.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||
if (subscriptionLimit != null) // Found subscription limit for the user's subscription
|
||||
return subscriptionLimit;
|
||||
} // If were are here, the user's subscription has no limit for this identifier, so we fallback to default
|
||||
|
||||
if (foundLimit != null)
|
||||
return foundLimit;
|
||||
var defaultSubscriptionLimit = defaultLimits
|
||||
.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
// If the default subscription limit with identifier is found, return it. if not, return empty
|
||||
return defaultLimits.FirstOrDefault(x => x.Identifier == identifier) ?? new()
|
||||
if (defaultSubscriptionLimit != null)
|
||||
return defaultSubscriptionLimit; // Default subscription limit found
|
||||
|
||||
return new() // No default subscription limit found
|
||||
{
|
||||
Identifier = identifier,
|
||||
Amount = 0
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<User?> GetCurrentUser()
|
||||
private Task<User> EnsureData(User u)
|
||||
{
|
||||
var user = await IdentityService.Get();
|
||||
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
var userWithData = UserRepository
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.Include(x => x.CurrentSubscription)
|
||||
.First(x => x.Id == user.Id);
|
||||
.First(x => x.Id == u.Id);
|
||||
|
||||
return userWithData;
|
||||
}
|
||||
|
||||
private async Task<SubscriptionLimit[]> GetDefaultLimits() // Add cache and reload option
|
||||
{
|
||||
var defaultSubscriptionJson = "[]";
|
||||
|
||||
if (File.Exists(PathBuilder.File("storage", "configs", "default_subscription.json")))
|
||||
{
|
||||
defaultSubscriptionJson =
|
||||
await File.ReadAllTextAsync(PathBuilder.File("storage", "configs", "default_subscription.json"));
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson) ?? Array.Empty<SubscriptionLimit>();
|
||||
return Task.FromResult(user);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" />
|
||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
|
||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using BlazorDownloadFile;
|
||||
using BlazorTable;
|
||||
using CurrieTechnologies.Razor.SweetAlert2;
|
||||
using HealthChecks.UI.Client;
|
||||
using Moonlight.App.ApiClients.CloudPanel;
|
||||
using Moonlight.App.ApiClients.Daemon;
|
||||
@@ -32,6 +31,8 @@ using Moonlight.App.Services.SupportChat;
|
||||
using Sentry;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Stripe;
|
||||
using SubscriptionService = Moonlight.App.Services.SubscriptionService;
|
||||
|
||||
namespace Moonlight
|
||||
{
|
||||
@@ -209,9 +210,9 @@ namespace Moonlight
|
||||
builder.Services.AddScoped<DynamicBackgroundService>();
|
||||
builder.Services.AddScoped<ServerAddonPluginService>();
|
||||
builder.Services.AddScoped<KeyListenerService>();
|
||||
|
||||
builder.Services.AddScoped<PopupService>();
|
||||
builder.Services.AddScoped<SubscriptionService>();
|
||||
builder.Services.AddScoped<SubscriptionAdminService>();
|
||||
builder.Services.AddScoped<BillingService>();
|
||||
|
||||
builder.Services.AddScoped<SessionClientService>();
|
||||
builder.Services.AddSingleton<SessionServerService>();
|
||||
@@ -252,6 +253,10 @@ namespace Moonlight
|
||||
builder.Services.AddBlazorContextMenu();
|
||||
builder.Services.AddBlazorDownloadFile();
|
||||
|
||||
StripeConfiguration.ApiKey = configService
|
||||
.Get()
|
||||
.Moonlight.Stripe.ApiKey;
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@using Moonlight.App.ApiClients.Modrinth
|
||||
@using Moonlight.App.ApiClients.Wings
|
||||
@using Moonlight.App.Helpers
|
||||
@using Stripe
|
||||
@inherits ErrorBoundaryBase
|
||||
|
||||
@inject AlertService AlertService
|
||||
@@ -105,6 +106,13 @@ else
|
||||
{
|
||||
await AlertService.Error(SmartTranslateService.Translate("This function is not implemented"));
|
||||
}
|
||||
else if (exception is StripeException stripeException)
|
||||
{
|
||||
await AlertService.Error(
|
||||
SmartTranslateService.Translate("Unknown error from stripe"),
|
||||
stripeException.Message
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warn(exception);
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Mappy.Net
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject SubscriptionRepository SubscriptionRepository
|
||||
@inject SubscriptionAdminService SubscriptionAdminService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
|
||||
<OnlyAdmin>
|
||||
<div class="card card-body p-10">
|
||||
@@ -31,7 +32,37 @@
|
||||
<TL>Description</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputText @bind-Value="Model.Description" class="form-control"></InputText>
|
||||
<InputTextArea @bind-Value="Model.Description" class="form-control"></InputTextArea>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Price</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputNumber @bind-Value="Model.Price" class="form-control"></InputNumber>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Currency</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<select @bind="Model.Currency" class="form-select">
|
||||
@foreach (var currency in (Currency[])Enum.GetValues(typeof(Currency)))
|
||||
{
|
||||
if (Model.Currency == currency)
|
||||
{
|
||||
<option value="@(currency)" selected="">@(currency)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(currency)">@(currency)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Duration</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputNumber @bind-Value="Model.Duration" class="form-control"></InputNumber>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -141,12 +172,10 @@
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
Subscription!.Name = Model.Name;
|
||||
Subscription.Description = Model.Description;
|
||||
Subscription = Mapper.Map(Subscription, Model);
|
||||
await SubscriptionService.Update(Subscription!);
|
||||
|
||||
SubscriptionRepository.Update(Subscription);
|
||||
|
||||
await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray());
|
||||
await SubscriptionService.UpdateLimits(Subscription!, Limits.ToArray());
|
||||
|
||||
NavigationManager.NavigateTo("/admin/subscriptions");
|
||||
}
|
||||
@@ -159,10 +188,9 @@
|
||||
|
||||
if (Subscription != null)
|
||||
{
|
||||
Model.Name = Subscription.Name;
|
||||
Model.Description = Subscription.Description;
|
||||
Model = Mapper.Map<SubscriptionDataModel>(Subscription);
|
||||
|
||||
Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList();
|
||||
Limits = (await SubscriptionService.GetLimits(Subscription)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,7 @@
|
||||
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject SubscriptionRepository SubscriptionRepository
|
||||
|
||||
@inject SubscriptionAdminService SubscriptionAdminService
|
||||
@inject AlertService AlertService
|
||||
@inject ClipboardService ClipboardService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
|
||||
<OnlyAdmin>
|
||||
<div class="card">
|
||||
@@ -34,7 +31,10 @@
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => x.Description)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Price"))" Field="@(x => x.Price)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Currency"))" Field="@(x => x.Currency)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Duration"))" Field="@(x => x.Duration)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<a href="/admin/subscriptions/edit/@(context.Id)/">
|
||||
<TL>Manage</TL>
|
||||
@@ -43,14 +43,9 @@
|
||||
</Column>
|
||||
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="float-end">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Create code"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn-primary"
|
||||
OnClick="() => GenerateCode(context)">
|
||||
</WButton>
|
||||
<DeleteButton Confirm="true" OnClick="() => Delete(context)"/>
|
||||
</div>
|
||||
<DeleteButton Confirm="true"
|
||||
OnClick="() => Delete(context)">
|
||||
</DeleteButton>
|
||||
</Template>
|
||||
</Column>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||
@@ -77,25 +72,7 @@
|
||||
|
||||
private async Task Delete(Subscription subscription)
|
||||
{
|
||||
SubscriptionRepository.Delete(subscription);
|
||||
|
||||
await SubscriptionService.Delete(subscription);
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task GenerateCode(Subscription subscription)
|
||||
{
|
||||
var durationText = await AlertService.Text(
|
||||
SmartTranslateService.Translate("Duration"),
|
||||
SmartTranslateService.Translate("Enter duration of subscription"),
|
||||
"30"
|
||||
);
|
||||
|
||||
if (int.TryParse(durationText, out int duration))
|
||||
{
|
||||
var code = await SubscriptionAdminService.GenerateCode(subscription, duration);
|
||||
|
||||
await ClipboardService.Copy(code);
|
||||
await AlertService.Success(SmartTranslateService.Translate("Copied code to clipboard"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject SubscriptionRepository SubscriptionRepository
|
||||
@inject SubscriptionAdminService SubscriptionAdminService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
|
||||
<OnlyAdmin>
|
||||
<div class="card card-body p-10">
|
||||
@@ -21,7 +21,37 @@
|
||||
<TL>Description</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputText @bind-Value="Model.Description" class="form-control"></InputText>
|
||||
<InputTextArea @bind-Value="Model.Description" class="form-control"></InputTextArea>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Price</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputNumber @bind-Value="Model.Price" class="form-control"></InputNumber>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Currency</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<select @bind="Model.Currency" class="form-select">
|
||||
@foreach (var currency in (Currency[])Enum.GetValues(typeof(Currency)))
|
||||
{
|
||||
if (Model.Currency == currency)
|
||||
{
|
||||
<option value="@(currency)" selected="">@(currency)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(currency)">@(currency)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Duration</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputNumber @bind-Value="Model.Duration" class="form-control"></InputNumber>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -124,13 +154,15 @@
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
var sub = SubscriptionRepository.Add(new()
|
||||
{
|
||||
Name = Model.Name,
|
||||
Description = Model.Description
|
||||
});
|
||||
var sub = await SubscriptionService.Create(
|
||||
Model.Name,
|
||||
Model.Description,
|
||||
Model.Currency,
|
||||
Model.Price,
|
||||
Model.Duration
|
||||
);
|
||||
|
||||
await SubscriptionAdminService.SaveLimits(sub, Limits.ToArray());
|
||||
await SubscriptionService.UpdateLimits(sub, Limits.ToArray());
|
||||
|
||||
NavigationManager.NavigateTo("/admin/subscriptions");
|
||||
}
|
||||
|
||||
@@ -130,12 +130,12 @@
|
||||
Model = new();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
Subscription = await SubscriptionService.GetActiveSubscription(User);
|
||||
|
||||
AllowOrder = DomainRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("domains")).Amount;
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "domains")).Amount;
|
||||
|
||||
await lazyLoader.SetText("Loading shared domains");
|
||||
SharedDomains = SharedDomainRepository.Get().ToArray();
|
||||
@@ -146,7 +146,7 @@
|
||||
if (DomainRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("domains")).Amount)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "domains")).Amount)
|
||||
{
|
||||
var domain = await DomainService.Create(Model.Name, Model.SharedDomain, User);
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
@page "/profile/subscriptions"
|
||||
|
||||
@using Moonlight.Shared.Components.Navigations
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Services.Interop
|
||||
|
||||
@inject ConfigService ConfigService
|
||||
@inject AlertService AlertService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
|
||||
<ProfileNavigation Index="3"/>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 p-10">
|
||||
<img src="/assets/media/svg/subscription.svg" class="img-fluid rounded-start" alt="Subscription">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
@if (Subscription == null)
|
||||
{
|
||||
var config = ConfigService
|
||||
.Get()
|
||||
.Moonlight.Subscriptions.SellPass;
|
||||
|
||||
var enableSellpass = config.Enable;
|
||||
var url = config.Url;
|
||||
|
||||
<h3 class="mb-2">
|
||||
<div class="input-group mb-3">
|
||||
<input @bind="Code" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter code"))">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Submit"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn btn-primary"
|
||||
OnClick="OnSubmit">
|
||||
</WButton>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
if (enableSellpass)
|
||||
{
|
||||
<div class="d-flex justify-content-end pb-0 px-0">
|
||||
<a href="@(url)" class="btn btn-light">Buy subscription</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = User.SubscriptionSince.AddDays(User.SubscriptionDuration).ToUniversalTime();
|
||||
|
||||
<h3 class="mb-2">
|
||||
<TL>Active until</TL> @(Formatter.FormatDateOnly(d))
|
||||
</h3>
|
||||
<p class="fs-5 text-gray-600 fw-semibold">
|
||||
<TL>Current subscription</TL>: @(Subscription.Name)
|
||||
</p>
|
||||
<p class="fs-6 text-gray-600 fw-semibold">
|
||||
@(Subscription.Description)
|
||||
</p>
|
||||
<p class="fs-7 text-gray-600 fw-semibold">
|
||||
<TL>We will send you a notification upon subscription expiration</TL>
|
||||
</p>
|
||||
<div class="d-flex justify-content-end pb-0 px-0">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn btn-light"
|
||||
OnClick="Cancel">
|
||||
</WButton>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Subscription? Subscription;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private string Code = "";
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
{
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
}
|
||||
|
||||
private async Task Cancel()
|
||||
{
|
||||
if (await AlertService.ConfirmMath())
|
||||
{
|
||||
await SubscriptionService.Cancel();
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
await SubscriptionService.ApplyCode(Code);
|
||||
Code = "";
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
5
Moonlight/Shared/Views/Profile/Subscriptions/Close.razor
Normal file
5
Moonlight/Shared/Views/Profile/Subscriptions/Close.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@page "/profile/subscriptions/close"
|
||||
|
||||
<script suppress-error="BL9992">
|
||||
window.close();
|
||||
</script>
|
||||
170
Moonlight/Shared/Views/Profile/Subscriptions/Index.razor
Normal file
170
Moonlight/Shared/Views/Profile/Subscriptions/Index.razor
Normal file
@@ -0,0 +1,170 @@
|
||||
@page "/profile/subscriptions"
|
||||
|
||||
@using Moonlight.Shared.Components.Navigations
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services.Interop
|
||||
@using Markdig
|
||||
|
||||
@inject BillingService BillingService
|
||||
@inject Repository<Subscription> SubscriptionRepository
|
||||
@inject AlertService AlertService
|
||||
@inject PopupService PopupService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
|
||||
<ProfileNavigation Index="3"/>
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
@if (CurrentSubscription == null)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body p-lg-17">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="mb-13 text-center">
|
||||
<h1 class="fs-1 fw-bold mb-2">
|
||||
<TL>Chose your plan</TL>
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-3">
|
||||
<TL>Select the perfect plan for your next project</TL>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-10">
|
||||
@foreach (var subscription in Subscriptions)
|
||||
{
|
||||
<div class="col-xl-4">
|
||||
<div class="card bg-secondary border border-primary">
|
||||
<div class="card-header">
|
||||
<div class="card-title">@(subscription.Name)</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center">
|
||||
<span class="mb-2 text-primary">@(subscription.Currency)</span>
|
||||
<span class="fs-3x fw-bold text-primary">
|
||||
@(subscription.Price)
|
||||
</span>
|
||||
<span class="fs-7 fw-semibold opacity-50">
|
||||
/
|
||||
<span>
|
||||
@(subscription.Duration) <TL>Days</TL>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 card-text text-center">
|
||||
@{
|
||||
var html = (MarkupString)Markdown.ToHtml(subscription.Description);
|
||||
}
|
||||
|
||||
@(html)
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="text-center">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Select"))"
|
||||
CssClasses="btn btn-primary"
|
||||
OnClick="() => StartCheckout(subscription)">
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if ((User.SubscriptionExpires - DateTime.UtcNow).TotalDays < 5)
|
||||
{
|
||||
<div class="notice d-flex bg-light-warning rounded border-warning border mb-12 p-6">
|
||||
<div class="d-flex flex-stack flex-grow-1 ">
|
||||
<div class="fw-semibold">
|
||||
<h4 class="text-gray-900 fw-bold">
|
||||
<TL>We need your attention!</TL>
|
||||
</h4>
|
||||
<div class="fs-6 text-gray-700 ">
|
||||
<TL>Your subscription expires soon</TL>
|
||||
<span>
|
||||
@(Math.Round((User.SubscriptionExpires - DateTime.UtcNow).TotalDays)) <TL>left</TL>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<h3 class="mb-2">
|
||||
<TL>Active until</TL> @(Formatter.FormatDateOnly(User.SubscriptionExpires))
|
||||
</h3>
|
||||
<p class="fs-6 text-gray-600 fw-semibold mb-6 mb-lg-15">
|
||||
<TL>We will send you a email upon subscription expiration</TL>
|
||||
</p>
|
||||
<div class="fs-5 mb-2">
|
||||
<span class="text-gray-800 fw-bold me-1">@(CurrentSubscription.Currency) @(CurrentSubscription.Price)</span>
|
||||
<span class="text-gray-600 fw-semibold">
|
||||
<TL>per</TL> @(CurrentSubscription.Duration) <TL>days</TL>
|
||||
</span>
|
||||
</div>
|
||||
<div class="fs-6 text-gray-600 fw-semibold">
|
||||
@(CurrentSubscription.Description)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-end">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
|
||||
CssClasses="btn btn-danger"
|
||||
OnClick="Cancel">
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Subscription[] Subscriptions;
|
||||
private Subscription? CurrentSubscription;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
Subscriptions = SubscriptionRepository.Get().ToArray();
|
||||
CurrentSubscription = await SubscriptionService.GetActiveSubscription(User);
|
||||
}
|
||||
|
||||
private async Task Cancel()
|
||||
{
|
||||
if (await AlertService.ConfirmMath())
|
||||
{
|
||||
await SubscriptionService.CancelSubscription(User);
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartCheckout(Subscription subscription)
|
||||
{
|
||||
var url = await BillingService.StartCheckout(User, subscription);
|
||||
|
||||
await PopupService.ShowCentered(
|
||||
url,
|
||||
"Moonlight Checkout",
|
||||
500,
|
||||
700
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
Model = new();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
Subscription = await SubscriptionService.GetActiveSubscription(User);
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node"));
|
||||
DeployNode = await SmartDeployService.GetNode();
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
var limit = await SubscriptionService.GetLimit("image." + image.Id);
|
||||
var limit = await SubscriptionService.GetLimit(User, "image." + image.Id);
|
||||
|
||||
if (limit.Amount > 0)
|
||||
{
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
var limit = await SubscriptionService.GetLimit("image." + Model.Image.Id);
|
||||
var limit = await SubscriptionService.GetLimit(User, "image." + Model.Image.Id);
|
||||
|
||||
if (limit.Amount > 0)
|
||||
{
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
Model = new();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
Subscription = await SubscriptionService.GetActiveSubscription(User);
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy web host"));
|
||||
CloudPanel = await SmartDeployService.GetCloudPanel();
|
||||
@@ -133,7 +133,7 @@
|
||||
AllowOrder = WebSpaceRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("websites")).Amount;
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "websites")).Amount;
|
||||
}
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
@@ -141,7 +141,7 @@
|
||||
if (WebSpaceRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("websites")).Amount)
|
||||
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "websites")).Amount)
|
||||
{
|
||||
var webSpace = await WebSpaceService.Create(Model.BaseDomain, User, CloudPanel);
|
||||
|
||||
|
||||
@@ -283,37 +283,32 @@
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
scrollToElement: function (id)
|
||||
{
|
||||
scrollToElement: function (id) {
|
||||
let e = document.getElementById(id);
|
||||
e.scrollTop = e.scrollHeight;
|
||||
},
|
||||
triggerResizeEvent: function ()
|
||||
{
|
||||
triggerResizeEvent: function () {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
showNotification: function (title, text, img) {
|
||||
let notification = new Notification(title, { body: text, icon: img });
|
||||
let notification = new Notification(title, {body: text, icon: img});
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
registerXterm: function()
|
||||
{
|
||||
registerXterm: function () {
|
||||
console.log("Registering xterm addons");
|
||||
|
||||
window.XtermBlazor.registerAddon("xterm-addon-fit", new window.FitAddon.FitAddon());
|
||||
//window.XtermBlazor.registerAddon("xterm-addon-search", new window.SearchAddon.SearchAddon());
|
||||
//window.XtermBlazor.registerAddon("xterm-addon-web-links", new window.WebLinksAddon.WebLinksAddon());
|
||||
},
|
||||
loadMonaco: function ()
|
||||
{
|
||||
loadMonaco: function () {
|
||||
console.log("Loading monaco");
|
||||
|
||||
monaco.editor.defineTheme('moonlight-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
],
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#000000'
|
||||
}
|
||||
@@ -321,22 +316,20 @@
|
||||
}
|
||||
},
|
||||
flashbang: {
|
||||
run: function()
|
||||
{
|
||||
run: function () {
|
||||
const light = document.getElementById("flashbang");
|
||||
light.style.boxShadow = "0 0 10000px 10000px white, 0 0 250px 10px #FFFFFF";
|
||||
light.style.animation = "flashbang 5s linear forwards";
|
||||
light.onanimationend = moonlight.flashbang.clean;
|
||||
},
|
||||
clean: function()
|
||||
{
|
||||
clean: function () {
|
||||
const light = document.getElementById("flashbang");
|
||||
light.style.animation = "";
|
||||
light.style.opacity = "0";
|
||||
}
|
||||
},
|
||||
downloads:{
|
||||
downloadStream: async function (fileName, contentStreamReference){
|
||||
downloads: {
|
||||
downloadStream: async function (fileName, contentStreamReference) {
|
||||
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -349,14 +342,11 @@
|
||||
}
|
||||
},
|
||||
keyListener: {
|
||||
register: function (dotNetObjRef)
|
||||
{
|
||||
moonlight.keyListener.listener = (event) =>
|
||||
{
|
||||
register: function (dotNetObjRef) {
|
||||
moonlight.keyListener.listener = (event) => {
|
||||
// filter here what key events should be sent to moonlight
|
||||
|
||||
if(event.code === "KeyS" && event.ctrlKey)
|
||||
{
|
||||
if (event.code === "KeyS" && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
dotNetObjRef.invokeMethodAsync('OnKeyPress', "saveShortcut");
|
||||
}
|
||||
@@ -364,23 +354,19 @@
|
||||
|
||||
window.addEventListener('keydown', moonlight.keyListener.listener);
|
||||
},
|
||||
unregister: function (dotNetObjRef)
|
||||
{
|
||||
unregister: function (dotNetObjRef) {
|
||||
window.removeEventListener('keydown', moonlight.keyListener.listener);
|
||||
}
|
||||
},
|
||||
serverList: {
|
||||
init: function ()
|
||||
{
|
||||
if(moonlight.serverList.Swappable)
|
||||
{
|
||||
init: function () {
|
||||
if (moonlight.serverList.Swappable) {
|
||||
moonlight.serverList.Swappable.destroy();
|
||||
}
|
||||
|
||||
let containers = document.querySelectorAll(".draggable-zone");
|
||||
|
||||
if (containers.length !== 0)
|
||||
{
|
||||
if (containers.length !== 0) {
|
||||
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
|
||||
draggable: ".draggable",
|
||||
handle: ".draggable .draggable-handle",
|
||||
@@ -392,30 +378,44 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
getData: function ()
|
||||
{
|
||||
getData: function () {
|
||||
let groups = new Array();
|
||||
|
||||
let groupElements = document.querySelectorAll('[ml-server-group]');
|
||||
|
||||
groupElements.forEach(groupElement => {
|
||||
let group = new Object();
|
||||
group.name = groupElement.attributes.getNamedItem("ml-server-group").value;
|
||||
let group = new Object();
|
||||
group.name = groupElement.attributes.getNamedItem("ml-server-group").value;
|
||||
|
||||
let servers = new Array();
|
||||
let serverElements = groupElement.querySelectorAll("[ml-server-id]");
|
||||
let servers = new Array();
|
||||
let serverElements = groupElement.querySelectorAll("[ml-server-id]");
|
||||
|
||||
serverElements.forEach(serverElement => {
|
||||
let id = serverElement.attributes.getNamedItem("ml-server-id").value;
|
||||
serverElements.forEach(serverElement => {
|
||||
let id = serverElement.attributes.getNamedItem("ml-server-id").value;
|
||||
|
||||
servers.push(id);
|
||||
});
|
||||
servers.push(id);
|
||||
});
|
||||
|
||||
group.servers = servers;
|
||||
groups.push(group);
|
||||
group.servers = servers;
|
||||
groups.push(group);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
},
|
||||
popup: {
|
||||
showCentered: function (url, title, w, h) {
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX;
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY;
|
||||
|
||||
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
||||
|
||||
const systemZoom = width / window.screen.availWidth;
|
||||
const left = (width - w) / 2 / systemZoom + dualScreenLeft
|
||||
const top = (height - h) / 2 / systemZoom + dualScreenTop
|
||||
const newWindow = window.open(url, title,`scrollbars=yes,width=${w / systemZoom},height=${h / systemZoom},top=${top},left=${left}`)
|
||||
if (window.focus) newWindow.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user