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("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
|
||||||
|
|
||||||
[JsonProperty("Sentry")] public SentryData Sentry { 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
|
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 class Subscription
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public string Description { 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 string LimitsJson { get; set; } = "";
|
||||||
|
public int Duration { get; set; } = 30;
|
||||||
}
|
}
|
||||||
@@ -51,8 +51,8 @@ public class User
|
|||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|
||||||
public Subscription? CurrentSubscription { get; set; } = null;
|
public Subscription? CurrentSubscription { get; set; } = null;
|
||||||
public DateTime SubscriptionSince { get; set; } = DateTime.Now;
|
public DateTime SubscriptionSince { get; set; } = DateTime.UtcNow;
|
||||||
public int SubscriptionDuration { get; set; }
|
public DateTime SubscriptionExpires { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
// Ip logs
|
// Ip logs
|
||||||
public string RegisterIp { get; set; } = "";
|
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()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Currency")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Duration")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("LimitsJson")
|
b.Property<string>("LimitsJson")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
@@ -675,6 +681,17 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.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.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Subscriptions");
|
b.ToTable("Subscriptions");
|
||||||
@@ -802,8 +819,8 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.Property<bool>("StreamerMode")
|
b.Property<bool>("StreamerMode")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<int>("SubscriptionDuration")
|
b.Property<DateTime>("SubscriptionExpires")
|
||||||
.HasColumnType("int");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<DateTime>("SubscriptionSince")
|
b.Property<DateTime>("SubscriptionSince")
|
||||||
.HasColumnType("datetime(6)");
|
.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 System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
namespace Moonlight.App.Models.Forms;
|
namespace Moonlight.App.Models.Forms;
|
||||||
|
|
||||||
@@ -10,4 +11,8 @@ public class SubscriptionDataModel
|
|||||||
|
|
||||||
[Required(ErrorMessage = "You need to enter a description")]
|
[Required(ErrorMessage = "You need to enter a description")]
|
||||||
public string Description { get; set; } = "";
|
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 Microsoft.EntityFrameworkCore;
|
||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Exceptions;
|
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
using Moonlight.App.Models.Misc;
|
using Moonlight.App.Models.Misc;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Services.Sessions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Stripe;
|
||||||
|
using File = System.IO.File;
|
||||||
|
using Subscription = Moonlight.App.Database.Entities.Subscription;
|
||||||
|
|
||||||
namespace Moonlight.App.Services;
|
namespace Moonlight.App.Services;
|
||||||
|
|
||||||
public class SubscriptionService
|
public class SubscriptionService
|
||||||
{
|
{
|
||||||
private readonly SubscriptionRepository SubscriptionRepository;
|
private readonly Repository<Subscription> SubscriptionRepository;
|
||||||
private readonly OneTimeJwtService OneTimeJwtService;
|
private readonly Repository<User> UserRepository;
|
||||||
private readonly IdentityService IdentityService;
|
|
||||||
private readonly UserRepository UserRepository;
|
|
||||||
|
|
||||||
public SubscriptionService(
|
public SubscriptionService(
|
||||||
SubscriptionRepository subscriptionRepository,
|
Repository<Subscription> subscriptionRepository,
|
||||||
OneTimeJwtService oneTimeJwtService,
|
Repository<User> userRepository)
|
||||||
IdentityService identityService,
|
|
||||||
UserRepository userRepository
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
SubscriptionRepository = subscriptionRepository;
|
SubscriptionRepository = subscriptionRepository;
|
||||||
OneTimeJwtService = oneTimeJwtService;
|
|
||||||
IdentityService = identityService;
|
|
||||||
UserRepository = userRepository;
|
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();
|
var optionsProduct = new ProductCreateOptions
|
||||||
|
|
||||||
if (user == null || user.CurrentSubscription == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var subscriptionEnd = user.SubscriptionSince.ToUniversalTime().AddDays(user.SubscriptionDuration);
|
|
||||||
|
|
||||||
if (subscriptionEnd > DateTime.UtcNow)
|
|
||||||
{
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
DefaultPriceData = new()
|
||||||
|
{
|
||||||
|
UnitAmount = (long)(price * 100),
|
||||||
|
Currency = currency.ToString().ToLower()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Task UpdateLimits(Subscription subscription, SubscriptionLimit[] limits)
|
||||||
|
{
|
||||||
|
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
|
||||||
|
SubscriptionRepository.Update(subscription);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
|
||||||
|
{
|
||||||
|
var limits =
|
||||||
|
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson) ?? Array.Empty<SubscriptionLimit>();
|
||||||
|
return Task.FromResult(limits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription?> GetActiveSubscription(User u)
|
||||||
|
{
|
||||||
|
var user = await EnsureData(u);
|
||||||
|
|
||||||
|
if (user.CurrentSubscription != null)
|
||||||
|
{
|
||||||
|
if (user.SubscriptionExpires < DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
user.CurrentSubscription = null;
|
||||||
|
UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return user.CurrentSubscription;
|
return user.CurrentSubscription;
|
||||||
}
|
}
|
||||||
|
public async Task CancelSubscription(User u)
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ApplyCode(string code)
|
|
||||||
{
|
{
|
||||||
var data = await OneTimeJwtService.Validate(code);
|
var user = await EnsureData(u);
|
||||||
|
|
||||||
if (data == null)
|
|
||||||
throw new DisplayException("Invalid or expired subscription code");
|
|
||||||
|
|
||||||
var id = int.Parse(data["subscription"]);
|
|
||||||
var duration = int.Parse(data["duration"]);
|
|
||||||
|
|
||||||
var subscription = SubscriptionRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefault(x => x.Id == id);
|
|
||||||
|
|
||||||
if (subscription == null)
|
|
||||||
throw new DisplayException("The subscription the code is associated with does not exist");
|
|
||||||
|
|
||||||
var user = await GetCurrentUser();
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new DisplayException("Unable to determine current user");
|
|
||||||
|
|
||||||
user.CurrentSubscription = subscription;
|
|
||||||
user.SubscriptionDuration = duration;
|
|
||||||
user.SubscriptionSince = DateTime.UtcNow;
|
|
||||||
|
|
||||||
UserRepository.Update(user);
|
|
||||||
|
|
||||||
await OneTimeJwtService.Revoke(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Cancel()
|
|
||||||
{
|
|
||||||
if (await GetCurrent() != null)
|
|
||||||
{
|
|
||||||
var user = await GetCurrentUser();
|
|
||||||
|
|
||||||
user.CurrentSubscription = null;
|
user.CurrentSubscription = null;
|
||||||
|
UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
public async Task SetActiveSubscription(User u, Subscription subscription)
|
||||||
|
{
|
||||||
|
var user = await EnsureData(u);
|
||||||
|
|
||||||
|
user.SubscriptionSince = DateTime.UtcNow;
|
||||||
|
user.SubscriptionExpires = DateTime.UtcNow.AddDays(subscription.Duration);
|
||||||
|
user.CurrentSubscription = subscription;
|
||||||
|
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SubscriptionLimit> GetLimit(string identifier) // Cache, optimize sql code
|
public async Task<SubscriptionLimit[]> GetDefaultLimits()
|
||||||
{
|
|
||||||
var subscription = await GetCurrent();
|
|
||||||
var defaultLimits = await GetDefaultLimits();
|
|
||||||
|
|
||||||
if (subscription == null)
|
|
||||||
{
|
|
||||||
// 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 =
|
|
||||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
|
|
||||||
?? Array.Empty<SubscriptionLimit>();
|
|
||||||
|
|
||||||
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
|
|
||||||
|
|
||||||
if (foundLimit != null)
|
|
||||||
return foundLimit;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User?> GetCurrentUser()
|
|
||||||
{
|
|
||||||
var user = await IdentityService.Get();
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var userWithData = UserRepository
|
|
||||||
.Get()
|
|
||||||
.Include(x => x.CurrentSubscription)
|
|
||||||
.First(x => x.Id == user.Id);
|
|
||||||
|
|
||||||
return userWithData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SubscriptionLimit[]> GetDefaultLimits() // Add cache and reload option
|
|
||||||
{
|
{
|
||||||
var defaultSubscriptionJson = "[]";
|
var defaultSubscriptionJson = "[]";
|
||||||
|
var path = PathBuilder.File("storage", "configs", "default_subscription.json");
|
||||||
|
|
||||||
if (File.Exists(PathBuilder.File("storage", "configs", "default_subscription.json")))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
defaultSubscriptionJson =
|
defaultSubscriptionJson =
|
||||||
await File.ReadAllTextAsync(PathBuilder.File("storage", "configs", "default_subscription.json"));
|
await File.ReadAllTextAsync(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson) ?? Array.Empty<SubscriptionLimit>();
|
return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson)
|
||||||
|
?? Array.Empty<SubscriptionLimit>();
|
||||||
|
}
|
||||||
|
public async Task<SubscriptionLimit> GetLimit(User u, string identifier)
|
||||||
|
{
|
||||||
|
var subscription = await GetActiveSubscription(u);
|
||||||
|
var defaultLimits = await GetDefaultLimits();
|
||||||
|
|
||||||
|
if (subscription != null) // User has a active subscriptions
|
||||||
|
{
|
||||||
|
var subscriptionLimits = await GetLimits(subscription);
|
||||||
|
|
||||||
|
var subscriptionLimit = 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
|
||||||
|
|
||||||
|
var defaultSubscriptionLimit = defaultLimits
|
||||||
|
.FirstOrDefault(x => x.Identifier == identifier);
|
||||||
|
|
||||||
|
if (defaultSubscriptionLimit != null)
|
||||||
|
return defaultSubscriptionLimit; // Default subscription limit found
|
||||||
|
|
||||||
|
return new() // No default subscription limit found
|
||||||
|
{
|
||||||
|
Identifier = identifier,
|
||||||
|
Amount = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<User> EnsureData(User u)
|
||||||
|
{
|
||||||
|
var user = UserRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CurrentSubscription)
|
||||||
|
.First(x => x.Id == u.Id);
|
||||||
|
|
||||||
|
return Task.FromResult(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" />
|
||||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
<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="UAParser" Version="3.1.47" />
|
||||||
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using BlazorDownloadFile;
|
using BlazorDownloadFile;
|
||||||
using BlazorTable;
|
using BlazorTable;
|
||||||
using CurrieTechnologies.Razor.SweetAlert2;
|
|
||||||
using HealthChecks.UI.Client;
|
using HealthChecks.UI.Client;
|
||||||
using Moonlight.App.ApiClients.CloudPanel;
|
using Moonlight.App.ApiClients.CloudPanel;
|
||||||
using Moonlight.App.ApiClients.Daemon;
|
using Moonlight.App.ApiClients.Daemon;
|
||||||
@@ -32,6 +31,8 @@ using Moonlight.App.Services.SupportChat;
|
|||||||
using Sentry;
|
using Sentry;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
using Stripe;
|
||||||
|
using SubscriptionService = Moonlight.App.Services.SubscriptionService;
|
||||||
|
|
||||||
namespace Moonlight
|
namespace Moonlight
|
||||||
{
|
{
|
||||||
@@ -209,9 +210,9 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<DynamicBackgroundService>();
|
builder.Services.AddScoped<DynamicBackgroundService>();
|
||||||
builder.Services.AddScoped<ServerAddonPluginService>();
|
builder.Services.AddScoped<ServerAddonPluginService>();
|
||||||
builder.Services.AddScoped<KeyListenerService>();
|
builder.Services.AddScoped<KeyListenerService>();
|
||||||
|
builder.Services.AddScoped<PopupService>();
|
||||||
builder.Services.AddScoped<SubscriptionService>();
|
builder.Services.AddScoped<SubscriptionService>();
|
||||||
builder.Services.AddScoped<SubscriptionAdminService>();
|
builder.Services.AddScoped<BillingService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SessionClientService>();
|
builder.Services.AddScoped<SessionClientService>();
|
||||||
builder.Services.AddSingleton<SessionServerService>();
|
builder.Services.AddSingleton<SessionServerService>();
|
||||||
@@ -252,6 +253,10 @@ namespace Moonlight
|
|||||||
builder.Services.AddBlazorContextMenu();
|
builder.Services.AddBlazorContextMenu();
|
||||||
builder.Services.AddBlazorDownloadFile();
|
builder.Services.AddBlazorDownloadFile();
|
||||||
|
|
||||||
|
StripeConfiguration.ApiKey = configService
|
||||||
|
.Get()
|
||||||
|
.Moonlight.Stripe.ApiKey;
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
@using Moonlight.App.ApiClients.Modrinth
|
@using Moonlight.App.ApiClients.Modrinth
|
||||||
@using Moonlight.App.ApiClients.Wings
|
@using Moonlight.App.ApiClients.Wings
|
||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
|
@using Stripe
|
||||||
@inherits ErrorBoundaryBase
|
@inherits ErrorBoundaryBase
|
||||||
|
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@@ -105,6 +106,13 @@ else
|
|||||||
{
|
{
|
||||||
await AlertService.Error(SmartTranslateService.Translate("This function is not implemented"));
|
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
|
else
|
||||||
{
|
{
|
||||||
Logger.Warn(exception);
|
Logger.Warn(exception);
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
@using Moonlight.App.Repositories
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Mappy.Net
|
||||||
|
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject SubscriptionRepository SubscriptionRepository
|
@inject SubscriptionRepository SubscriptionRepository
|
||||||
@inject SubscriptionAdminService SubscriptionAdminService
|
@inject SubscriptionService SubscriptionService
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<div class="card card-body p-10">
|
<div class="card card-body p-10">
|
||||||
@@ -31,7 +32,37 @@
|
|||||||
<TL>Description</TL>
|
<TL>Description</TL>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group mb-5">
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -141,12 +172,10 @@
|
|||||||
|
|
||||||
private async Task OnSubmit()
|
private async Task OnSubmit()
|
||||||
{
|
{
|
||||||
Subscription!.Name = Model.Name;
|
Subscription = Mapper.Map(Subscription, Model);
|
||||||
Subscription.Description = Model.Description;
|
await SubscriptionService.Update(Subscription!);
|
||||||
|
|
||||||
SubscriptionRepository.Update(Subscription);
|
await SubscriptionService.UpdateLimits(Subscription!, Limits.ToArray());
|
||||||
|
|
||||||
await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray());
|
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/admin/subscriptions");
|
NavigationManager.NavigateTo("/admin/subscriptions");
|
||||||
}
|
}
|
||||||
@@ -159,10 +188,9 @@
|
|||||||
|
|
||||||
if (Subscription != null)
|
if (Subscription != null)
|
||||||
{
|
{
|
||||||
Model.Name = Subscription.Name;
|
Model = Mapper.Map<SubscriptionDataModel>(Subscription);
|
||||||
Model.Description = Subscription.Description;
|
|
||||||
|
|
||||||
Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList();
|
Limits = (await SubscriptionService.GetLimits(Subscription)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,7 @@
|
|||||||
|
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
@inject SubscriptionRepository SubscriptionRepository
|
@inject SubscriptionRepository SubscriptionRepository
|
||||||
|
@inject SubscriptionService SubscriptionService
|
||||||
@inject SubscriptionAdminService SubscriptionAdminService
|
|
||||||
@inject AlertService AlertService
|
|
||||||
@inject ClipboardService ClipboardService
|
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<div class="card">
|
<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("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("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("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>
|
<Template>
|
||||||
<a href="/admin/subscriptions/edit/@(context.Id)/">
|
<a href="/admin/subscriptions/edit/@(context.Id)/">
|
||||||
<TL>Manage</TL>
|
<TL>Manage</TL>
|
||||||
@@ -43,14 +43,9 @@
|
|||||||
</Column>
|
</Column>
|
||||||
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
<Template>
|
<Template>
|
||||||
<div class="float-end">
|
<DeleteButton Confirm="true"
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Create code"))"
|
OnClick="() => Delete(context)">
|
||||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
</DeleteButton>
|
||||||
CssClasses="btn-primary"
|
|
||||||
OnClick="() => GenerateCode(context)">
|
|
||||||
</WButton>
|
|
||||||
<DeleteButton Confirm="true" OnClick="() => Delete(context)"/>
|
|
||||||
</div>
|
|
||||||
</Template>
|
</Template>
|
||||||
</Column>
|
</Column>
|
||||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
@@ -77,25 +72,7 @@
|
|||||||
|
|
||||||
private async Task Delete(Subscription subscription)
|
private async Task Delete(Subscription subscription)
|
||||||
{
|
{
|
||||||
SubscriptionRepository.Delete(subscription);
|
await SubscriptionService.Delete(subscription);
|
||||||
|
|
||||||
await LazyLoader.Reload();
|
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 NavigationManager NavigationManager
|
||||||
@inject SubscriptionRepository SubscriptionRepository
|
@inject SubscriptionRepository SubscriptionRepository
|
||||||
@inject SubscriptionAdminService SubscriptionAdminService
|
@inject SubscriptionService SubscriptionService
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<div class="card card-body p-10">
|
<div class="card card-body p-10">
|
||||||
@@ -21,7 +21,37 @@
|
|||||||
<TL>Description</TL>
|
<TL>Description</TL>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group mb-5">
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -124,13 +154,15 @@
|
|||||||
|
|
||||||
private async Task OnSubmit()
|
private async Task OnSubmit()
|
||||||
{
|
{
|
||||||
var sub = SubscriptionRepository.Add(new()
|
var sub = await SubscriptionService.Create(
|
||||||
{
|
Model.Name,
|
||||||
Name = Model.Name,
|
Model.Description,
|
||||||
Description = Model.Description
|
Model.Currency,
|
||||||
});
|
Model.Price,
|
||||||
|
Model.Duration
|
||||||
|
);
|
||||||
|
|
||||||
await SubscriptionAdminService.SaveLimits(sub, Limits.ToArray());
|
await SubscriptionService.UpdateLimits(sub, Limits.ToArray());
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/admin/subscriptions");
|
NavigationManager.NavigateTo("/admin/subscriptions");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,12 +130,12 @@
|
|||||||
Model = new();
|
Model = new();
|
||||||
|
|
||||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
||||||
Subscription = await SubscriptionService.GetCurrent();
|
Subscription = await SubscriptionService.GetActiveSubscription(User);
|
||||||
|
|
||||||
AllowOrder = DomainRepository
|
AllowOrder = DomainRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Owner)
|
.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");
|
await lazyLoader.SetText("Loading shared domains");
|
||||||
SharedDomains = SharedDomainRepository.Get().ToArray();
|
SharedDomains = SharedDomainRepository.Get().ToArray();
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
if (DomainRepository
|
if (DomainRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Owner)
|
.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);
|
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();
|
Model = new();
|
||||||
|
|
||||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
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"));
|
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node"));
|
||||||
DeployNode = await SmartDeployService.GetNode();
|
DeployNode = await SmartDeployService.GetNode();
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
foreach (var image in images)
|
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)
|
if (limit.Amount > 0)
|
||||||
{
|
{
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
|
|
||||||
private async Task OnValidSubmit()
|
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)
|
if (limit.Amount > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
Model = new();
|
Model = new();
|
||||||
|
|
||||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
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"));
|
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy web host"));
|
||||||
CloudPanel = await SmartDeployService.GetCloudPanel();
|
CloudPanel = await SmartDeployService.GetCloudPanel();
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
AllowOrder = WebSpaceRepository
|
AllowOrder = WebSpaceRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Owner)
|
.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()
|
private async Task OnValidSubmit()
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
if (WebSpaceRepository
|
if (WebSpaceRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Owner)
|
.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);
|
var webSpace = await WebSpaceService.Create(Model.BaseDomain, User, CloudPanel);
|
||||||
|
|
||||||
|
|||||||
@@ -283,37 +283,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
scrollToElement: function (id)
|
scrollToElement: function (id) {
|
||||||
{
|
|
||||||
let e = document.getElementById(id);
|
let e = document.getElementById(id);
|
||||||
e.scrollTop = e.scrollHeight;
|
e.scrollTop = e.scrollHeight;
|
||||||
},
|
},
|
||||||
triggerResizeEvent: function ()
|
triggerResizeEvent: function () {
|
||||||
{
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
},
|
},
|
||||||
showNotification: function (title, text, img) {
|
showNotification: function (title, text, img) {
|
||||||
let notification = new Notification(title, { body: text, icon: img });
|
let notification = new Notification(title, {body: text, icon: img});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
registerXterm: function()
|
registerXterm: function () {
|
||||||
{
|
|
||||||
console.log("Registering xterm addons");
|
console.log("Registering xterm addons");
|
||||||
|
|
||||||
window.XtermBlazor.registerAddon("xterm-addon-fit", new window.FitAddon.FitAddon());
|
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-search", new window.SearchAddon.SearchAddon());
|
||||||
//window.XtermBlazor.registerAddon("xterm-addon-web-links", new window.WebLinksAddon.WebLinksAddon());
|
//window.XtermBlazor.registerAddon("xterm-addon-web-links", new window.WebLinksAddon.WebLinksAddon());
|
||||||
},
|
},
|
||||||
loadMonaco: function ()
|
loadMonaco: function () {
|
||||||
{
|
|
||||||
console.log("Loading monaco");
|
console.log("Loading monaco");
|
||||||
|
|
||||||
monaco.editor.defineTheme('moonlight-theme', {
|
monaco.editor.defineTheme('moonlight-theme', {
|
||||||
base: 'vs-dark',
|
base: 'vs-dark',
|
||||||
inherit: true,
|
inherit: true,
|
||||||
rules: [
|
rules: [],
|
||||||
],
|
|
||||||
colors: {
|
colors: {
|
||||||
'editor.background': '#000000'
|
'editor.background': '#000000'
|
||||||
}
|
}
|
||||||
@@ -321,22 +316,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
flashbang: {
|
flashbang: {
|
||||||
run: function()
|
run: function () {
|
||||||
{
|
|
||||||
const light = document.getElementById("flashbang");
|
const light = document.getElementById("flashbang");
|
||||||
light.style.boxShadow = "0 0 10000px 10000px white, 0 0 250px 10px #FFFFFF";
|
light.style.boxShadow = "0 0 10000px 10000px white, 0 0 250px 10px #FFFFFF";
|
||||||
light.style.animation = "flashbang 5s linear forwards";
|
light.style.animation = "flashbang 5s linear forwards";
|
||||||
light.onanimationend = moonlight.flashbang.clean;
|
light.onanimationend = moonlight.flashbang.clean;
|
||||||
},
|
},
|
||||||
clean: function()
|
clean: function () {
|
||||||
{
|
|
||||||
const light = document.getElementById("flashbang");
|
const light = document.getElementById("flashbang");
|
||||||
light.style.animation = "";
|
light.style.animation = "";
|
||||||
light.style.opacity = "0";
|
light.style.opacity = "0";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloads:{
|
downloads: {
|
||||||
downloadStream: async function (fileName, contentStreamReference){
|
downloadStream: async function (fileName, contentStreamReference) {
|
||||||
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
||||||
const blob = new Blob([arrayBuffer]);
|
const blob = new Blob([arrayBuffer]);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -349,14 +342,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyListener: {
|
keyListener: {
|
||||||
register: function (dotNetObjRef)
|
register: function (dotNetObjRef) {
|
||||||
{
|
moonlight.keyListener.listener = (event) => {
|
||||||
moonlight.keyListener.listener = (event) =>
|
|
||||||
{
|
|
||||||
// filter here what key events should be sent to moonlight
|
// filter here what key events should be sent to moonlight
|
||||||
|
|
||||||
if(event.code === "KeyS" && event.ctrlKey)
|
if (event.code === "KeyS" && event.ctrlKey) {
|
||||||
{
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dotNetObjRef.invokeMethodAsync('OnKeyPress', "saveShortcut");
|
dotNetObjRef.invokeMethodAsync('OnKeyPress', "saveShortcut");
|
||||||
}
|
}
|
||||||
@@ -364,23 +354,19 @@
|
|||||||
|
|
||||||
window.addEventListener('keydown', moonlight.keyListener.listener);
|
window.addEventListener('keydown', moonlight.keyListener.listener);
|
||||||
},
|
},
|
||||||
unregister: function (dotNetObjRef)
|
unregister: function (dotNetObjRef) {
|
||||||
{
|
|
||||||
window.removeEventListener('keydown', moonlight.keyListener.listener);
|
window.removeEventListener('keydown', moonlight.keyListener.listener);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
serverList: {
|
serverList: {
|
||||||
init: function ()
|
init: function () {
|
||||||
{
|
if (moonlight.serverList.Swappable) {
|
||||||
if(moonlight.serverList.Swappable)
|
|
||||||
{
|
|
||||||
moonlight.serverList.Swappable.destroy();
|
moonlight.serverList.Swappable.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
let containers = document.querySelectorAll(".draggable-zone");
|
let containers = document.querySelectorAll(".draggable-zone");
|
||||||
|
|
||||||
if (containers.length !== 0)
|
if (containers.length !== 0) {
|
||||||
{
|
|
||||||
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
|
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
|
||||||
draggable: ".draggable",
|
draggable: ".draggable",
|
||||||
handle: ".draggable .draggable-handle",
|
handle: ".draggable .draggable-handle",
|
||||||
@@ -392,8 +378,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getData: function ()
|
getData: function () {
|
||||||
{
|
|
||||||
let groups = new Array();
|
let groups = new Array();
|
||||||
|
|
||||||
let groupElements = document.querySelectorAll('[ml-server-group]');
|
let groupElements = document.querySelectorAll('[ml-server-group]');
|
||||||
@@ -417,5 +402,20 @@
|
|||||||
|
|
||||||
return groups;
|
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