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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions apps/backend/src/BackendApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
NotificationSendingService,
NotificationsConfigurationService,
NotificationTokenService,
OrganizationBillingPort,
OrganizationLifecyclePort,
OrganizationMembershipSyncPort,
OrganizationMembershipWebhookPort,
OrganizationService,
Expand Down Expand Up @@ -637,10 +637,8 @@ const HealthCheckRoute = Layer.effectDiscard(
}),
);

export interface BackendRuntimeCapabilities {
readonly auditLogs: boolean;
readonly billing: boolean;
}
/** Feature-supplied capability flags advertised to clients, keyed by feature name. */
export type BackendRuntimeCapabilities = Readonly<Record<string, boolean>>;

const RuntimeCapabilitiesRoute = (capabilities: BackendRuntimeCapabilities) =>
Layer.effectDiscard(
Expand All @@ -658,7 +656,7 @@ const RuntimeCapabilitiesRoute = (capabilities: BackendRuntimeCapabilities) =>

export type BackendCoreFeatureServices =
| AuditLogPort
| OrganizationBillingPort
| OrganizationLifecyclePort
| OrganizationMembershipSyncPort
| OrganizationMembershipWebhookPort;

Expand Down Expand Up @@ -924,12 +922,12 @@ export interface BackendFeatureComposition<RFeatureRpcs extends Rpc.Any, RFeatur
export const NoBackendFeatures: BackendFeatureComposition<never, never> = {
group: RpcGroup.make(),
routes: () => Layer.empty,
runtimeCapabilities: { auditLogs: false, billing: false },
runtimeCapabilities: {},
services: () => Layer.empty,
supportServices: () =>
Layer.mergeAll(
AuditLogPort.noop,
OrganizationBillingPort.noop,
OrganizationLifecyclePort.noop,
OrganizationMembershipSyncPort.noop,
OrganizationMembershipWebhookPort.noop,
),
Expand Down Expand Up @@ -1067,10 +1065,10 @@ export const buildBackendFetch = <
// layer dependency — so `Layer.provide` does not discharge it (it would
// silently leak to the request handler and die with "Service not found").
// `HttpRouter.provideRequest` is the combinator that satisfies these
// request-scoped requirements. The Autumn webhook handler resolves `Db` +
// `BillingService` at request time (both part of the merged graph below);
// the Apple handler resolves the live public App Store service the same way;
// the Google handler additionally resolves its Pub/Sub OIDC verifier.
// request-scoped requirements. Feature-supplied webhook routes resolve their
// own services at request time the same way (all part of the merged graph
// below); the Apple handler resolves the live public App Store service; the
// Google handler additionally resolves its Pub/Sub OIDC verifier.
const WebhookRoutesLayer = Layer.mergeAll(
AppleServerToServerNotificationRouteLayer,
GooglePlayRtdnNotificationRouteLayer,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/rpc-smoke.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ describe("Backend runtime capabilities", () => {
);

expect(status).toBe(200);
expect(JSON.parse(text)).toEqual({ enterprise: { auditLogs: false, billing: false } });
expect(JSON.parse(text)).toEqual({ enterprise: {} });
});
});

Expand Down
1 change: 0 additions & 1 deletion apps/backend/src/testing/smoke-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const makeSmokeIds = (runId: string) => {
adminMemberId: `smk_mem_admin_${suffix}`,
adminUserId: `smk_admin_${suffix}`,
apiKeyId: `smk_api_key_${suffix}`,
billingId: `smk_billing_${suffix}`,
invitedEmail: `smoke-invite-${suffix}@example.test`,
invitedUserId: `smk_invite_${suffix}`,
normalEmail: `smoke-user-${suffix}@example.test`,
Expand Down
25 changes: 2 additions & 23 deletions apps/backend/src/testing/smoke-seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import {
apiKeys,
apikey,
auditLogs,
BillingSubscriptionStatus,
BillingTier,
captureProjectPolicies,
Db,
eq,
Expand All @@ -15,7 +13,6 @@ import {
invitation,
member,
organization,
organizationBilling,
paymentProviderConfigurationProducts,
paymentProviderConfigurations,
paywallLocationShowings,
Expand All @@ -33,8 +30,6 @@ import {
productPerks,
products,
projects,
usageAggregates,
usageRecords,
user,
webhookDeliveries,
webhookDeliveryAttempts,
Expand All @@ -46,8 +41,8 @@ import { makeSmokeIds } from "./smoke-ids.ts";

/**
* Deterministic fixture for the backend RPC smoke. Seeds an `admin`-role user, a
* normal user, their organization/memberships, a project, a seeded API key, and a
* billing row — everything the {@link rpcSmokeCases} manifest reads or scopes
* normal user, their organization/memberships, a project, and a seeded API key
* — everything the {@link rpcSmokeCases} manifest reads or scopes
* against. Kept separate from the lean shared `CoreTestFixture` because the smoke
* needs a richer, smoke-specific tenant; it still rides the same once-deployed
* stack + `testConnections` as the service-level integration tests.
Expand Down Expand Up @@ -186,11 +181,6 @@ export const resetSmokeData = (runId: string) =>
.where(eq(captureProjectPolicies.projectId, ids.projectId));
yield* db.delete(projects).where(eq(projects.id, ids.projectId));

yield* db.delete(usageRecords).where(eq(usageRecords.organizationId, ids.organizationId));
yield* db.delete(usageAggregates).where(eq(usageAggregates.organizationId, ids.organizationId));
yield* db
.delete(organizationBilling)
.where(eq(organizationBilling.organizationId, ids.organizationId));
yield* db.delete(invitation).where(eq(invitation.organizationId, ids.organizationId));
yield* db.delete(member).where(eq(member.organizationId, ids.organizationId));
yield* db.delete(organization).where(eq(organization.id, ids.organizationId));
Expand Down Expand Up @@ -296,15 +286,4 @@ export const seedSmokeData = (runId: string) =>
projectId: ids.projectId,
updatedAt: now,
});
yield* db.insert(organizationBilling).values({
billingProviderId: "smoke",
currentPeriodEnd: null,
currentPeriodStart: null,
externalCustomerId: null,
externalSubscriptionId: null,
id: ids.billingId,
organizationId: ids.organizationId,
subscriptionStatus: BillingSubscriptionStatus.None,
tier: BillingTier.Free,
});
});
12 changes: 0 additions & 12 deletions apps/www/scripts/pricing-preview/index.html

This file was deleted.

6 changes: 0 additions & 6 deletions apps/www/scripts/pricing-preview/main.tsx

This file was deleted.

16 changes: 0 additions & 16 deletions apps/www/scripts/pricing-preview/vite.config.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ const GENERAL_QUESTIONS = [
},
];

const BILLING_QUESTIONS = [
const SYNC_QUESTIONS = [
{
q: "What is the difference between Basic and Pro pricing tiers?",
a: "Basic includes budgeting, goal tracking, and up to 3 linked accounts. Pro adds unlimited accounts, dividend tracking, portfolio analysis, and priority support.",
q: "How often does data refresh?",
a: "Connected institutions sync every six hours, and you can pull a manual refresh from the account detail view at any time.",
},
{
q: "How do I cancel my subscription?",
a: "Go to Settings > Billing > Manage Plan and click Cancel. Your access continues until the end of your current billing period.",
q: "Why is a transaction missing?",
a: "Pending transactions appear once the institution posts them. If a posted transaction is still missing after 48 hours, reconnect the account from Settings.",
},
{
q: "Do you offer a free trial?",
a: "Yes. All new accounts start with a 14-day Pro trial. No credit card required.",
q: "Can I import a statement manually?",
a: "Yes. Upload a CSV or OFX file from the account detail view and map the columns once — the mapping is remembered for later imports.",
},
];

Expand Down Expand Up @@ -72,8 +72,8 @@ export function Faq() {
<TabsTrigger value="general" className="flex-1">
General
</TabsTrigger>
<TabsTrigger value="billing" className="flex-1">
Billing
<TabsTrigger value="sync" className="flex-1">
Sync
</TabsTrigger>
<TabsTrigger value="goals" className="flex-1">
Goals
Expand All @@ -82,8 +82,8 @@ export function Faq() {
<TabsContent value="general">
<QuestionList questions={GENERAL_QUESTIONS} />
</TabsContent>
<TabsContent value="billing">
<QuestionList questions={BILLING_QUESTIONS} />
<TabsContent value="sync">
<QuestionList questions={SYNC_QUESTIONS} />
</TabsContent>
<TabsContent value="goals">
<QuestionList questions={GOALS_QUESTIONS} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,13 @@ export function SidebarNav() {
<SidebarMenuItem>
<SidebarMenuButton isActive>
<IconPlaceholder
lucide="CreditCardIcon"
tabler="IconCreditCard"
hugeicons="CreditCardIcon"
phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
lucide="UsersIcon"
tabler="IconUsers"
hugeicons="UserMultipleIcon"
phosphor="UsersIcon"
remixicon="RiTeamLine"
/>
Billing
Members
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Create organizations and projects, list the projects in an organiza
---

Organizations and projects are the account-level containers in Voidhash. An **organization** groups
your team and billing; a **project** lives inside an organization and owns everything else —
your team; a **project** lives inside an organization and owns everything else —
products, perks, paywalls, API keys, and persons. The endpoints on this page manage those
containers and the user account itself.

Expand Down
27 changes: 27 additions & 0 deletions apps/www/src/features/studio/enterprise/organization-nav-slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { LucideIcon } from "lucide-react";

import type { RuntimeCapabilities } from "./runtime-capabilities";

/** A single sidebar entry, matching the item shape `SidebarShell` renders. */
export interface OrganizationNavItem {
title: string;
url: string;
icon?: LucideIcon;
isActive?: () => boolean;
}

export interface OrganizationNavSlotContext {
readonly capabilities: RuntimeCapabilities | undefined;
readonly organizationSlug: string;
readonly pathname: string;
}

/**
* Community extension slot for host-provided organization settings entries.
* Hosts replace this module to append their own nav items.
*/
export function organizationSettingsNavItems(
_context: OrganizationNavSlotContext,
): OrganizationNavItem[] {
return [];
}
33 changes: 19 additions & 14 deletions apps/www/src/features/studio/enterprise/runtime-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { useQuery } from "@tanstack/react-query";

import { env } from "@/lib/env";

const disabledCapabilities = {
enterprise: { auditLogs: false, billing: false },
} as const;
/** Enabled enterprise capability ids advertised by the host composition. */
export type EnterpriseCapabilities = Readonly<Record<string, boolean>>;

const loadRuntimeCapabilities = async () => {
export interface RuntimeCapabilities {
readonly enterprise: EnterpriseCapabilities;
}

const disabledCapabilities: RuntimeCapabilities = {
enterprise: {},
};

const loadRuntimeCapabilities = async (): Promise<RuntimeCapabilities> => {
try {
const apiBaseUrl = env.VITE_APP_API_URL.replace(/\/+$/, "");
const response = await fetch(`${apiBaseUrl}/api/runtime-capabilities`, {
Expand All @@ -15,17 +22,15 @@ const loadRuntimeCapabilities = async () => {
if (!response.ok) return disabledCapabilities;

const body = (await response.json()) as {
readonly enterprise?: {
readonly auditLogs?: unknown;
readonly billing?: unknown;
};
};
return {
enterprise: {
auditLogs: body.enterprise?.auditLogs === true,
billing: body.enterprise?.billing === true,
},
readonly enterprise?: Readonly<Record<string, unknown>>;
};
const enterprise: Record<string, boolean> = {};
for (const [capability, enabled] of Object.entries(body.enterprise ?? {})) {
if (enabled === true) {
enterprise[capability] = true;
}
}
return { enterprise };
} catch {
return disabledCapabilities;
}
Expand Down
Loading
Loading