Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multi tenancy subscriptions routing #539

Merged
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PageHeadline } from '@sb/webapp-core/components/pageHeadline';
import { PageLayout } from '@sb/webapp-core/components/pageLayout';
import { useGenerateLocalePath } from '@sb/webapp-core/hooks';
import { useToast } from '@sb/webapp-core/toast/useToast';
import { useGenerateTenantPath } from '@sb/webapp-tenants/hooks';
import { Elements } from '@stripe/react-stripe-js';
import { FormattedMessage, useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
Expand All @@ -14,7 +14,7 @@ export const EditPaymentMethod = () => {
const intl = useIntl();
const { toast } = useToast();
const navigate = useNavigate();
const generateLocalePath = useGenerateLocalePath();
const generateTenantPath = useGenerateTenantPath();

const successMessage = intl.formatMessage({
defaultMessage: 'Payment method changed successfully',
Expand All @@ -34,7 +34,7 @@ export const EditPaymentMethod = () => {
<Elements stripe={stripePromise} options={{ locale: 'en' }}>
<EditPaymentMethodForm
onSuccess={() => {
navigate(generateLocalePath(RoutesConfig.subscriptions.index));
navigate(generateTenantPath(RoutesConfig.subscriptions.index));
toast({ description: successMessage });
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const Subscriptions = () => {
return (
<PageLayout>
<PageHeadline
header={<FormattedMessage defaultMessage="My subscription" id="My subscription / Header" />}
header={<FormattedMessage defaultMessage="Subscription plan" id="Tenant subscription plan / Header" />}
subheader={
<FormattedMessage
defaultMessage="An example of a subscription management page powered by Stripe. You can select a subscription plan, add a payment method, and view payment history."
id="My subscription / Subheading"
id="Tenant subscription plan / Subheading"
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TenantUserRole } from '@sb/webapp-api-client';
import { currentUserFactory, fillCommonQueryWithUser } from '@sb/webapp-api-client/tests/factories';
import { screen } from '@testing-library/react';

import { membershipFactory, tenantFactory } from '../../../tests/factories/tenant';
import { PLACEHOLDER_CONTENT, PLACEHOLDER_TEST_ID, render } from '../../../tests/utils/rendering';
import { RoleAccessProps, TenantRoleAccess } from '../tenantRoleAccess.component';

describe('TenantRoleAccess: Component', () => {
const defaultProps: RoleAccessProps = {
allowedRoles: [TenantUserRole.ADMIN],
children: PLACEHOLDER_CONTENT,
};

const Component = (props: Partial<RoleAccessProps>) => <TenantRoleAccess {...defaultProps} {...props} />;

const createUser = (role: TenantUserRole) => {
const currentUserMembership = membershipFactory({ role });
const tenant = tenantFactory({ membership: currentUserMembership });
return currentUserFactory({ tenants: [tenant] });
};

it('should render children if user has allowed role', async () => {
const user = createUser(TenantUserRole.ADMIN);
const apolloMocks = [fillCommonQueryWithUser(user)];
render(<Component allowedRoles={TenantUserRole.ADMIN} />, { apolloMocks });
expect(await screen.findByTestId(PLACEHOLDER_TEST_ID)).toBeInTheDocument();
});

it('should render nothing if user doesnt have allowed role', async () => {
const user = createUser(TenantUserRole.MEMBER);
const apolloMocks = [fillCommonQueryWithUser(user)];
const { waitForApolloMocks } = render(<Component allowedRoles={TenantUserRole.ADMIN} />, { apolloMocks });
await waitForApolloMocks();
expect(screen.queryByTestId(PLACEHOLDER_TEST_ID)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TenantRoleAccess } from './tenantRoleAccess.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TenantUserRole } from '@sb/webapp-api-client';
import { ReactNode } from 'react';

import { useTenantRoleAccessCheck } from '../../hooks';

export type RoleAccessProps = {
children: ReactNode;
allowedRoles?: TenantUserRole | TenantUserRole[];
};

export const TenantRoleAccess = ({ children, allowedRoles }: RoleAccessProps) => {
const { isAllowed } = useTenantRoleAccessCheck(allowedRoles);
return <>{isAllowed ? children : null}</>;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PageHeadline } from '@sb/webapp-core/components/pageHeadline';
import { PageLayout } from '@sb/webapp-core/components/pageLayout';
import { Tabs, TabsList, TabsTrigger } from '@sb/webapp-core/components/tabs';
import { RoutesConfig as FinancesRoutesConfig } from '@sb/webapp-finances/config/routes';
import { FormattedMessage } from 'react-intl';
import { Link, Outlet, useLocation } from 'react-router-dom';

Expand Down Expand Up @@ -29,6 +30,11 @@ export const TenantSettings = () => {
<FormattedMessage defaultMessage="General" id="Tenant settings / General" />
</TabsTrigger>
</Link>
<Link to={generateTenantPath(FinancesRoutesConfig.subscriptions.index)}>
<TabsTrigger value={generateTenantPath(FinancesRoutesConfig.subscriptions.index)}>
<FormattedMessage defaultMessage="Subscription" id="Tenant settings / Subscription" />
</TabsTrigger>
</Link>
</TabsList>

<Outlet />
Expand Down
29 changes: 14 additions & 15 deletions packages/webapp/src/app/app.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,22 @@ export const App = () => {
<Route path={RoutesConfig.tenant.settings.members} element={<TenantMembers />} />
<Route path={RoutesConfig.tenant.settings.general} element={<TenantGeneralSettings />} />
</Route>
</Route>

<Route element={<ActiveSubscriptionContext />}>
<Route element={<Subscriptions />}>
<Route index path={RoutesConfig.subscriptions.index} element={<CurrentSubscriptionContent />} />
<Route path={RoutesConfig.subscriptions.paymentMethods.index} element={<PaymentMethodContent />} />
<Route
path={RoutesConfig.subscriptions.transactionHistory.index}
element={<TransactionsHistoryContent />}
/>
<Route element={<ActiveSubscriptionContext />}>
<Route element={<Subscriptions />}>
<Route index path={RoutesConfig.subscriptions.index} element={<CurrentSubscriptionContent />} />
<Route path={RoutesConfig.subscriptions.paymentMethods.index} element={<PaymentMethodContent />} />
<Route
path={RoutesConfig.subscriptions.transactionHistory.index}
element={<TransactionsHistoryContent />}
/>
</Route>
<Route path={RoutesConfig.subscriptions.currentSubscription.edit} element={<EditSubscription />} />
<Route path={RoutesConfig.subscriptions.currentSubscription.cancel} element={<CancelSubscription />} />
<Route path={RoutesConfig.subscriptions.paymentMethods.edit} element={<EditPaymentMethod />} />
</Route>
<Route path={RoutesConfig.subscriptions.currentSubscription.edit} element={<EditSubscription />} />
<Route path={RoutesConfig.subscriptions.currentSubscription.cancel} element={<CancelSubscription />} />
<Route path={RoutesConfig.subscriptions.paymentMethods.edit} element={<EditPaymentMethod />} />
<Route path={RoutesConfig.finances.paymentConfirm} element={<PaymentConfirm />} />
<Route path={RoutesConfig.subscriptions.transactionHistory.history} element={<TransactionHistory />} />
</Route>
<Route path={RoutesConfig.finances.paymentConfirm} element={<PaymentConfirm />} />
<Route path={RoutesConfig.subscriptions.transactionHistory.history} element={<TransactionHistory />} />
<Route path={RoutesConfig.demoItems} element={<DemoItems />} />
<Route path={RoutesConfig.demoItem} element={<DemoItem routesConfig={RoutesConfig} />} />
<Route path={RoutesConfig.crudDemoItem.index} element={<CrudDemoItem routesConfig={RoutesConfig} />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { TenantUserRole } from '@sb/webapp-api-client';
import { Alert, AlertDescription, AlertTitle } from '@sb/webapp-core/components/alert';
import { Link } from '@sb/webapp-core/components/buttons';
import { buttonVariants } from '@sb/webapp-core/components/buttons/button/button.styles';
import { Separator } from '@sb/webapp-core/components/separator';
import { useGenerateLocalePath, useMediaQuery } from '@sb/webapp-core/hooks';
import { cn } from '@sb/webapp-core/lib/utils';
import { media } from '@sb/webapp-core/theme';
import { TenantRoleAccess } from '@sb/webapp-tenants/components/tenantRoleAccess';
import { useGenerateTenantPath } from '@sb/webapp-tenants/hooks';
import { X } from 'lucide-react';
import { HTMLAttributes, useCallback, useContext } from 'react';
Expand Down Expand Up @@ -94,7 +96,7 @@ export const Sidebar = (props: HTMLAttributes<HTMLDivElement>) => {
</Link>
</RoleAccess>

<RoleAccess>
<TenantRoleAccess allowedRoles={[TenantUserRole.OWNER, TenantUserRole.ADMIN]}>
<Link
className={menuItemClassName}
to={generateTenantPath(RoutesConfig.finances.paymentConfirm)}
Expand All @@ -103,9 +105,9 @@ export const Sidebar = (props: HTMLAttributes<HTMLDivElement>) => {
>
<FormattedMessage defaultMessage="Payments" id="Home / payments link" />
</Link>
</RoleAccess>
</TenantRoleAccess>

<RoleAccess>
<TenantRoleAccess allowedRoles={[TenantUserRole.OWNER, TenantUserRole.ADMIN]}>
<Link
className={menuItemClassName}
to={generateTenantPath(RoutesConfig.subscriptions.index)}
Expand All @@ -114,7 +116,7 @@ export const Sidebar = (props: HTMLAttributes<HTMLDivElement>) => {
>
<FormattedMessage defaultMessage="Subscriptions" id="Home / subscriptions link" />
</Link>
</RoleAccess>
</TenantRoleAccess>

<RoleAccess>
<Link
Expand Down Expand Up @@ -171,6 +173,17 @@ export const Sidebar = (props: HTMLAttributes<HTMLDivElement>) => {
</Link>
</RoleAccess>

<TenantRoleAccess allowedRoles={[TenantUserRole.OWNER, TenantUserRole.ADMIN]}>
<Link
className={menuItemClassName}
to={generateTenantPath(RoutesConfig.tenant.settings.members)}
onClick={closeSidebar}
navLink
>
<FormattedMessage defaultMessage="Organization settings" id="Home / organization settings" />
</Link>
</TenantRoleAccess>

<p className="my-2 ml-2 mt-4 text-sm text-muted-foreground">
<FormattedMessage defaultMessage="Static pages" id="Sidebar / static pages" />
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { screen } from '@testing-library/react';

import { Role } from '../../../../modules/auth/auth.types';
import { PLACEHOLDER_CONTENT, PLACEHOLDER_TEST_ID, render } from '../../../../tests/utils/rendering';
import { RoleAccess, RoleAccessProps } from '../roleAccess.component';
import { RoleAccess, TenantRoleAccessProps } from '../roleAccess.component';

describe('RoleAccess: Component', () => {
const defaultProps: RoleAccessProps = {
const defaultProps: TenantRoleAccessProps = {
allowedRoles: [Role.ADMIN],
children: PLACEHOLDER_CONTENT,
};

const Component = (props: Partial<RoleAccessProps>) => <RoleAccess {...defaultProps} {...props} />;
const Component = (props: Partial<TenantRoleAccessProps>) => <RoleAccess {...defaultProps} {...props} />;

it('should render children if user has allowed role', async () => {
const apolloMocks = [fillCommonQueryWithUser(currentUserFactory({ roles: [Role.ADMIN] }))];
Expand Down
Loading