Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .git-town.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# See https://www.git-town.com/configuration-file for details

[branches]
main = "develop"
1 change: 1 addition & 0 deletions apps/portal-app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions apps/portal-app-shell/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Side‑effect import — ensure it won’t be treeshaken.

If @lumeweb/advanced-rest-provider must run at import time, verify its package.json has sideEffects true (or lists the entry) so bundlers keep it. Otherwise, expose an explicit init and call it here.


🏁 Script executed:

#!/bin/bash
# Find the workspace package.json and print its sideEffects field
PKG=$(rg -n '"name":\s*"@lumeweb/advanced-rest-provider"' -g '**/package.json' | cut -d: -f1 | head -n1)
echo "Package file: $PKG"
jq '.sideEffects // "UNSET"' "$PKG"

Length of output: 53


Side‑effect import will be tree‑shaken — add sideEffects declaration or use explicit init.

Verified: @lumeweb/advanced-rest-provider (libs/advanced-rest/package.json) has no sideEffects field. Bundlers will remove this import during build.

Fix one of:

  1. Add "sideEffects": true (or list the entry point) to libs/advanced-rest/package.json
  2. Replace the side-effect import with an explicit initAdvancedRestProvider() call (or equivalent) in this file and export that function for tree-shaking safety
🤖 Prompt for AI Agents
In apps/portal-app-shell/src/App.tsx around line 4, the side-effect import
import "@lumeweb/advanced-rest-provider"; can be tree‑shaken because
libs/advanced-rest/package.json has no sideEffects field; either add
"sideEffects": true (or list the specific entry file path) to
libs/advanced-rest/package.json to prevent removal, or replace the side‑effect
import with an explicit exported initializer from the package (e.g.
export/initAdvancedRestProvider) and call that initializer here so the import is
live and tree‑shake safe; pick one approach and apply it consistently (update
package.json for the package if choosing option 1, or change this file and the
package to expose/init the provider function if choosing option 2).

import React from "react";

import { env } from "./env";
Expand Down
32 changes: 32 additions & 0 deletions libs/portal-plugin-billing/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
15 changes: 15 additions & 0 deletions libs/portal-plugin-billing/plugin.config.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions libs/portal-plugin-billing/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@lumeweb/portal-framework-ui-core/postcss.config");
1 change: 1 addition & 0 deletions libs/portal-plugin-billing/src/capabilities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Capability } from './refineConfig';
39 changes: 39 additions & 0 deletions libs/portal-plugin-billing/src/capabilities/refineConfig.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Unused private field #apiUrl.

The #apiUrl field is set during initialization (line 33) but never read or used elsewhere in the class. This suggests either incomplete implementation or unnecessary code.

Looking at the dashboard capability (see relevant code snippets), it uses setupDataProvider(this.#apiUrl, existing, dashboardResources) in its getConfig method. Should the billing capability be doing something similar?

Run this script to check if billing needs its own data provider setup:


🏁 Script executed:

#!/bin/bash
# Search for billing-specific API endpoints or data provider usage
rg -nP --type=ts -C3 'billing.*api|data.*provider.*billing' -g '!node_modules'

Length of output: 0


I need to investigate the codebase more thoroughly since the initial pattern search yielded no results. Let me examine the file structure and look for actual usage patterns.


---

🏁 Script executed:

```shell
#!/bin/bash
# View the refineConfig.ts file to understand the class structure and #apiUrl usage
fd -t f "refineConfig.ts" && cat "$(fd -t f 'refineConfig.ts' | head -1)"

Length of output: 1526


🏁 Script executed:

#!/bin/bash
# Search for setupDataProvider usage across the codebase
rg -nP --type=ts 'setupDataProvider' -C5 -g '!node_modules'

Length of output: 3208


🏁 Script executed:

#!/bin/bash
# Search for any usage or reference to #apiUrl in the codebase
rg -nP --type=ts '#apiUrl' -C3 -g '!node_modules'

Length of output: 11437


🏁 Script executed:

#!/bin/bash
# Look for dashboard capability file to understand the pattern
fd -t f "dashboard.*capability\|.*dashboard.*config" --type f | grep -E '\.(ts|js)$' | head -10

Length of output: 0


I need to see the complete billing capability file to verify if #apiUrl is actually used in the getConfig() method.


---

🏁 Script executed:

```shell
#!/bin/bash
# View the complete billing refineConfig.ts file
cat libs/portal-plugin-billing/src/capabilities/refineConfig.ts

Length of output: 1067


Remove unused #apiUrl field or implement data provider setup.

The #apiUrl field is set during initialization (lines 32-37) but never used. The getConfig() method returns mergeRefineConfig(existing, {}) without using it. Other plugins (dashboard, IPFS, core) follow a pattern where #apiUrl is used inside getConfig() to set up data providers—billing should either implement similar functionality or remove the unused field entirely.

🤖 Prompt for AI Agents
In libs/portal-plugin-billing/src/capabilities/refineConfig.ts around line 13,
the private field `#apiUrl` is declared and set during initialization but never
used; either remove the unused `#apiUrl` declaration and its initialization
code, or implement the same data-provider merge pattern used in other plugins:
update getConfig() to call mergeRefineConfig(existing, { dataProvider:
createDataProvider(this.#apiUrl) }) (or the billing-specific provider), ensure
the provider factory is imported/available, and remove dead imports if you
choose to delete the field.

dependencies = ["dashboard:refine-config"];

async destroy() {}

getConfig(existing?: Partial<RefineProps>) {
return mergeRefineConfig(existing, {});
}
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Empty configuration passed to mergeRefineConfig.

The getConfig method passes an empty object {} to mergeRefineConfig, which provides no custom data providers or resources. This seems unusual for a capability that retrieves and stores an API URL.

Consider whether billing-specific resources or data providers should be configured here, similar to how the dashboard capability uses setupDataProvider.

🤖 Prompt for AI Agents
In libs/portal-plugin-billing/src/capabilities/refineConfig.ts around lines 18
to 20, getConfig currently calls mergeRefineConfig(existing, {}) which passes an
empty config; replace that second argument with a billing-aware config that
wires up the billing data provider(s)/resources and persists the API URL
(similar to dashboard’s setupDataProvider usage). Concretely, construct and pass
a config object that invokes setupDataProvider or equivalent to register the
billing API URL and any billing resources (resources, dataProvider, authProvider
as needed) before calling mergeRefineConfig(existing, billingConfig).


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");
}
}
}
2 changes: 2 additions & 0 deletions libs/portal-plugin-billing/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useSubscription';
export * from './useCustomerPortal';
48 changes: 48 additions & 0 deletions libs/portal-plugin-billing/src/hooks/useCustomerPortal.ts
Original file line number Diff line number Diff line change
@@ -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<CustomerPortalResponse>({
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,
};
}
28 changes: 28 additions & 0 deletions libs/portal-plugin-billing/src/hooks/useSubscription.ts
Original file line number Diff line number Diff line change
@@ -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<SubscriptionStatusResponse>({
url: "/account/billing/subscription",
method: "get",
dataProviderName: DATA_PROVIDER_NAME,
queryOptions: {
// Apply user-provided query options last to allow overrides
...queryOptions,
},
});
}
26 changes: 26 additions & 0 deletions libs/portal-plugin-billing/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions libs/portal-plugin-billing/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions libs/portal-plugin-billing/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './subscription';
16 changes: 16 additions & 0 deletions libs/portal-plugin-billing/src/types/subscription.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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>,
HTMLElement
>;
}
}
}

export const StripePricingTable: React.FC<StripePricingTableProps> = ({
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);
};
}, []);
Comment on lines +27 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix script cleanup to prevent breaking other component instances.

Removing the script element on unmount will break other mounted instances of StripePricingTable. If multiple instances exist or the component remounts quickly, the script removal will cause failures.

Apply this diff to fix the cleanup logic:

 export const StripePricingTable: React.FC<StripePricingTableProps> = ({
   pricingTableId,
   publishableKey,
   customerEmail,
   clientReferenceId
 }) => {
   React.useEffect(() => {
+    // Check if script already exists
+    const existingScript = document.querySelector(
+      'script[src="https://js.stripe.com/v3/pricing-table.js"]'
+    );
+    
+    if (existingScript) {
+      return; // Script already loaded, no cleanup needed
+    }
+
     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);
-    };
+    
+    // Don't remove the script on unmount since it may be used by other instances
+    // The script is safe to leave in the DOM
   }, []);

   return (
     <stripe-pricing-table
       pricing-table-id={pricingTableId}
       publishable-key={publishableKey}
       {...(customerEmail && { "customer-email": customerEmail })}
       {...(clientReferenceId && { "client-reference-id": clientReferenceId })}
     />
   );
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libs/portal-plugin-billing/src/ui/components/StripePricingTable.tsx around
lines 27 to 35, the effect always removes the injected Stripe script on unmount
which breaks other mounted instances; change the logic so the component only
removes the script if it was the one that added it (or no other instances
remain). Implement this by checking for an existing script before creating one
(query document for the Stripe script URL), set a marker (e.g.,
script.dataset.addedBy = "StripePricingTable") or maintain a module-level
ref/count of active instances and increment on mount/decrement on unmount, and
only call document.body.removeChild(script) when the count reaches zero or the
dataset marker shows this instance added it; ensure you still append the script
when missing and preserve async attribute.

⚠️ Potential issue | 🟠 Major

Add error handling for script loading failures.

The component doesn't handle cases where the Stripe script fails to load (network errors, CSP violations, etc.), which could result in a broken UI with no feedback to the user.

Consider adding error handling:

 export const StripePricingTable: React.FC<StripePricingTableProps> = ({
   pricingTableId,
   publishableKey,
   customerEmail,
   clientReferenceId
 }) => {
+  const [error, setError] = React.useState<string | null>(null);
+
   React.useEffect(() => {
     const existingScript = document.querySelector(
       'script[src="https://js.stripe.com/v3/pricing-table.js"]'
     );
     
     if (existingScript) {
       return;
     }

     const script = document.createElement("script");
     script.src = "https://js.stripe.com/v3/pricing-table.js";
     script.async = true;
+    
+    script.onerror = () => {
+      setError("Failed to load Stripe pricing table. Please refresh the page.");
+    };
+    
     document.body.appendChild(script);
   }, []);

+  if (error) {
+    return <div className="text-red-600">{error}</div>;
+  }
+
   return (
     <stripe-pricing-table
       pricing-table-id={pricingTableId}
       publishable-key={publishableKey}
       {...(customerEmail && { "customer-email": customerEmail })}
       {...(clientReferenceId && { "client-reference-id": clientReferenceId })}
     />
   );
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libs/portal-plugin-billing/src/ui/components/StripePricingTable.tsx around
lines 27 to 35, the effect that injects the Stripe pricing-table script lacks
error handling so failures (network/CSP) leave the UI broken; modify the effect
to add script.onerror and script.onload handlers: set component state (e.g.,
loading, error) on those callbacks, show a user-friendly fallback or error
message when onerror fires, ensure you also handle timeout cases if needed, and
remove the event handlers during cleanup before removing the script to avoid
leaks.


return (
<stripe-pricing-table
pricing-table-id={pricingTableId}
publishable-key={publishableKey}
{...(customerEmail && { "customer-email": customerEmail })}
{...(clientReferenceId && { "client-reference-id": clientReferenceId })}
/>
);
};
1 change: 1 addition & 0 deletions libs/portal-plugin-billing/src/ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './StripePricingTable';
2 changes: 2 additions & 0 deletions libs/portal-plugin-billing/src/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './components';
export * from './routes';
Loading
Loading