B2B-87: add company statistics consent (#29)

* B2B-87: add company statistics consent

* add toggle for company statistics consent under profile

* add toggle for company statistics consent under profile

* add audit logging to accounts

* change policy

* add audit logging to accounts

* remove full account data query and just query the entire account every time

* add comment about consent toggle

* make constants hardcoded, as dynamic ones do not work

* add back pending check

---------

Co-authored-by: Helena <helena@Helenas-MacBook-Pro.local>
This commit is contained in:
Helena
2025-07-03 17:55:23 +03:00
committed by GitHub
parent 517dce3146
commit ad08155063
18 changed files with 298 additions and 36 deletions

View File

@@ -16,6 +16,7 @@ import { Trans } from '@kit/ui/trans';
import { usePersonalAccountData } from '../../hooks/use-personal-account-data';
import { AccountDangerZone } from './account-danger-zone';
import ConsentToggle from './consent/consent-toggle';
import { UpdateEmailFormContainer } from './email/update-email-form-container';
import { MultiFactorAuthFactorsList } from './mfa/multi-factor-auth-list';
import { UpdatePasswordFormContainer } from './password/update-password-container';
@@ -150,6 +151,32 @@ export function PersonalAccountSettingsContainer(
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex justify-between">
<div>
<p className="font-medium">
<Trans
i18nKey={'account:consentToAnonymizedCompanyData.label'}
/>
</p>
<CardDescription>
<Trans
i18nKey={'account:consentToAnonymizedCompanyData.description'}
/>
</CardDescription>
</div>
<ConsentToggle
userId={props.userId}
initialState={
!!user.data.has_consent_anonymized_company_statistics
}
/>
</div>
</CardHeader>
</Card>
<If condition={props.features.enableAccountDeletion}>
<Card className={'border-destructive'}>
<CardHeader>

View File

@@ -0,0 +1,46 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { Switch } from '@kit/ui/switch';
import { Trans } from '@kit/ui/trans';
import { useRevalidatePersonalAccountDataQuery } from '../../../hooks/use-personal-account-data';
import { useUpdateAccountData } from '../../../hooks/use-update-account';
// This is temporary. When the profile views are ready, all account values included in the form will be updated together on form submit.
export default function ConsentToggle({
userId,
initialState,
}: {
userId: string;
initialState: boolean;
}) {
const [isConsent, setIsConsent] = useState(initialState);
const updateAccountMutation = useUpdateAccountData(userId);
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
const updateConsent = (consent: boolean) => {
const promise = updateAccountMutation
.mutateAsync({
has_consent_anonymized_company_statistics: consent,
})
.then(() => {
revalidateUserDataQuery(userId);
});
return toast.promise(() => promise, {
success: <Trans i18nKey={'account:updateConsentSuccess'} />,
error: <Trans i18nKey={'account:updateConsentError'} />,
loading: <Trans i18nKey={'account:updateConsentLoading'} />,
});
};
return (
<Switch
checked={isConsent}
onCheckedChange={setIsConsent}
onClick={() => updateConsent(!isConsent)}
disabled={updateAccountMutation.isPending}
/>
);
}

View File

@@ -22,14 +22,7 @@ export function usePersonalAccountData(
const response = await client
.from('accounts')
.select(
`
id,
name,
picture_url,
last_name
`,
)
.select()
.eq('primary_owner_user_id', userId)
.eq('is_personal_account', true)
.single();

View File

@@ -189,6 +189,7 @@ export type Database = {
created_at: string | null
created_by: string | null
email: string | null
has_consent_anonymized_company_statistics: boolean | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean
@@ -208,6 +209,7 @@ export type Database = {
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_anonymized_company_statistics?: boolean | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
@@ -227,6 +229,7 @@ export type Database = {
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_anonymized_company_statistics?: boolean | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
@@ -1498,6 +1501,7 @@ export type Database = {
created_at: string | null
created_by: string | null
email: string | null
has_consent_anonymized_company_statistics: boolean | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean

View File

@@ -1,3 +1,7 @@
export const SIDEBAR_WIDTH = '16rem';
export const SIDEBAR_WIDTH_MOBILE = '18rem';
export const SIDEBAR_WIDTH_ICON = '4rem';
export const SIDEBAR_WIDTH_PROPERTY = 'w-[16rem]';
export const SIDEBAR_WIDTH_MOBILE_PROPERTY = 'w-[18rem]';
export const SIDEBAR_WIDTH_ICON_PROPERTY = 'w-[4rem]';

View File

@@ -32,8 +32,16 @@ const DialogContent: React.FC<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
customClose?: React.JSX.Element;
preventAutoFocus?: boolean;
disableClose?: boolean;
}
> = ({ className, children, customClose, preventAutoFocus, ...props }) => (
> = ({
className,
children,
customClose,
disableClose,
preventAutoFocus,
...props
}) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@@ -47,16 +55,26 @@ const DialogContent: React.FC<
onCloseAutoFocus={
preventAutoFocus ? (e) => e.preventDefault() : props.onOpenAutoFocus
}
onInteractOutside={
disableClose ? (e) => e.preventDefault() : props.onInteractOutside
}
onPointerDownOutside={
disableClose ? (e) => e.preventDefault() : props.onPointerDownOutside
}
onEscapeKeyDown={
disableClose ? (e) => e.preventDefault() : props.onEscapeKeyDown
}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs transition-opacity hover:opacity-70 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
{customClose || (
<>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</>
)}
{!disableClose &&
(customClose || (
<>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</>
))}
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
@@ -114,13 +132,13 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};