diff --git a/.git-town.toml b/.git-town.toml new file mode 100644 index 000000000..f08e1f7d4 --- /dev/null +++ b/.git-town.toml @@ -0,0 +1,4 @@ +# See https://www.git-town.com/configuration-file for details + +[branches] +main = "develop" diff --git a/apps/portal-app-shell/package.json b/apps/portal-app-shell/package.json index 20a9e7ee3..639423df1 100644 --- a/apps/portal-app-shell/package.json +++ b/apps/portal-app-shell/package.json @@ -17,6 +17,7 @@ "@lumeweb/portal-framework-core": "workspace:*", "@lumeweb/portal-framework-ui": "workspace:*", "@lumeweb/portal-framework-ui-core": "workspace:*", + "@lumeweb/advanced-rest-provider": "workspace:*", "@lumeweb/portal-sdk": "workspace:*", "@t3-oss/env-core": "^0.12.0", "@tanstack/react-query": "^4.36.1", diff --git a/apps/portal-app-shell/src/App.tsx b/apps/portal-app-shell/src/App.tsx index 45932a703..3dee56f02 100644 --- a/apps/portal-app-shell/src/App.tsx +++ b/apps/portal-app-shell/src/App.tsx @@ -1,6 +1,7 @@ import { AppComponent, AppComponentProps } from "@lumeweb/portal-framework-ui"; import "@fontsource-variable/manrope"; import "@lumeweb/portal-framework-ui-core/tailwind.css"; +import "@lumeweb/advanced-rest-provider"; import React from "react"; import { env } from "./env"; diff --git a/libs/portal-plugin-billing/package.json b/libs/portal-plugin-billing/package.json new file mode 100644 index 000000000..3bfd2a58f --- /dev/null +++ b/libs/portal-plugin-billing/package.json @@ -0,0 +1,32 @@ +{ + "name": "@lumeweb/portal-plugin-billing", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "@lumeweb/advanced-rest-provider": "workspace:*", + "@lumeweb/portal-framework-auth": "workspace:*", + "@lumeweb/portal-framework-core": "workspace:*", + "@lumeweb/portal-framework-ui": "workspace:*", + "@lumeweb/portal-framework-ui-core": "workspace:*", + "@lumeweb/portal-plugin-dashboard": "workspace:*", + "@refinedev/core": "^4.47.0", + "@refinedev/react-router": "^2.0.2", + "@tanstack/react-query": "4.36.1", + "lucide-react": "^0.543.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "7.54.0", + "react-router": "7.5.2" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/libs/portal-plugin-billing/plugin.config.ts b/libs/portal-plugin-billing/plugin.config.ts new file mode 100644 index 000000000..8b127dd18 --- /dev/null +++ b/libs/portal-plugin-billing/plugin.config.ts @@ -0,0 +1,15 @@ +import type { PluginConfig } from "@lumeweb/portal-framework-core/vite"; + +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default { + dir: __dirname, + exposes: { + ".": "./src/index", + "./account/subscriptions": "./src/ui/routes/account.subscriptions", + }, + name: "core:billing", +} satisfies PluginConfig; diff --git a/libs/portal-plugin-billing/postcss.config.cjs b/libs/portal-plugin-billing/postcss.config.cjs new file mode 100644 index 000000000..bc0c7d88f --- /dev/null +++ b/libs/portal-plugin-billing/postcss.config.cjs @@ -0,0 +1 @@ +module.exports = require("@lumeweb/portal-framework-ui-core/postcss.config"); \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/capabilities/index.ts b/libs/portal-plugin-billing/src/capabilities/index.ts new file mode 100644 index 000000000..3a5eaf73d --- /dev/null +++ b/libs/portal-plugin-billing/src/capabilities/index.ts @@ -0,0 +1 @@ +export { Capability } from './refineConfig'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/capabilities/refineConfig.ts b/libs/portal-plugin-billing/src/capabilities/refineConfig.ts new file mode 100644 index 000000000..40115e0f2 --- /dev/null +++ b/libs/portal-plugin-billing/src/capabilities/refineConfig.ts @@ -0,0 +1,39 @@ +import type { RefineProps } from "@refinedev/core"; +import { + Framework, + mergeRefineConfig, + RefineConfigCapability, +} from "@lumeweb/portal-framework-core"; + +export class Capability implements RefineConfigCapability { + readonly id: string = "billing:refine-config"; + status; + readonly type = "core:refine-config"; + version: string; + #apiUrl: string; + dependencies = ["dashboard:refine-config"]; + + async destroy() {} + + getConfig(existing?: Partial) { + return mergeRefineConfig(existing, {}); + } + + async initialize(framework: Framework) { + // Get the dashboard refine capability + const dashboardCapability = await framework.getCapability< + RefineConfigCapability & { apiUrl: string } + >("dashboard:refine-config"); + + if (!dashboardCapability) { + throw new Error("Dashboard refine capability not found"); + } + + // Get the API URL from the dashboard capability + this.#apiUrl = dashboardCapability.apiUrl; + + if (!this.#apiUrl) { + throw new Error("API URL not found in dashboard capability"); + } + } +} diff --git a/libs/portal-plugin-billing/src/hooks/index.ts b/libs/portal-plugin-billing/src/hooks/index.ts new file mode 100644 index 000000000..ef86a9732 --- /dev/null +++ b/libs/portal-plugin-billing/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useSubscription'; +export * from './useCustomerPortal'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/hooks/useCustomerPortal.ts b/libs/portal-plugin-billing/src/hooks/useCustomerPortal.ts new file mode 100644 index 000000000..57efec010 --- /dev/null +++ b/libs/portal-plugin-billing/src/hooks/useCustomerPortal.ts @@ -0,0 +1,48 @@ +import { useCustomMutation, type HttpError } from "@refinedev/core"; +import type { CustomerPortalResponse } from "@/types/subscription"; +import type { UseCustomMutationProps } from "@refinedev/core"; +import { DATA_PROVIDER_NAME } from "@lumeweb/portal-framework-auth"; + +interface UseCustomerPortalConfig { + return_url?: string; + mutationOptions?: UseCustomMutationProps< + CustomerPortalResponse, + HttpError, + unknown + >["mutationOptions"]; +} + +interface UseCustomerPortalResult { + mutate: () => void; + isLoading: boolean; + data?: CustomerPortalResponse; + error?: HttpError | null; +} + +export function useCustomerPortal( + config: UseCustomerPortalConfig = {}, +): UseCustomerPortalResult { + const { return_url, mutationOptions } = config; + + const mutation = useCustomMutation({ + mutationOptions, + }); + + const mutate = () => { + mutation.mutate({ + url: "/account/billing/customer-portal", + method: "post", + values: { + return_url, + }, + dataProviderName: DATA_PROVIDER_NAME, + }); + }; + + return { + mutate, + isLoading: mutation.isLoading, + data: mutation.data?.data, + error: mutation.error, + }; +} diff --git a/libs/portal-plugin-billing/src/hooks/useSubscription.ts b/libs/portal-plugin-billing/src/hooks/useSubscription.ts new file mode 100644 index 000000000..cde94e1e7 --- /dev/null +++ b/libs/portal-plugin-billing/src/hooks/useSubscription.ts @@ -0,0 +1,28 @@ +import { useCustom } from "@refinedev/core"; +import type { SubscriptionStatusResponse } from "@/types/subscription"; +import type { UseCustomProps } from "@refinedev/core"; +import { DATA_PROVIDER_NAME } from "@lumeweb/portal-framework-auth"; + +interface UseSubscriptionConfig { + queryOptions?: UseCustomProps< + SubscriptionStatusResponse, + unknown, + unknown, + unknown, + SubscriptionStatusResponse + >["queryOptions"]; +} + +export function useSubscription(config: UseSubscriptionConfig = {}) { + const { queryOptions } = config; + + return useCustom({ + url: "/account/billing/subscription", + method: "get", + dataProviderName: DATA_PROVIDER_NAME, + queryOptions: { + // Apply user-provided query options last to allow overrides + ...queryOptions, + }, + }); +} diff --git a/libs/portal-plugin-billing/src/index.ts b/libs/portal-plugin-billing/src/index.ts new file mode 100644 index 000000000..5c27e65af --- /dev/null +++ b/libs/portal-plugin-billing/src/index.ts @@ -0,0 +1,26 @@ +export * from "./ui"; +export * from "./hooks"; +export * from "./types"; + +import { + createNamespacedId, + Framework, + type Plugin, +} from "@lumeweb/portal-framework-core"; +import { Capability as RefineConfigCapability } from "./capabilities"; +import routes from "./routes"; + +export default function (): Plugin { + return { + dependencies: [{ id: "core:dashboard" }], + capabilities: [new RefineConfigCapability()], + async destroy(_framework: Framework) { + console.log("Plugin Billing destroyed"); + }, + id: createNamespacedId("core", "billing"), + async initialize(_framework: Framework) { + console.log("Plugin Billing initialized"); + }, + routes, + } satisfies Plugin; +} diff --git a/libs/portal-plugin-billing/src/routes.tsx b/libs/portal-plugin-billing/src/routes.tsx new file mode 100644 index 000000000..9596a4869 --- /dev/null +++ b/libs/portal-plugin-billing/src/routes.tsx @@ -0,0 +1,16 @@ +import type { RouteDefinition } from "@lumeweb/portal-framework-core"; +import { CreditCard } from "lucide-react"; + +const routes = [ + { + component: "account/subscriptions", + id: "account_subscription", + navigation: { + icon: CreditCard, + label: "Subscription", + }, + path: "/subscription", + }, +] satisfies RouteDefinition[]; + +export default routes; diff --git a/libs/portal-plugin-billing/src/types/index.ts b/libs/portal-plugin-billing/src/types/index.ts new file mode 100644 index 000000000..431d72140 --- /dev/null +++ b/libs/portal-plugin-billing/src/types/index.ts @@ -0,0 +1 @@ +export * from './subscription'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/types/subscription.ts b/libs/portal-plugin-billing/src/types/subscription.ts new file mode 100644 index 000000000..ae31557e4 --- /dev/null +++ b/libs/portal-plugin-billing/src/types/subscription.ts @@ -0,0 +1,16 @@ +export interface SubscriptionStatusResponse { + is_subscribed: boolean; + gateway_type?: string; + plan_id?: number; + created_at?: string; + updated_at?: string; +} + +export interface CustomerPortalResponse { + url: string; +} + +export interface BillingPluginMeta { + stripe_pricing_table_id?: string; + stripe_publishable_key?: string; +} diff --git a/libs/portal-plugin-billing/src/ui/components/StripePricingTable.tsx b/libs/portal-plugin-billing/src/ui/components/StripePricingTable.tsx new file mode 100644 index 000000000..5504c5b70 --- /dev/null +++ b/libs/portal-plugin-billing/src/ui/components/StripePricingTable.tsx @@ -0,0 +1,45 @@ +import React from "react"; + +interface StripePricingTableProps { + pricingTableId: string; + publishableKey: string; + customerEmail?: string; + clientReferenceId?: string; +} + +declare global { + namespace JSX { + interface IntrinsicElements { + "stripe-pricing-table": React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + >; + } + } +} + +export const StripePricingTable: React.FC = ({ + pricingTableId, + publishableKey, + customerEmail, + clientReferenceId +}) => { + React.useEffect(() => { + const script = document.createElement("script"); + script.src = "https://js.stripe.com/v3/pricing-table.js"; + script.async = true; + document.body.appendChild(script); + return () => { + document.body.removeChild(script); + }; + }, []); + + return ( + + ); +}; diff --git a/libs/portal-plugin-billing/src/ui/components/index.ts b/libs/portal-plugin-billing/src/ui/components/index.ts new file mode 100644 index 000000000..650850304 --- /dev/null +++ b/libs/portal-plugin-billing/src/ui/components/index.ts @@ -0,0 +1 @@ +export * from './StripePricingTable'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/ui/index.ts b/libs/portal-plugin-billing/src/ui/index.ts new file mode 100644 index 000000000..72733dc5d --- /dev/null +++ b/libs/portal-plugin-billing/src/ui/index.ts @@ -0,0 +1,2 @@ +export * from './components'; +export * from './routes'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/src/ui/routes/account.subscriptions.tsx b/libs/portal-plugin-billing/src/ui/routes/account.subscriptions.tsx new file mode 100644 index 000000000..7dba11ec5 --- /dev/null +++ b/libs/portal-plugin-billing/src/ui/routes/account.subscriptions.tsx @@ -0,0 +1,221 @@ +import React from "react"; +import { + GeneralLayout, + PageHeader, + SkeletonLoader, + usePluginMeta, + withTheme, +} from "@lumeweb/portal-framework-ui"; +import { ExternalLink, Settings, ArrowRight } from "lucide-react"; +import { useSubscription } from "@/hooks/useSubscription"; +import { useCustomerPortal } from "@/hooks/useCustomerPortal"; +import { StripePricingTable } from "@/ui/components/StripePricingTable"; +import type { BillingPluginMeta } from "@/types/subscription"; +import { Authenticated, useGetIdentity } from "@refinedev/core"; +import type { Identity } from "@lumeweb/portal-framework-core"; +import { getCurrentLocation } from "@lumeweb/portal-framework-core"; +import { Button } from "@lumeweb/portal-framework-ui-core"; +import { Card, CardHeader, CardContent } from "@lumeweb/portal-framework-ui-core"; +import "@lumeweb/portal-framework-ui-core/tailwind-plugin.css"; + +const SubscriptionContainer = ({ children }: { children: React.ReactNode }) => ( +
+
+
{children}
+
+
+); + +const ActiveSubscriptionCard = ({ handleManageSubscription, isPortalLoading }: { + handleManageSubscription: () => void; + isPortalLoading: boolean; +}) => ( + +
+
+ +
+ +
+

Customer Portal

+

+ Access your secure customer portal to manage all aspects of your subscription. +

+
+ + + +
+
+ + + + Secure Access +
+
+ + + + Cancel Anytime +
+
+
+
+); + +function AccountSubscriptionsInner() { + const { data: subscriptionData, isLoading, error } = useSubscription(); + const { data: identity, isLoading: isIdentityLoading } = useGetIdentity(); + + // Get current URL for return_url + const returnUrl = getCurrentLocation().href; + + const { + mutate: createCustomerPortal, + isLoading: isPortalLoading, + data: portalData, + error: portalError, + } = useCustomerPortal({ return_url: returnUrl }); + + // Get Stripe configuration from plugin meta + const billingMeta = usePluginMeta("billing"); + + const handleManageSubscription = () => { + createCustomerPortal(); + }; + + // Handle portal response effect + React.useEffect(() => { + if (portalData?.url) { + // Redirect in the same window instead of opening a new tab + window.location.href = portalData.url; + } + }, [portalData]); + + // Handle portal error effect + React.useEffect(() => { + if (portalError) { + console.error("Failed to create customer portal session:", portalError); + } + }, [portalError]); + + // Show loading state + if (isLoading || isIdentityLoading) { + return ( +
+ +
+ ); + } + + // Show error state + if (error) { + return ( +
+
+

+ Error Loading Subscription +

+

+ Unable to load your subscription information. Please try again + later. +

+
+
+ ); + } + + const isSubscribed = subscriptionData?.data?.is_subscribed; + const hasStripeConfig = + billingMeta?.stripe_pricing_table_id && billingMeta?.stripe_publishable_key; + + return ( + <> + + + {/* Portal Button for Subscribed Users */} + {isSubscribed && ( + + + + )} + + {/* Pricing Table Embed Section - Only show if not subscribed and config is available */} + {!isSubscribed && hasStripeConfig && ( + + + +

+ Secure payment powered by Stripe +

+
+ + + +
+
+ )} + + {/* Fallback message when Stripe config is not available */} + {!isSubscribed && !hasStripeConfig && ( + + + +

+ Subscription Plans Unavailable +

+

+ Subscription plans are currently not available. Please contact + support for assistance. +

+
+
+
+ )} + + ); +} +function AccountSubscriptions() { + return ( + + + + + + ); +} + +export default withTheme(AccountSubscriptions); diff --git a/libs/portal-plugin-billing/src/ui/routes/index.ts b/libs/portal-plugin-billing/src/ui/routes/index.ts new file mode 100644 index 000000000..e336459cd --- /dev/null +++ b/libs/portal-plugin-billing/src/ui/routes/index.ts @@ -0,0 +1 @@ +export { default as AccountSubscriptions } from './account.subscriptions'; \ No newline at end of file diff --git a/libs/portal-plugin-billing/tailwind.config.ts b/libs/portal-plugin-billing/tailwind.config.ts new file mode 100644 index 000000000..f4aff7202 --- /dev/null +++ b/libs/portal-plugin-billing/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "tailwindcss"; + +import { tailwindSafelist, tailwindBlocklist } from "@lumeweb/portal-framework-ui-core/config/classlist"; +import baseConfig from "@lumeweb/portal-framework-ui-core/tailwind.config"; + +const config = { + ...baseConfig, + safelist: tailwindSafelist, + blocklist: tailwindBlocklist, + corePlugins: { + preflight: false, + }, + layers: { + base: false, + components: false, + utilities: false, + }, +} satisfies Config; + +export default config; diff --git a/libs/portal-plugin-billing/tsconfig.json b/libs/portal-plugin-billing/tsconfig.json new file mode 100644 index 000000000..218327782 --- /dev/null +++ b/libs/portal-plugin-billing/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/libs/portal-plugin-billing/vite.config.ts b/libs/portal-plugin-billing/vite.config.ts new file mode 100644 index 000000000..77f4e9e39 --- /dev/null +++ b/libs/portal-plugin-billing/vite.config.ts @@ -0,0 +1,13 @@ +import { Config } from "@lumeweb/portal-framework-core/vite"; + +import * as sharedModules from "../../shared-modules"; +import config from "./plugin.config"; + +export default Config({ + devPort: 4177, + dir: config.dir, + exposes: config.exposes, + name: config.name, + sharedModules: sharedModules.getSharedModules(), + type: "plugin", +}); diff --git a/libs/portal-plugin-dashboard/src-lib/index.ts b/libs/portal-plugin-dashboard/src-lib/index.ts index 69d5523c9..30fddb540 100644 --- a/libs/portal-plugin-dashboard/src-lib/index.ts +++ b/libs/portal-plugin-dashboard/src-lib/index.ts @@ -1,2 +1,4 @@ export * from "./types"; export * from "./util"; +export * from "./util/api"; +export * from "./util/dataProvider"; diff --git a/libs/portal-plugin-dashboard/src-lib/util/api.ts b/libs/portal-plugin-dashboard/src-lib/util/api.ts new file mode 100644 index 000000000..19f14afe4 --- /dev/null +++ b/libs/portal-plugin-dashboard/src-lib/util/api.ts @@ -0,0 +1,27 @@ +import { env, Framework, getApiBaseUrl, getPluginMeta } from "@lumeweb/portal-framework-core"; + +/** + * Resolves the dashboard API URL based on framework configuration + */ +export function resolveDashboardApiUrl(framework: Framework): string { + const apiUrl = getApiBaseUrl({ + currentUrl: framework.portalUrl, + preserveSubdomain: !env.VITE_PORTAL_DOMAIN_IS_ROOT, + }); + + if (!apiUrl) { + throw new Error("Failed to get API base URL"); + } + + const subdomain = getPluginMeta(framework.meta!, "dashboard", "subdomain"); + if (!subdomain) { + throw new Error("Failed to get subdomain from plugin metadata"); + } + + try { + const apiDomain = new URL(apiUrl); + return `${apiDomain.protocol}//${subdomain}.${apiDomain.hostname}/api`; + } catch (error) { + throw new Error(`Failed to construct API URL: ${error.message}`); + } +} \ No newline at end of file diff --git a/libs/portal-plugin-dashboard/src-lib/util/dataProvider.ts b/libs/portal-plugin-dashboard/src-lib/util/dataProvider.ts new file mode 100644 index 000000000..46be605d4 --- /dev/null +++ b/libs/portal-plugin-dashboard/src-lib/util/dataProvider.ts @@ -0,0 +1,61 @@ +import type { RefineProps } from "@refinedev/core"; +import dataProvider from "@lumeweb/advanced-rest-provider"; +import { + type AuthProviderWithEmitter, + DATA_PROVIDER_NAME, +} from "@lumeweb/portal-framework-auth"; +import { mergeRefineConfig } from "@lumeweb/portal-framework-core"; + +/** + * Resource configuration for refine data provider + */ +export type RefineResource = { + name: string; + meta?: any; +}; + +/** + * Creates and configures a data provider with the given API URL and auth configuration + */ +export function createDataProvider( + apiUrl: string, + existing?: Partial, +) { + const token = localStorage.getItem("jwt"); + const acctProvider = dataProvider(apiUrl, true); + + if (token) { + acctProvider.setAuthToken(token); + } + + const authProvider = existing?.authProvider as + | AuthProviderWithEmitter + | undefined; + if (authProvider) { + authProvider.on("authCheckSuccess", (params) => { + acctProvider.setAuthToken(params.token); + }); + } + + return acctProvider; +} + +/** + * Sets up a data provider with the given API URL and merges it into the refine config + * @param apiUrl - The API URL to use for the data provider + * @param existing - Existing refine config to merge with + * @param resources - Array of resource configurations to include + */ +export function setupDataProvider( + apiUrl: string, + existing?: Partial, + resources: RefineResource[] = [], +) { + const acctProvider = createDataProvider(apiUrl, existing); + + return mergeRefineConfig( + existing, + { [DATA_PROVIDER_NAME]: acctProvider }, + resources, + ); +} diff --git a/libs/portal-plugin-dashboard/src-lib/util/index.ts b/libs/portal-plugin-dashboard/src-lib/util/index.ts index 36a2423d0..b18f88925 100644 --- a/libs/portal-plugin-dashboard/src-lib/util/index.ts +++ b/libs/portal-plugin-dashboard/src-lib/util/index.ts @@ -1,3 +1,5 @@ +export * from "./api"; +export * from "./dataProvider"; export * from "./file"; export * from "./uppy"; export * from "./validation"; diff --git a/libs/portal-plugin-dashboard/src/capabilities/refineConfig.ts b/libs/portal-plugin-dashboard/src/capabilities/refineConfig.ts index 8abcd8feb..f4949a57d 100644 --- a/libs/portal-plugin-dashboard/src/capabilities/refineConfig.ts +++ b/libs/portal-plugin-dashboard/src/capabilities/refineConfig.ts @@ -1,18 +1,10 @@ import type { RefineProps } from "@refinedev/core"; - -import dataProvider from "@lumeweb/advanced-rest-provider"; -import { - type AuthProviderWithEmitter, - DATA_PROVIDER_NAME, -} from "@lumeweb/portal-framework-auth"; import { - env, Framework, - getApiBaseUrl, - getPluginMeta, - mergeRefineConfig, RefineConfigCapability, } from "@lumeweb/portal-framework-core"; +import { resolveDashboardApiUrl, setupDataProvider } from "@lib/util"; +import { DATA_PROVIDER_NAME } from "@lumeweb/portal-framework-auth"; export class Capability implements RefineConfigCapability { readonly id: string = "dashboard:refine-config"; @@ -23,24 +15,15 @@ export class Capability implements RefineConfigCapability { async destroy() {} - getConfig(existing?: Partial) { - const token = localStorage.getItem("jwt"); - const acctProvider = dataProvider(this.#apiUrl, true); - - if (token) { - acctProvider.setAuthToken(token); - } - - const authProvider = existing?.authProvider as - | AuthProviderWithEmitter - | undefined; - if (authProvider) { - authProvider.on("authCheckSuccess", (params) => { - acctProvider.setAuthToken(params.token); - }); - } + /** + * Gets the resolved API URL for the dashboard + */ + get apiUrl(): string { + return this.#apiUrl; + } - return mergeRefineConfig(existing, { [DATA_PROVIDER_NAME]: acctProvider }, [ + getConfig(existing?: Partial) { + const dashboardResources = [ { meta: { template: "/account" }, name: DATA_PROVIDER_NAME, @@ -66,29 +49,12 @@ export class Capability implements RefineConfigCapability { }, name: "operations.filters", }, - ]); + ]; + + return setupDataProvider(this.#apiUrl, existing, dashboardResources); } async initialize(framework: Framework) { - const apiUrl = getApiBaseUrl({ - currentUrl: framework.portalUrl, - preserveSubdomain: !env.VITE_PORTAL_DOMAIN_IS_ROOT, - }); - - if (!apiUrl) { - throw new Error("Failed to get API base URL"); - } - - const subdomain = getPluginMeta(framework.meta!, "dashboard", "subdomain"); - if (!subdomain) { - throw new Error("Failed to get subdomain from plugin metadata"); - } - - try { - const apiDomain = new URL(apiUrl); - this.#apiUrl = `${apiDomain.protocol}//${subdomain}.${apiDomain.hostname}/api`; - } catch (error) { - throw new Error(`Failed to construct API URL: ${error.message}`); - } + this.#apiUrl = resolveDashboardApiUrl(framework); } } diff --git a/package.json b/package.json index 5ed23c1eb..92464566c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build:lumeweb.com": "turbo build --filter=@lumeweb/lumeweb.com", "build:portal-app-shell": "turbo build --filter=@lumeweb/portal-app-shell", "build:portal-plugin-dashboard": "turbo build --filter=@lumeweb/portal-plugin-dashboard", + "build:portal-plugin-billing": "turbo build --filter=@lumeweb/portal-plugin-billing", "e2e:portal-plugin-dashboard": "turbo run e2e --filter=@lumeweb/portal-plugin-dashboard", "build:portal-plugin-core": "turbo build --filter=@lumeweb/portal-plugin-core", "build:portal-plugin-ipfs": "turbo build --filter=@lumeweb/portal-plugin-ipfs" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 446b01441..88b52de5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: '@fontsource-variable/manrope': specifier: ^5.2.5 version: 5.2.6 + '@lumeweb/advanced-rest-provider': + specifier: workspace:* + version: link:../../libs/advanced-rest '@lumeweb/portal-framework-core': specifier: workspace:* version: link:../../libs/portal-framework-core @@ -902,6 +905,51 @@ importers: specifier: 3.24.2 version: 3.24.2 + libs/portal-plugin-billing: + dependencies: + '@lumeweb/advanced-rest-provider': + specifier: workspace:* + version: link:../advanced-rest + '@lumeweb/portal-framework-auth': + specifier: workspace:* + version: link:../portal-framework-auth + '@lumeweb/portal-framework-core': + specifier: workspace:* + version: link:../portal-framework-core + '@lumeweb/portal-framework-ui': + specifier: workspace:* + version: link:../portal-framework-ui + '@lumeweb/portal-framework-ui-core': + specifier: workspace:* + version: link:../portal-framework-ui-core + '@lumeweb/portal-plugin-dashboard': + specifier: workspace:* + version: link:../portal-plugin-dashboard + '@refinedev/core': + specifier: ^4.47.0 + version: 4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) + '@refinedev/react-router': + specifier: ^2.0.2 + version: 2.0.2(@refinedev/core@4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router@7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) + lucide-react: + specifier: ^0.543.0 + version: 0.543.0(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: 7.54.0 + version: 7.54.0(react@18.3.1) + react-router: + specifier: 7.5.2 + version: 7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + libs/portal-plugin-core: dependencies: '@conform-to/react': @@ -1152,6 +1200,37 @@ importers: specifier: 3.24.2 version: 3.24.2 + libs/portal-plugin-template: + dependencies: + '@lumeweb/portal-framework-core': + specifier: workspace:* + version: link:../portal-framework-core + '@lumeweb/portal-framework-ui': + specifier: workspace:* + version: link:../portal-framework-ui + '@lumeweb/portal-framework-ui-core': + specifier: workspace:* + version: link:../portal-framework-ui-core + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-router: + specifier: 7.5.2 + version: 7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: 18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: 18.3.0 + version: 18.3.0 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + libs/portal-sdk: devDependencies: orval: @@ -3706,6 +3785,17 @@ packages: react-dom: 18.3.1 react-router: ^7.0.2 + '@refinedev/react-router@2.0.2': + resolution: {integrity: sha512-EERz4+wXGsBRkVpbRvFS/5echhmPqhF8968nSdA5EecnxFpOg3Auh2joEFoIS7AysxdYSR7rguQ0husirFBvJw==} + engines: {node: '>=20'} + peerDependencies: + '@refinedev/core': ^5.0.0 + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1 + react-router: ^7.0.2 + '@refinedev/react-table@5.6.17': resolution: {integrity: sha512-K30i5EqwaaxGOmT0hshNMNgyxbXsDZ9E7sGQnw4prNqCEgpWFrC/SBbDhQPvWoJaHakROLeVMmU6AFp9UHM4BA==} engines: {node: '>=10'} @@ -14151,6 +14241,16 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-router: 7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@refinedev/react-router@2.0.2(@refinedev/core@4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-router@7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@refinedev/core': 4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + qs: 6.14.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@refinedev/react-table@5.6.17(@refinedev/core@4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@refinedev/core': 4.57.10(patch_hash=dd77179754d9c0ec947ca9c6fff854acf119afb5b0fee50b1125f5d34f838104)(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) diff --git a/shared-modules.ts b/shared-modules.ts index 8ad32fc21..0617d41d0 100644 --- a/shared-modules.ts +++ b/shared-modules.ts @@ -4,6 +4,7 @@ const modules = [ "@lumeweb/portal-framework-core", "@lumeweb/portal-framework-ui", "@lumeweb/portal-framework-ui-core", + "@lumeweb/advanced-rest-provider", "@refinedev/core", "@tanstack/react-query", "react",