@oxy.so/services 11.9.0 → 11.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/assets/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
- package/lib/commonjs/assets/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
- package/lib/commonjs/assets/assets/fonts/icons/manifest.json +5 -4
- package/lib/commonjs/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
- package/lib/commonjs/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
- package/lib/commonjs/assets/fonts/icons/manifest.json +5 -4
- package/lib/commonjs/index.js +45 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/appAssertion/AppAssertionContext.js +79 -0
- package/lib/commonjs/ui/appAssertion/AppAssertionContext.js.map +1 -0
- package/lib/commonjs/ui/appAssertion/appAssertionManager.js +309 -0
- package/lib/commonjs/ui/appAssertion/appAssertionManager.js.map +1 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityPlatform.js +18 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityPlatform.js.map +1 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityPlatform.native.js +30 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityPlatform.native.js.map +1 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityTypes.js +6 -0
- package/lib/commonjs/ui/appAssertion/appIntegrityTypes.js.map +1 -0
- package/lib/commonjs/ui/components/AccountInvitationAcceptance.js +97 -0
- package/lib/commonjs/ui/components/AccountInvitationAcceptance.js.map +1 -0
- package/lib/commonjs/ui/components/DisplayCurrencySelector.js +49 -0
- package/lib/commonjs/ui/components/DisplayCurrencySelector.js.map +1 -0
- package/lib/commonjs/ui/components/fileManagement/FileAppAccessSection.js +121 -0
- package/lib/commonjs/ui/components/fileManagement/FileAppAccessSection.js.map +1 -0
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js +6 -2
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +10 -1
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/mutationKeys.js +4 -0
- package/lib/commonjs/ui/hooks/mutations/mutationKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +49 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +6 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js +35 -1
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useDisplayCurrency.js +77 -0
- package/lib/commonjs/ui/hooks/useDisplayCurrency.js.map +1 -0
- package/lib/commonjs/ui/hooks/useDisplayPrice.js +59 -0
- package/lib/commonjs/ui/hooks/useDisplayPrice.js.map +1 -0
- package/lib/commonjs/ui/icons/subsetGlyphMaps.js +1 -0
- package/lib/commonjs/ui/icons/subsetGlyphMaps.js.map +1 -1
- package/lib/commonjs/ui/index.js +7 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountMembersScreen.js +63 -20
- package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PreferencesScreen.js +4 -3
- package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
- package/lib/module/assets/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
- package/lib/module/assets/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
- package/lib/module/assets/assets/fonts/icons/manifest.json +5 -4
- package/lib/module/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
- package/lib/module/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
- package/lib/module/assets/fonts/icons/manifest.json +5 -4
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/appAssertion/AppAssertionContext.js +72 -0
- package/lib/module/ui/appAssertion/AppAssertionContext.js.map +1 -0
- package/lib/module/ui/appAssertion/appAssertionManager.js +304 -0
- package/lib/module/ui/appAssertion/appAssertionManager.js.map +1 -0
- package/lib/module/ui/appAssertion/appIntegrityPlatform.js +14 -0
- package/lib/module/ui/appAssertion/appIntegrityPlatform.js.map +1 -0
- package/lib/module/ui/appAssertion/appIntegrityPlatform.native.js +26 -0
- package/lib/module/ui/appAssertion/appIntegrityPlatform.native.js.map +1 -0
- package/lib/module/ui/appAssertion/appIntegrityTypes.js +4 -0
- package/lib/module/ui/appAssertion/appIntegrityTypes.js.map +1 -0
- package/lib/module/ui/components/AccountInvitationAcceptance.js +94 -0
- package/lib/module/ui/components/AccountInvitationAcceptance.js.map +1 -0
- package/lib/module/ui/components/DisplayCurrencySelector.js +44 -0
- package/lib/module/ui/components/DisplayCurrencySelector.js.map +1 -0
- package/lib/module/ui/components/fileManagement/FileAppAccessSection.js +116 -0
- package/lib/module/ui/components/fileManagement/FileAppAccessSection.js.map +1 -0
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js +6 -2
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +10 -1
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/mutationKeys.js +4 -0
- package/lib/module/ui/hooks/mutations/mutationKeys.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +47 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +6 -1
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useFileQueries.js +33 -1
- package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -1
- package/lib/module/ui/hooks/useDisplayCurrency.js +72 -0
- package/lib/module/ui/hooks/useDisplayCurrency.js.map +1 -0
- package/lib/module/ui/hooks/useDisplayPrice.js +56 -0
- package/lib/module/ui/hooks/useDisplayPrice.js.map +1 -0
- package/lib/module/ui/icons/subsetGlyphMaps.js +1 -0
- package/lib/module/ui/icons/subsetGlyphMaps.js.map +1 -1
- package/lib/module/ui/index.js +1 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/AccountMembersScreen.js +63 -20
- package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/module/ui/screens/PreferencesScreen.js +4 -3
- package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +7 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/appAssertion/AppAssertionContext.d.ts +41 -0
- package/lib/typescript/commonjs/ui/appAssertion/AppAssertionContext.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/appAssertion/appAssertionManager.d.ts +92 -0
- package/lib/typescript/commonjs/ui/appAssertion/appAssertionManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityPlatform.d.ts +9 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityPlatform.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityPlatform.native.d.ts +3 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityPlatform.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityTypes.d.ts +22 -0
- package/lib/typescript/commonjs/ui/appAssertion/appIntegrityTypes.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts +5 -0
- package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts +3 -0
- package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileAppAccessSection.d.ts +15 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileAppAccessSection.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileDetailsModal.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/mutationKeys.d.ts +3 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/mutationKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +20 -2
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +5 -0
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts +11 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +2 -1
- package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts +6 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/icons/subsetGlyphMaps.d.ts +1 -0
- package/lib/typescript/commonjs/ui/icons/subsetGlyphMaps.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +2 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/PreferencesScreen.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +7 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/appAssertion/AppAssertionContext.d.ts +41 -0
- package/lib/typescript/module/ui/appAssertion/AppAssertionContext.d.ts.map +1 -0
- package/lib/typescript/module/ui/appAssertion/appAssertionManager.d.ts +92 -0
- package/lib/typescript/module/ui/appAssertion/appAssertionManager.d.ts.map +1 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityPlatform.d.ts +9 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityPlatform.d.ts.map +1 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityPlatform.native.d.ts +3 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityPlatform.native.d.ts.map +1 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityTypes.d.ts +22 -0
- package/lib/typescript/module/ui/appAssertion/appIntegrityTypes.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts +5 -0
- package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts +3 -0
- package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/fileManagement/FileAppAccessSection.d.ts +15 -0
- package/lib/typescript/module/ui/components/fileManagement/FileAppAccessSection.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/fileManagement/FileDetailsModal.d.ts +2 -1
- package/lib/typescript/module/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/mutationKeys.d.ts +3 -0
- package/lib/typescript/module/ui/hooks/mutations/mutationKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +20 -2
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +5 -0
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts +11 -1
- package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +2 -1
- package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts +6 -0
- package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts.map +1 -0
- package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts.map +1 -1
- package/lib/typescript/module/ui/icons/subsetGlyphMaps.d.ts +1 -0
- package/lib/typescript/module/ui/icons/subsetGlyphMaps.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +2 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
- package/package.json +9 -6
- package/src/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
- package/src/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
- package/src/assets/fonts/icons/manifest.json +5 -4
- package/src/index.ts +17 -0
- package/src/ui/appAssertion/AppAssertionContext.tsx +109 -0
- package/src/ui/appAssertion/appAssertionManager.ts +363 -0
- package/src/ui/appAssertion/appIntegrityPlatform.native.ts +27 -0
- package/src/ui/appAssertion/appIntegrityPlatform.ts +12 -0
- package/src/ui/appAssertion/appIntegrityTypes.ts +23 -0
- package/src/ui/components/AccountInvitationAcceptance.tsx +90 -0
- package/src/ui/components/DisplayCurrencySelector.tsx +38 -0
- package/src/ui/components/__tests__/AccountInvitationAcceptance.test.tsx +61 -0
- package/src/ui/components/fileManagement/FileAppAccessSection.tsx +111 -0
- package/src/ui/components/fileManagement/FileDetailsModal.tsx +5 -1
- package/src/ui/context/OxyContext.tsx +13 -1
- package/src/ui/hooks/mutations/mutationKeys.ts +5 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +33 -0
- package/src/ui/hooks/queries/queryKeys.ts +5 -0
- package/src/ui/hooks/queries/useFileQueries.ts +44 -2
- package/src/ui/hooks/useDisplayCurrency.ts +64 -0
- package/src/ui/hooks/useDisplayPrice.ts +40 -0
- package/src/ui/icons/subsetGlyphMaps.ts +1 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/screens/AccountMembersScreen.tsx +71 -27
- package/src/ui/screens/PreferencesScreen.tsx +2 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
4
|
+
import { Avatar } from '@oxy.so/bloom/avatar';
|
|
5
|
+
import { Button } from '@oxy.so/bloom/button';
|
|
6
|
+
import { SettingsListGroup, SettingsListItem } from '@oxy.so/bloom/settings-list';
|
|
7
|
+
import { surfaces } from '@oxy.so/bloom/surfaces';
|
|
8
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
9
|
+
import { toast } from '@oxy.so/bloom/toast';
|
|
10
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
11
|
+
import type { FileApplicationAccess } from '@oxy.so/core';
|
|
12
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
13
|
+
import { useFileAppAccess } from '../../hooks/queries/useFileQueries';
|
|
14
|
+
import { useRevokeFileAppAccess } from '../../hooks/mutations/useAccountMutations';
|
|
15
|
+
|
|
16
|
+
const APP_ICON_SIZE = 32;
|
|
17
|
+
|
|
18
|
+
interface FileAppAccessSectionProps {
|
|
19
|
+
fileId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* "Apps with access" (ADR 0043 stage 4c): the applications that can open this
|
|
24
|
+
* file on their own — one that stored it for the person (`upload`), one the
|
|
25
|
+
* person attached it to (`link`) — each with a Revoke action behind a
|
|
26
|
+
* confirmation. Revoking stops the application reading the file and every
|
|
27
|
+
* playback it started; the row leaves the list at once and comes back if the
|
|
28
|
+
* revoke fails. Shown only on the owner's own file.
|
|
29
|
+
*/
|
|
30
|
+
export const FileAppAccessSection: React.FC<FileAppAccessSectionProps> = ({ fileId }) => {
|
|
31
|
+
const { t } = useI18n();
|
|
32
|
+
const { colors } = useTheme();
|
|
33
|
+
const { data: apps, isLoading, isError } = useFileAppAccess(fileId);
|
|
34
|
+
const revoke = useRevokeFileAppAccess();
|
|
35
|
+
const [revokingId, setRevokingId] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const confirmRevoke = useCallback(
|
|
38
|
+
async (app: FileApplicationAccess) => {
|
|
39
|
+
const confirmed = await surfaces.confirm({
|
|
40
|
+
title: t('fileManagement.appAccess.confirm.title', { name: app.name }),
|
|
41
|
+
description: t('fileManagement.appAccess.confirm.message', { name: app.name }),
|
|
42
|
+
confirmLabel: t('fileManagement.appAccess.revoke'),
|
|
43
|
+
cancelLabel: t('common.cancel'),
|
|
44
|
+
destructive: true,
|
|
45
|
+
});
|
|
46
|
+
if (!confirmed) return;
|
|
47
|
+
setRevokingId(app.applicationId);
|
|
48
|
+
try {
|
|
49
|
+
await revoke.mutateAsync({ fileId, applicationId: app.applicationId });
|
|
50
|
+
toast.success(t('fileManagement.appAccess.toasts.revoked', { name: app.name }));
|
|
51
|
+
} catch {
|
|
52
|
+
toast.error(t('fileManagement.appAccess.toasts.revokeFailed', { name: app.name }));
|
|
53
|
+
} finally {
|
|
54
|
+
setRevokingId(null);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[fileId, revoke, t],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const viaText = (app: FileApplicationAccess): string =>
|
|
61
|
+
app.via.map((via) => t(`fileManagement.appAccess.via.${via}`)).join(' · ');
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View className="w-full mb-space-24">
|
|
65
|
+
<Text className="text-text text-[16px] font-semibold mb-space-4">
|
|
66
|
+
{t('fileManagement.appAccess.title')}
|
|
67
|
+
</Text>
|
|
68
|
+
<Text className="text-text-secondary text-[14px] mb-space-12">
|
|
69
|
+
{t('fileManagement.appAccess.description')}
|
|
70
|
+
</Text>
|
|
71
|
+
{isLoading ? (
|
|
72
|
+
<ActivityIndicator color={colors.primary} />
|
|
73
|
+
) : isError ? (
|
|
74
|
+
<Text className="text-text-secondary text-[14px]">
|
|
75
|
+
{t('fileManagement.appAccess.loadError')}
|
|
76
|
+
</Text>
|
|
77
|
+
) : !apps || apps.length === 0 ? (
|
|
78
|
+
<Text className="text-text-secondary text-[14px]">
|
|
79
|
+
{t('fileManagement.appAccess.empty')}
|
|
80
|
+
</Text>
|
|
81
|
+
) : (
|
|
82
|
+
<SettingsListGroup>
|
|
83
|
+
{apps.map((app) => (
|
|
84
|
+
<SettingsListItem
|
|
85
|
+
key={app.applicationId}
|
|
86
|
+
icon={<Avatar source={app.icon} name={app.name} size={APP_ICON_SIZE} />}
|
|
87
|
+
title={app.name}
|
|
88
|
+
description={viaText(app)}
|
|
89
|
+
showChevron={false}
|
|
90
|
+
rightElement={
|
|
91
|
+
<Button
|
|
92
|
+
appearance="subtle"
|
|
93
|
+
tone="danger"
|
|
94
|
+
size="sm"
|
|
95
|
+
loading={revokingId === app.applicationId}
|
|
96
|
+
disabled={revokingId !== null}
|
|
97
|
+
accessibilityLabel={t('fileManagement.appAccess.a11y.revoke', { name: app.name })}
|
|
98
|
+
onPress={() => {
|
|
99
|
+
void confirmRevoke(app);
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{t('fileManagement.appAccess.revoke')}
|
|
103
|
+
</Button>
|
|
104
|
+
}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</SettingsListGroup>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -9,6 +9,7 @@ import { surfaces, type SurfaceControls } from '@oxy.so/bloom/surfaces';
|
|
|
9
9
|
import type { FileMetadata } from '@oxy.so/core';
|
|
10
10
|
import { useI18n } from '../../hooks/useI18n';
|
|
11
11
|
import { formatFileSize, getFileIcon } from '../../utils/fileManagement';
|
|
12
|
+
import { FileAppAccessSection } from './FileAppAccessSection';
|
|
12
13
|
|
|
13
14
|
interface FileDetailsModalProps {
|
|
14
15
|
/** The presenting surface's controls (from `surfaces.present`). */
|
|
@@ -21,7 +22,8 @@ interface FileDetailsModalProps {
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* File-detail panel — a rich presented surface (NOT a yes/no confirm): a file's
|
|
24
|
-
* icon + metadata
|
|
25
|
+
* icon + metadata, for the owner the applications that can open it ("Apps with
|
|
26
|
+
* access", ADR 0043 stage 4c), plus download / delete / cancel actions. Each action first
|
|
25
27
|
* dismisses this surface, then runs its handler, so a follow-up delete confirm
|
|
26
28
|
* stacks cleanly above whatever remains. Presented via {@link presentFileDetails}.
|
|
27
29
|
*/
|
|
@@ -83,6 +85,8 @@ export const FileDetailsModal: React.FC<FileDetailsModalProps> = ({
|
|
|
83
85
|
</View>
|
|
84
86
|
)}
|
|
85
87
|
</View>
|
|
88
|
+
|
|
89
|
+
{isOwner ? <FileAppAccessSection fileId={file.id} /> : null}
|
|
86
90
|
</Card>
|
|
87
91
|
|
|
88
92
|
<View style={{ gap: 8 }}>
|
|
@@ -125,6 +125,7 @@ import { useFollow } from '../hooks/useFollow';
|
|
|
125
125
|
import { commitDeviceSetAndResolve } from './commitSessionFlow';
|
|
126
126
|
import { clearSignInFlows } from '../components/signIn/signInFlowStore';
|
|
127
127
|
import { useOxyAccountGraph } from './useOxyAccountGraph';
|
|
128
|
+
import { AppAssertionProvider } from '../appAssertion/AppAssertionContext';
|
|
128
129
|
|
|
129
130
|
export type { LogoutResult, OxyContextState, OxyRuntimeProviderProps } from './oxyContextTypes';
|
|
130
131
|
|
|
@@ -1742,7 +1743,18 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1742
1743
|
return (
|
|
1743
1744
|
<OxyRuntimeHandleProvider value={runtime}>
|
|
1744
1745
|
<OxyAuthActionsContext.Provider value={authActionsValue}>
|
|
1745
|
-
<OxyRuntimeContext.Provider value={contextValue}>
|
|
1746
|
+
<OxyRuntimeContext.Provider value={contextValue}>
|
|
1747
|
+
<AppAssertionProvider
|
|
1748
|
+
oxyServices={oxyServices}
|
|
1749
|
+
clientId={clientId}
|
|
1750
|
+
userId={canUsePrivateApi ? (user?.id ?? null) : null}
|
|
1751
|
+
sessionId={canUsePrivateApi ? activeSessionId : null}
|
|
1752
|
+
storage={storage}
|
|
1753
|
+
storageKeyPrefix={storageKeyPrefix}
|
|
1754
|
+
>
|
|
1755
|
+
{children}
|
|
1756
|
+
</AppAssertionProvider>
|
|
1757
|
+
</OxyRuntimeContext.Provider>
|
|
1746
1758
|
</OxyAuthActionsContext.Provider>
|
|
1747
1759
|
</OxyRuntimeHandleProvider>
|
|
1748
1760
|
);
|
|
@@ -36,4 +36,9 @@ export const mutationKeys = {
|
|
|
36
36
|
connectedApps: {
|
|
37
37
|
revoke: ['mutation', 'connectedApps', 'revoke'] as const,
|
|
38
38
|
},
|
|
39
|
+
|
|
40
|
+
// A file's applications with access (ADR 0043 stage 4c)
|
|
41
|
+
fileAppAccess: {
|
|
42
|
+
revoke: ['mutation', 'fileAppAccess', 'revoke'] as const,
|
|
43
|
+
},
|
|
39
44
|
} as const;
|
|
@@ -3,6 +3,7 @@ import { authenticatedApiCall } from '@oxy.so/core';
|
|
|
3
3
|
import type {
|
|
4
4
|
AssetMetadata,
|
|
5
5
|
AssetUploadInput,
|
|
6
|
+
FileApplicationAccessRevocation,
|
|
6
7
|
NotificationPreferences,
|
|
7
8
|
PrivacySettings,
|
|
8
9
|
User,
|
|
@@ -21,6 +22,7 @@ import { useOxy } from '../../context/OxyContext';
|
|
|
21
22
|
import { toast } from '@oxy.so/bloom/toast';
|
|
22
23
|
import { useAuthStore } from '../../stores/authStore';
|
|
23
24
|
import { clearedFieldsFromProfileUpdate, upsertCachedUser } from '../queries/userCache';
|
|
25
|
+
import { removeAppAccessFromCache } from '../queries/useFileQueries';
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Update user profile with optimistic updates and offline queue support
|
|
@@ -708,3 +710,34 @@ export const useUploadFile = () => {
|
|
|
708
710
|
},
|
|
709
711
|
});
|
|
710
712
|
};
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Take an application's access to one of the person's files away (ADR 0043
|
|
716
|
+
* stage 4c): optimistic — the application leaves the cached list at once and
|
|
717
|
+
* comes back if the revoke fails — then the list is re-read either way. The
|
|
718
|
+
* caller shows its own toasts (it knows the application's name).
|
|
719
|
+
*/
|
|
720
|
+
export const useRevokeFileAppAccess = () => {
|
|
721
|
+
const { oxyServices, activeSessionId } = useOxy();
|
|
722
|
+
const queryClient = useQueryClient();
|
|
723
|
+
|
|
724
|
+
return useMutation({
|
|
725
|
+
mutationKey: [...mutationKeys.fileAppAccess.revoke],
|
|
726
|
+
mutationFn: async ({ fileId, applicationId }: { fileId: string; applicationId: string }) =>
|
|
727
|
+
authenticatedApiCall<FileApplicationAccessRevocation>(oxyServices, activeSessionId, () =>
|
|
728
|
+
oxyServices.assets.revokeAppAccess(fileId, applicationId),
|
|
729
|
+
),
|
|
730
|
+
onMutate: async ({ fileId, applicationId }) => {
|
|
731
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.files.appAccess(fileId) });
|
|
732
|
+
return { previous: removeAppAccessFromCache(queryClient, fileId, applicationId) };
|
|
733
|
+
},
|
|
734
|
+
onError: (_error, { fileId }, context) => {
|
|
735
|
+
if (context?.previous) {
|
|
736
|
+
queryClient.setQueryData(queryKeys.files.appAccess(fileId), context.previous);
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
onSettled: (_data, _error, { fileId }) => {
|
|
740
|
+
void queryClient.invalidateQueries({ queryKey: queryKeys.files.appAccess(fileId) });
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
};
|
|
@@ -115,6 +115,11 @@ export const queryKeys = {
|
|
|
115
115
|
list: (ownerId?: string) => [...queryKeys.files.all, 'list', ownerId || 'anonymous'] as const,
|
|
116
116
|
/** The owner's trash (ADR 0043 D5) — under `files.all`, so one invalidation covers both. */
|
|
117
117
|
trash: (ownerId?: string) => [...queryKeys.files.all, 'trash', ownerId || 'anonymous'] as const,
|
|
118
|
+
/**
|
|
119
|
+
* The applications that can read one file with their own token (ADR 0043
|
|
120
|
+
* stage 4c) — under `files.all`, so a library invalidation refreshes it too.
|
|
121
|
+
*/
|
|
122
|
+
appAccess: (fileId: string) => [...queryKeys.files.all, 'appAccess', fileId] as const,
|
|
118
123
|
},
|
|
119
124
|
|
|
120
125
|
// Connected apps (OAuth grants the user has authorized)
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* infinite scroll, pull-to-refresh, and optimistic upload reconciliation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
useInfiniteQuery,
|
|
14
|
+
useQuery,
|
|
15
|
+
type InfiniteData,
|
|
16
|
+
type QueryClient,
|
|
17
|
+
} from '@tanstack/react-query';
|
|
18
|
+
import type { FileApplicationAccess, FileMetadata } from '@oxy.so/core';
|
|
14
19
|
import { useOxy } from '../../context/OxyContext';
|
|
15
20
|
import { queryKeys } from './queryKeys';
|
|
16
21
|
|
|
@@ -185,3 +190,40 @@ export function patchFileMetadataInCache(
|
|
|
185
190
|
: data,
|
|
186
191
|
);
|
|
187
192
|
}
|
|
193
|
+
|
|
194
|
+
// --- Applications with access (ADR 0043 stage 4c) ---------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The applications that can open one of the owner's files with their own
|
|
198
|
+
* token: one that stored it for the person, or one the person attached it to.
|
|
199
|
+
* Fetched when `enabled` (the owner's own file); `staleTime: 0` re-reads it
|
|
200
|
+
* every time the details open, since an application can gain access at any
|
|
201
|
+
* time (a new upload on the person's behalf).
|
|
202
|
+
*/
|
|
203
|
+
export const useFileAppAccess = (fileId: string, enabled = true) => {
|
|
204
|
+
const { oxyServices } = useOxy();
|
|
205
|
+
return useQuery<FileApplicationAccess[], Error>({
|
|
206
|
+
queryKey: queryKeys.files.appAccess(fileId),
|
|
207
|
+
queryFn: () => oxyServices.assets.listAppAccess(fileId),
|
|
208
|
+
enabled: enabled && fileId.length > 0,
|
|
209
|
+
staleTime: 0,
|
|
210
|
+
refetchOnWindowFocus: false,
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Drop one application from a file's cached access list (the optimistic half of a revoke). */
|
|
215
|
+
export function removeAppAccessFromCache(
|
|
216
|
+
queryClient: QueryClient,
|
|
217
|
+
fileId: string,
|
|
218
|
+
applicationId: string,
|
|
219
|
+
): FileApplicationAccess[] | undefined {
|
|
220
|
+
const key = queryKeys.files.appAccess(fileId);
|
|
221
|
+
const previous = queryClient.getQueryData<FileApplicationAccess[]>(key);
|
|
222
|
+
if (previous) {
|
|
223
|
+
queryClient.setQueryData<FileApplicationAccess[]>(
|
|
224
|
+
key,
|
|
225
|
+
previous.filter((app) => app.applicationId !== applicationId),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return previous;
|
|
229
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { authenticatedApiCall, type UserPreferences } from '@oxy.so/core';
|
|
4
|
+
import { useOxy } from '../context/OxyContext';
|
|
5
|
+
import { useCurrentUser } from './queries/useAccountQueries';
|
|
6
|
+
import { queryKeys } from './queries/queryKeys';
|
|
7
|
+
import { useAuthStore } from '../stores/authStore';
|
|
8
|
+
import { mutationKeys } from './mutations/mutationKeys';
|
|
9
|
+
|
|
10
|
+
export type DisplayCurrency = NonNullable<UserPreferences['displayCurrency']>;
|
|
11
|
+
export const DISPLAY_CURRENCIES: readonly DisplayCurrency[] = ['USD', 'EUR', 'FAIR'];
|
|
12
|
+
|
|
13
|
+
let anonymousCurrency: DisplayCurrency = 'USD';
|
|
14
|
+
const anonymousListeners = new Set<() => void>();
|
|
15
|
+
const subscribeAnonymous = (listener: () => void) => {
|
|
16
|
+
anonymousListeners.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
anonymousListeners.delete(listener);
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const anonymousSnapshot = () => anonymousCurrency;
|
|
22
|
+
|
|
23
|
+
/** Account-scoped server preference. A late save can never replace another account. */
|
|
24
|
+
export function useDisplayCurrency() {
|
|
25
|
+
const { oxyServices, activeSessionId, isAuthenticated, user } = useOxy();
|
|
26
|
+
const current = useCurrentUser({ enabled: isAuthenticated });
|
|
27
|
+
const client = useQueryClient();
|
|
28
|
+
const anonymous = useSyncExternalStore(
|
|
29
|
+
subscribeAnonymous,
|
|
30
|
+
anonymousSnapshot,
|
|
31
|
+
() => 'USD' as const,
|
|
32
|
+
);
|
|
33
|
+
const currency = isAuthenticated
|
|
34
|
+
? (current.data?.userPreferences?.displayCurrency ??
|
|
35
|
+
user?.userPreferences?.displayCurrency ??
|
|
36
|
+
'USD')
|
|
37
|
+
: anonymous;
|
|
38
|
+
const mutation = useMutation({
|
|
39
|
+
mutationKey: [...mutationKeys.account.updateSettings],
|
|
40
|
+
mutationFn: async (input: { currency: DisplayCurrency; sessionId: string; userId: string }) => {
|
|
41
|
+
const updated = await authenticatedApiCall(oxyServices, input.sessionId, () =>
|
|
42
|
+
oxyServices.users.updateMe({ userPreferences: { displayCurrency: input.currency } }),
|
|
43
|
+
);
|
|
44
|
+
if (updated.id !== input.userId) throw new Error('Account changed. Please try again.');
|
|
45
|
+
return updated;
|
|
46
|
+
},
|
|
47
|
+
onSuccess: (updated, input) => {
|
|
48
|
+
client.setQueryData(queryKeys.accounts.current(input.sessionId), updated);
|
|
49
|
+
if (useAuthStore.getState().user?.id === input.userId)
|
|
50
|
+
useAuthStore.getState().setUser(updated);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
function selectCurrency(next: DisplayCurrency) {
|
|
54
|
+
if (!DISPLAY_CURRENCIES.includes(next)) return;
|
|
55
|
+
if (!isAuthenticated) {
|
|
56
|
+
anonymousCurrency = next;
|
|
57
|
+
for (const listener of anonymousListeners) listener();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (activeSessionId && user?.id)
|
|
61
|
+
mutation.mutate({ currency: next, sessionId: activeSessionId, userId: user.id });
|
|
62
|
+
}
|
|
63
|
+
return { currency, selectCurrency, isSaving: mutation.isPending, error: mutation.error };
|
|
64
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { convertDisplayAmount, readDisplayRates } from '@peable.to/sdk/display';
|
|
4
|
+
import { useDisplayCurrency } from './useDisplayCurrency';
|
|
5
|
+
import { useI18n } from './useI18n';
|
|
6
|
+
|
|
7
|
+
/** Display conversion only. Pricing and checkout request bodies retain their source units. */
|
|
8
|
+
export function useDisplayPrice() {
|
|
9
|
+
const { currency } = useDisplayCurrency();
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
const [now, setNow] = useState(Date.now);
|
|
12
|
+
const rates = useQuery({
|
|
13
|
+
queryKey: ['display-currency', 'faircoin-indexed-price'],
|
|
14
|
+
queryFn: ({ signal }) => readDisplayRates({ signal }),
|
|
15
|
+
enabled: currency !== 'USD',
|
|
16
|
+
staleTime: 30_000,
|
|
17
|
+
gcTime: 0,
|
|
18
|
+
retry: false,
|
|
19
|
+
refetchInterval: 60_000,
|
|
20
|
+
refetchOnWindowFocus: 'always',
|
|
21
|
+
});
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
24
|
+
return () => clearInterval(timer);
|
|
25
|
+
}, []);
|
|
26
|
+
function formatUsd(minorUnits: number) {
|
|
27
|
+
const display = convertDisplayAmount({
|
|
28
|
+
units: String(minorUnits),
|
|
29
|
+
currency: 'USD',
|
|
30
|
+
displayCurrency: currency,
|
|
31
|
+
rates: rates.isError ? null : rates.data,
|
|
32
|
+
now,
|
|
33
|
+
});
|
|
34
|
+
if (!display) return t('preferences.displayCurrency.unavailable', { currency });
|
|
35
|
+
return currency === 'USD'
|
|
36
|
+
? display
|
|
37
|
+
: t('preferences.displayCurrency.approximate', { amount: display });
|
|
38
|
+
}
|
|
39
|
+
return { currency, formatUsd };
|
|
40
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ export { default as ProfileButton } from './components/ProfileButton';
|
|
|
37
37
|
|
|
38
38
|
// Context + hooks
|
|
39
39
|
export { useOxy, useOptionalOxy, OxyProviderMissingError } from './context/OxyContext';
|
|
40
|
+
export { useAppAssertion } from './appAssertion/AppAssertionContext';
|
|
41
|
+
export type { UseAppAssertionResult } from './appAssertion/AppAssertionContext';
|
|
40
42
|
export { useAuth } from './hooks/useAuth';
|
|
41
43
|
export type { AuthActions, AuthState, SignInOutcome, UseAuthReturn } from './hooks/useAuth';
|
|
42
44
|
export { useFollow, useSeedFollowStatuses } from './hooks/useFollow';
|
|
@@ -30,13 +30,8 @@ const ASSIGNABLE_ROLES: AssignableRole[] = ['admin', 'editor', 'developer', 'bil
|
|
|
30
30
|
const errorMessage = (error: unknown, fallback: string): string =>
|
|
31
31
|
error instanceof Error && error.message ? error.message : fallback;
|
|
32
32
|
|
|
33
|
-
const isUserNotFound = (error: unknown): boolean => {
|
|
34
|
-
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
35
|
-
return message.includes('user not found') || message.includes('not found');
|
|
36
|
-
};
|
|
37
|
-
|
|
38
33
|
/**
|
|
39
|
-
* Manage the members of an account (invite by
|
|
34
|
+
* Manage the members of an account (invite by email, change roles,
|
|
40
35
|
* remove, transfer ownership). Gated on the caller's `callerMembership`
|
|
41
36
|
* permissions. Receives the target `accountId` via navigation props.
|
|
42
37
|
*/
|
|
@@ -86,6 +81,27 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
86
81
|
queryFn: () => oxyServices.accounts.members.list(id),
|
|
87
82
|
enabled: canUsePrivateApi && id.length > 0 && canRead,
|
|
88
83
|
});
|
|
84
|
+
const invitationsQuery = useQuery({
|
|
85
|
+
queryKey: ['account-invitations', id],
|
|
86
|
+
queryFn: () => oxyServices.accounts.invitations.list(id),
|
|
87
|
+
enabled: canUsePrivateApi && !!id && canRead,
|
|
88
|
+
});
|
|
89
|
+
const invitationAction = useMutation({
|
|
90
|
+
mutationFn: async ({
|
|
91
|
+
invitationId,
|
|
92
|
+
action,
|
|
93
|
+
}: {
|
|
94
|
+
invitationId: string;
|
|
95
|
+
action: 'resend' | 'cancel';
|
|
96
|
+
}) => {
|
|
97
|
+
if (action === 'resend') await oxyServices.accounts.invitations.resend(id, invitationId);
|
|
98
|
+
else await oxyServices.accounts.invitations.cancel(id, invitationId);
|
|
99
|
+
},
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
void invitationsQuery.refetch();
|
|
102
|
+
},
|
|
103
|
+
onError: (error) => toast.error(errorMessage(error, 'Could not update invitation')),
|
|
104
|
+
});
|
|
89
105
|
const members = useMemo<AccountMember[]>(() => membersQuery.data ?? [], [membersQuery.data]);
|
|
90
106
|
const memberUserIds = useMemo(
|
|
91
107
|
() => [...new Set(members.map((member) => member.memberUserId).filter((id) => id.length > 0))],
|
|
@@ -126,6 +142,7 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
126
142
|
// tables — mirror the SDK's per-account prefix sweep so every cached member
|
|
127
143
|
// list refetches, not only the account that was mutated.
|
|
128
144
|
queryClient.invalidateQueries({ queryKey: ['accounts', 'members'] });
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: ['account-invitations', id] });
|
|
129
146
|
queryClient.invalidateQueries({ queryKey: ['accounts', 'detail', id] });
|
|
130
147
|
}, [queryClient, id]);
|
|
131
148
|
|
|
@@ -137,24 +154,20 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
137
154
|
const inviteMutation = useMutation({
|
|
138
155
|
mutationKey: ['accounts', 'members', 'invite', id],
|
|
139
156
|
mutationFn: (input: { usernameOrEmail: string; role: AssignableRole }) =>
|
|
140
|
-
oxyServices.accounts.
|
|
157
|
+
oxyServices.accounts.invitations.create(id, {
|
|
158
|
+
email: input.usernameOrEmail,
|
|
159
|
+
role: input.role,
|
|
160
|
+
}),
|
|
141
161
|
onSuccess: () => {
|
|
142
162
|
setInviteIdentifier('');
|
|
143
163
|
setInviteRole('editor');
|
|
144
164
|
setInviteError(null);
|
|
145
165
|
invalidateMembers();
|
|
146
|
-
toast.success(
|
|
166
|
+
toast.success('Invitation sent');
|
|
147
167
|
},
|
|
148
168
|
onError: (error) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setInviteError(message);
|
|
152
|
-
toast.error(message);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
toast.error(
|
|
156
|
-
errorMessage(error, t('accounts.members.toasts.addFailed') || 'Failed to add member'),
|
|
157
|
-
);
|
|
169
|
+
invalidateMembers();
|
|
170
|
+
toast.error(errorMessage(error, 'Could not send invitation'));
|
|
158
171
|
},
|
|
159
172
|
});
|
|
160
173
|
|
|
@@ -207,14 +220,12 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
207
220
|
const handleInvite = useCallback(() => {
|
|
208
221
|
const usernameOrEmail = inviteIdentifier.trim();
|
|
209
222
|
if (!usernameOrEmail) {
|
|
210
|
-
setInviteError(
|
|
211
|
-
t('accounts.members.errors.identifierRequired') || 'Enter a username or email to invite',
|
|
212
|
-
);
|
|
223
|
+
setInviteError('Enter an email to invite');
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
215
226
|
setInviteError(null);
|
|
216
227
|
inviteMutation.mutate({ usernameOrEmail, role: inviteRole });
|
|
217
|
-
}, [inviteIdentifier, inviteRole, inviteMutation
|
|
228
|
+
}, [inviteIdentifier, inviteRole, inviteMutation]);
|
|
218
229
|
|
|
219
230
|
const handleChangeRole = useCallback(
|
|
220
231
|
(member: AccountMember, role: AccountRole) => {
|
|
@@ -282,13 +293,11 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
282
293
|
{/* Invite form */}
|
|
283
294
|
{canInvite ? (
|
|
284
295
|
<View className="gap-space-12 p-space-16 rounded-radius-20 bg-fill">
|
|
285
|
-
<Text className="text-body font-semibold text-text">
|
|
286
|
-
{t('accounts.members.invite.title') || 'Add a member'}
|
|
287
|
-
</Text>
|
|
296
|
+
<Text className="text-body font-semibold text-text">{'Invite a member'}</Text>
|
|
288
297
|
<TextField isInvalid={!!inviteError}>
|
|
289
298
|
<TextFieldInput
|
|
290
299
|
floatingLabel
|
|
291
|
-
label={
|
|
300
|
+
label={'Email'}
|
|
292
301
|
value={inviteIdentifier}
|
|
293
302
|
onChangeText={(value) => {
|
|
294
303
|
setInviteIdentifier(value);
|
|
@@ -332,14 +341,49 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
332
341
|
onPress={handleInvite}
|
|
333
342
|
disabled={inviteMutation.isPending || !inviteIdentifier.trim()}
|
|
334
343
|
loading={inviteMutation.isPending}
|
|
335
|
-
accessibilityLabel={
|
|
344
|
+
accessibilityLabel={'Send invitation'}
|
|
336
345
|
className="w-full"
|
|
337
346
|
>
|
|
338
|
-
{
|
|
347
|
+
{'Send invitation'}
|
|
339
348
|
</Button>
|
|
340
349
|
</View>
|
|
341
350
|
) : null}
|
|
342
351
|
|
|
352
|
+
{canRead ? (
|
|
353
|
+
<View className="gap-space-12">
|
|
354
|
+
<Text className="text-body font-semibold text-text">Pending invitations</Text>
|
|
355
|
+
{invitationsQuery.isError ? <Text>Could not load invitations.</Text> : null}
|
|
356
|
+
{(invitationsQuery.data ?? []).map((invitation) => (
|
|
357
|
+
<View key={invitation.id} className="gap-space-8 p-space-16 rounded-radius-20 bg-fill">
|
|
358
|
+
<Text>
|
|
359
|
+
{invitation.email} · {roleLabel(invitation.role)} · {invitation.status}
|
|
360
|
+
</Text>
|
|
361
|
+
<Text>Expires {new Date(invitation.expiresAt).toLocaleDateString()}</Text>
|
|
362
|
+
{canInvite ? (
|
|
363
|
+
<Button
|
|
364
|
+
disabled={invitationAction.isPending}
|
|
365
|
+
onPress={() =>
|
|
366
|
+
invitationAction.mutate({ invitationId: invitation.id, action: 'resend' })
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
Resend
|
|
370
|
+
</Button>
|
|
371
|
+
) : null}
|
|
372
|
+
{canRemove ? (
|
|
373
|
+
<Button
|
|
374
|
+
disabled={invitationAction.isPending}
|
|
375
|
+
onPress={() =>
|
|
376
|
+
invitationAction.mutate({ invitationId: invitation.id, action: 'cancel' })
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
Cancel invitation
|
|
380
|
+
</Button>
|
|
381
|
+
) : null}
|
|
382
|
+
</View>
|
|
383
|
+
))}
|
|
384
|
+
</View>
|
|
385
|
+
) : null}
|
|
386
|
+
|
|
343
387
|
{/* Member list */}
|
|
344
388
|
{canRead ? (
|
|
345
389
|
<View className="gap-space-8">
|
|
@@ -5,6 +5,7 @@ import { Switch } from '@oxy.so/bloom/switch';
|
|
|
5
5
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
6
6
|
import type { UserPreferences } from '@oxy.so/core';
|
|
7
7
|
import { getNativeLanguageName } from '@oxy.so/core';
|
|
8
|
+
import { DisplayCurrencySelector } from '../components/DisplayCurrencySelector';
|
|
8
9
|
import type { BaseScreenProps } from '../types/navigation';
|
|
9
10
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
10
11
|
import { useI18n } from '../hooks/useI18n';
|
|
@@ -158,6 +159,7 @@ const PreferencesScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navigat
|
|
|
158
159
|
</SettingsListGroup>
|
|
159
160
|
|
|
160
161
|
<SettingsListGroup title={t('preferences.sections.region') || 'Region'}>
|
|
162
|
+
<DisplayCurrencySelector />
|
|
161
163
|
<SettingsListItem
|
|
162
164
|
icon={<SettingsIcon name="clock-outline" color={bloomTheme.colors.info} />}
|
|
163
165
|
title={t('preferences.items.timezone.title') || 'Timezone'}
|