Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/workflows/web-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,19 @@ jobs:
cat << EOF > "$HOME/.npmrc"
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
working-directory: ./web/lib
working-directory: ./web/sdk
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Bump Package Version
run: npm run bump-version
working-directory: ./web/lib
working-directory: ./web/sdk
env:
GIT_REFNAME: ${{ github.ref_name }}

- name: Run Semantic Release 🚀
run: npm run release:ci
working-directory: ./web/lib
working-directory: ./web/sdk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
315 changes: 54 additions & 261 deletions web/apps/admin/src/pages/organizations/details/index.tsx
Original file line number Diff line number Diff line change
@@ -1,272 +1,65 @@
import { useEffect, useMemo, useState } from "react";
import {
useQuery,
createConnectQueryKey,
useTransport,
} from "@connectrpc/connect-query";
import { Outlet, useParams } from "react-router-dom";
OrganizationDetails,
} from "@raystack/frontier/admin";
import { useCallback, useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { AppContext } from "~/contexts/App";
import { clients } from "~/connect/clients";
import { exportCsvFromStream } from "~/utils/helper";

const adminClient = clients.admin({ useBinary: true });

async function loadCountries(): Promise<string[]> {
const data = await import("~/assets/data/countries.json");
return (data.default as { name: string }[]).map((c) => c.name);
}

export function OrganizationDetailsPage() {
const { organizationId } = useParams<{ organizationId: string }>();
const { config } = useContext(AppContext);
const [countries, setCountries] = useState<string[]>([]);

import { OrganizationDetailsLayout } from "./layout";
import { ORG_NAMESPACE } from "./types";
import { OrganizationContext } from "./contexts/organization-context";
import {
FrontierServiceQueries,
GetOrganizationKycResponseSchema,
type Organization,
type User,
} from "@raystack/proton/frontier";
import { queryClient } from "~/contexts/ConnectProvider";
import { create } from "@bufbuild/protobuf";

export const OrganizationDetails = () => {
const [isSearchVisible, setIsSearchVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState("");

const { organizationId } = useParams();
const transport = useTransport();

// Use Connect RPC for fetching organization
const {
data: organization,
isLoading: isOrganizationLoading,
error: organizationError,
} = useQuery(
FrontierServiceQueries.getOrganization,
{ id: organizationId },
{
enabled: !!organizationId,
select: (data) => data?.organization,
},
);

const getOrganizationQueryKey = [
FrontierServiceQueries.getOrganization,
{ id: organizationId },
];

async function updateOrganization(org: Organization) {
queryClient.setQueryData(getOrganizationQueryKey, { organization: org });
}

// Fetch KYC details
const {
data: kycData,
isLoading: isKYCLoading,
error: kycError,
} = useQuery(
FrontierServiceQueries.getOrganizationKyc,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
},
);

const kycDetails = useMemo(() => kycData?.organizationKyc, [kycData]);
useEffect(() => {
loadCountries().then(setCountries);
}, []);

function updateKYCDetails(kyc: typeof kycDetails) {
const onExportMembers = useCallback(async () => {
if (!organizationId) return;
queryClient.setQueryData(
createConnectQueryKey({
schema: FrontierServiceQueries.getOrganizationKyc,
transport,
input: { orgId: organizationId },
cardinality: "finite",
}),
create(GetOrganizationKycResponseSchema, { organizationKyc: kyc }),
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
}
}, [organizationId]);

// Fetch default roles
const {
data: defaultRoles = [],
isLoading: isDefaultRolesLoading,
error: defaultRolesError,
} = useQuery(
FrontierServiceQueries.listRoles,
{ scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);

// Fetch organization-specific roles
const {
data: organizationRoles = [],
isLoading: isOrgRolesLoading,
error: orgRolesError,
} = useQuery(
FrontierServiceQueries.listOrganizationRoles,
{ orgId: organizationId || "", scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);

const roles = [...defaultRoles, ...organizationRoles];

// Fetch organization members
const {
data: orgMembersMap = {},
isLoading: isOrgMembersMapLoading,
error: orgMembersError,
} = useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => {
const users = data?.users || [];
return users.reduce(
(acc, user) => {
const id = user.id || "";
acc[id] = user;
return acc;
},
{} as Record<string, User>,
);
},
},
);

// Fetch billing accounts list
const { data: firstBillingAccountId = "", error: billingAccountsError } =
useQuery(
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);

// Fetch billing account details
const {
data: billingAccountData,
isLoading: isBillingAccountLoading,
error: billingAccountError,
refetch: fetchBillingAccountDetails,
} = useQuery(
FrontierServiceQueries.getBillingAccount,
{
orgId: organizationId || "",
id: firstBillingAccountId,
withBillingDetails: true,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => ({
billingAccount: data?.billingAccount,
billingAccountDetails: data?.billingDetails,
}),
},
);

const billingAccount = billingAccountData?.billingAccount;
const billingAccountDetails = billingAccountData?.billingAccountDetails;

// Fetch billing balance
const {
data: tokenBalance = "0",
isLoading: isTokenBalanceLoading,
error: tokenBalanceError,
refetch: fetchTokenBalance,
} = useQuery(
FrontierServiceQueries.getBillingBalance,
{
orgId: organizationId || "",
id: firstBillingAccountId,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => String(data?.balance?.amount || "0"),
},
);

// Error handling
useEffect(() => {
if (organizationError) {
console.error("Failed to fetch organization:", organizationError);
}
if (kycError) {
console.error("Failed to fetch KYC details:", kycError);
}
if (defaultRolesError) {
console.error("Failed to fetch default roles:", defaultRolesError);
}
if (orgRolesError) {
console.error("Failed to fetch organization roles:", orgRolesError);
}
if (orgMembersError) {
console.error("Failed to fetch organization members:", orgMembersError);
}
if (billingAccountsError) {
console.error("Failed to fetch billing accounts:", billingAccountsError);
}
if (billingAccountError) {
console.error(
"Failed to fetch billing account details:",
billingAccountError,
);
}
if (tokenBalanceError) {
console.error("Failed to fetch token balance:", tokenBalanceError);
}
}, [
organizationError,
kycError,
defaultRolesError,
orgRolesError,
orgMembersError,
billingAccountsError,
billingAccountError,
tokenBalanceError,
]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);
Comment on lines +26 to +51
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

Export callbacks have no error handling — failures are silent.

If exportCsvFromStream throws (network error, streaming failure, etc.), the promise rejection is unhandled. No user-facing feedback is shown.

🐛 Proposed fix for each callback (pattern shown for `onExportMembers`)
  const onExportMembers = useCallback(async () => {
    if (!organizationId) return;
-   await exportCsvFromStream(
-     adminClient.exportOrganizationUsers,
-     { id: organizationId },
-     "organization-members.csv",
-   );
+   try {
+     await exportCsvFromStream(
+       adminClient.exportOrganizationUsers,
+       { id: organizationId },
+       "organization-members.csv",
+     );
+   } catch (err) {
+     console.error("Failed to export members:", err);
+     // show toast or surface error to user
+   }
  }, [organizationId]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onExportMembers = useCallback(async () => {
if (!organizationId) return;
queryClient.setQueryData(
createConnectQueryKey({
schema: FrontierServiceQueries.getOrganizationKyc,
transport,
input: { orgId: organizationId },
cardinality: "finite",
}),
create(GetOrganizationKycResponseSchema, { organizationKyc: kyc }),
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
}
}, [organizationId]);
// Fetch default roles
const {
data: defaultRoles = [],
isLoading: isDefaultRolesLoading,
error: defaultRolesError,
} = useQuery(
FrontierServiceQueries.listRoles,
{ scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);
// Fetch organization-specific roles
const {
data: organizationRoles = [],
isLoading: isOrgRolesLoading,
error: orgRolesError,
} = useQuery(
FrontierServiceQueries.listOrganizationRoles,
{ orgId: organizationId || "", scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);
const roles = [...defaultRoles, ...organizationRoles];
// Fetch organization members
const {
data: orgMembersMap = {},
isLoading: isOrgMembersMapLoading,
error: orgMembersError,
} = useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => {
const users = data?.users || [];
return users.reduce(
(acc, user) => {
const id = user.id || "";
acc[id] = user;
return acc;
},
{} as Record<string, User>,
);
},
},
);
// Fetch billing accounts list
const { data: firstBillingAccountId = "", error: billingAccountsError } =
useQuery(
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);
// Fetch billing account details
const {
data: billingAccountData,
isLoading: isBillingAccountLoading,
error: billingAccountError,
refetch: fetchBillingAccountDetails,
} = useQuery(
FrontierServiceQueries.getBillingAccount,
{
orgId: organizationId || "",
id: firstBillingAccountId,
withBillingDetails: true,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => ({
billingAccount: data?.billingAccount,
billingAccountDetails: data?.billingDetails,
}),
},
);
const billingAccount = billingAccountData?.billingAccount;
const billingAccountDetails = billingAccountData?.billingAccountDetails;
// Fetch billing balance
const {
data: tokenBalance = "0",
isLoading: isTokenBalanceLoading,
error: tokenBalanceError,
refetch: fetchTokenBalance,
} = useQuery(
FrontierServiceQueries.getBillingBalance,
{
orgId: organizationId || "",
id: firstBillingAccountId,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => String(data?.balance?.amount || "0"),
},
);
// Error handling
useEffect(() => {
if (organizationError) {
console.error("Failed to fetch organization:", organizationError);
}
if (kycError) {
console.error("Failed to fetch KYC details:", kycError);
}
if (defaultRolesError) {
console.error("Failed to fetch default roles:", defaultRolesError);
}
if (orgRolesError) {
console.error("Failed to fetch organization roles:", orgRolesError);
}
if (orgMembersError) {
console.error("Failed to fetch organization members:", orgMembersError);
}
if (billingAccountsError) {
console.error("Failed to fetch billing accounts:", billingAccountsError);
}
if (billingAccountError) {
console.error(
"Failed to fetch billing account details:",
billingAccountError,
);
}
if (tokenBalanceError) {
console.error("Failed to fetch token balance:", tokenBalanceError);
}
}, [
organizationError,
kycError,
defaultRolesError,
orgRolesError,
orgMembersError,
billingAccountsError,
billingAccountError,
tokenBalanceError,
]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);
const onExportMembers = useCallback(async () => {
if (!organizationId) return;
try {
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
} catch (err) {
console.error("Failed to export members:", err);
// show toast or surface error to user
}
}, [organizationId]);
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);


const isLoading =
isOrganizationLoading ||
isDefaultRolesLoading ||
isOrgRolesLoading ||
isBillingAccountLoading;
return (
<OrganizationContext.Provider
value={{
organization: organization,
updateOrganization,
roles,
billingAccount,
billingAccountDetails,
isBillingAccountLoading,
fetchBillingAccountDetails,
tokenBalance,
isTokenBalanceLoading,
fetchTokenBalance,
orgMembersMap,
isOrgMembersMapLoading,
updateKYCDetails,
kycDetails,
isKYCLoading,
search: {
isVisible: isSearchVisible,
setVisibility: setIsSearchVisible,
query: searchQuery,
onChange: setSearchQuery,
},
}}
>
<OrganizationDetailsLayout
organization={organization}
isLoading={isLoading}
>
{organization?.id ? (
<Outlet
context={{
organization,
}}
/>
) : null}
</OrganizationDetailsLayout>
</OrganizationContext.Provider>
<OrganizationDetails
organizationId={organizationId}
appUrl={config?.app_url}
tokenProductId={config?.token_product_id}
countries={countries}
organizationTypes={config?.organization_types}
onExportMembers={onExportMembers}
onExportProjects={onExportProjects}
onExportTokens={onExportTokens}
/>
);
};
}
Loading
Loading