diff --git a/docs/widgets/services/wallos.md b/docs/widgets/services/wallos.md
new file mode 100644
index 000000000..f90cdb26b
--- /dev/null
+++ b/docs/widgets/services/wallos.md
@@ -0,0 +1,23 @@
+---
+title: Wallos
+description: Wallos Widget Configuration
+---
+
+Learn more about [Wallos](https://github.com/ellite/wallos).
+
+If you're using more than one currency to record subscriptions then you should also have your "Fixer API" key set-up (`Settings > Fixer API Key`).
+
+> **Please Note:** The monthly cost displayed is the total cost of subscriptions in that month, **not** the _"monthly"_ average cost.
+
+Get your API key under `Profile > API Key`.
+
+Allowed fields: `["activeSubscriptions", "nextRenewingSubscription", "previousMonthlyCost", "thisMonthlyCost", "nextMonthlyCost"]`.
+
+Default fields: `["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"]`.
+
+```yaml
+widget:
+ type: wallos
+ url: http://wallos.host.or.ip
+ key: apikeyapikeyapikeyapikeyapikey
+```
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 07c12d5a8..a4bc77210 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1071,5 +1071,12 @@
"servers": "Servers",
"stacks": "Stacks",
"containers": "Containers"
+ },
+ "wallos": {
+ "activeSubscriptions": "Subscriptions",
+ "thisMonthlyCost": "This Month",
+ "nextMonthlyCost": "Next Month",
+ "previousMonthlyCost": "Prev. Month",
+ "nextRenewingSubscription": "Next Payment"
}
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index b652c2d86..b67ebc7ea 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -142,6 +142,7 @@ const components = {
uptimerobot: dynamic(() => import("./uptimerobot/component")),
urbackup: dynamic(() => import("./urbackup/component")),
vikunja: dynamic(() => import("./vikunja/component")),
+ wallos: dynamic(() => import("./wallos/component")),
watchtower: dynamic(() => import("./watchtower/component")),
wgeasy: dynamic(() => import("./wgeasy/component")),
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
diff --git a/src/widgets/wallos/component.jsx b/src/widgets/wallos/component.jsx
new file mode 100644
index 000000000..c3be5c434
--- /dev/null
+++ b/src/widgets/wallos/component.jsx
@@ -0,0 +1,100 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+const MAX_ALLOWED_FIELDS = 4;
+
+export default function Component({ service }) {
+ const todayDate = new Date();
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ if (!widget.fields) {
+ widget.fields = ["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"];
+ } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ const subscriptionsEndPoint =
+ widget.fields.includes("activeSubscriptions") || widget.fields.includes("nextRenewingSubscription")
+ ? "get_subscriptions"
+ : "";
+ const { data: subscriptionsData, error: subscriptionsError } = useWidgetAPI(widget, subscriptionsEndPoint, {
+ state: 0,
+ sort: "next_payment",
+ });
+ const subscriptionsThisMonthlyEndpoint = widget.fields.includes("thisMonthlyCost") ? "get_monthly_cost" : "";
+ const { data: subscriptionsThisMonthlyCostData, error: subscriptionsThisMonthlyCostError } = useWidgetAPI(
+ widget,
+ subscriptionsThisMonthlyEndpoint,
+ {
+ month: todayDate.getMonth(),
+ year: todayDate.getFullYear(),
+ },
+ );
+ const subscriptionsNextMonthlyEndpoint = widget.fields.includes("nextMonthlyCost") ? "get_monthly_cost" : "";
+ const { data: subscriptionsNextMonthlyCostData, error: subscriptionsNextMonthlyCostError } = useWidgetAPI(
+ widget,
+ subscriptionsNextMonthlyEndpoint,
+ {
+ month: todayDate.getMonth() + 1,
+ year: todayDate.getFullYear(),
+ },
+ );
+ const subscriptionsPreviousMonthlyEndpoint = widget.fields.includes("previousMonthlyCost") ? "get_monthly_cost" : "";
+ const { data: subscriptionsPreviousMonthlyCostData, error: subscriptionsPreviousMonthlyCostError } = useWidgetAPI(
+ widget,
+ subscriptionsPreviousMonthlyEndpoint,
+ {
+ month: todayDate.getMonth() - 1,
+ year: todayDate.getFullYear(),
+ },
+ );
+
+ if (
+ subscriptionsError ||
+ subscriptionsThisMonthlyCostError ||
+ subscriptionsNextMonthlyCostError ||
+ subscriptionsPreviousMonthlyCostError
+ ) {
+ const finalError =
+ subscriptionsError ??
+ subscriptionsThisMonthlyCostError ??
+ subscriptionsNextMonthlyCostError ??
+ subscriptionsPreviousMonthlyCostError;
+ return ;
+ }
+
+ if (
+ (!subscriptionsData &&
+ (widget.fields.includes("activeSubscriptions") || widget.fields.includes("nextRenewingSubscription"))) ||
+ (!subscriptionsThisMonthlyCostData && widget.fields.includes("thisMonthlyCost")) ||
+ (!subscriptionsNextMonthlyCostData && widget.fields.includes("nextMonthlyCost")) ||
+ (!subscriptionsPreviousMonthlyCostData && widget.fields.includes("previousMonthlyCost"))
+ ) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/wallos/widget.js b/src/widgets/wallos/widget.js
new file mode 100644
index 000000000..5eddbbc55
--- /dev/null
+++ b/src/widgets/wallos/widget.js
@@ -0,0 +1,21 @@
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/api/{endpoint}?api_key={key}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ get_monthly_cost: {
+ endpoint: "subscriptions/get_monthly_cost.php",
+ validate: ["localized_monthly_cost", "currency_symbol"],
+ params: ["month", "year"],
+ },
+ get_subscriptions: {
+ endpoint: "subscriptions/get_subscriptions.php",
+ validate: ["subscriptions"],
+ params: ["state", "sort"],
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 8178f26ba..c1865fce3 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -133,6 +133,7 @@ import uptimekuma from "./uptimekuma/widget";
import uptimerobot from "./uptimerobot/widget";
import urbackup from "./urbackup/widget";
import vikunja from "./vikunja/widget";
+import wallos from "./wallos/widget";
import watchtower from "./watchtower/widget";
import wgeasy from "./wgeasy/widget";
import whatsupdocker from "./whatsupdocker/widget";
@@ -279,6 +280,7 @@ const widgets = {
uptimerobot,
urbackup,
vikunja,
+ wallos,
watchtower,
wgeasy,
whatsupdocker,