@oxyhq/services 5.13.26 → 5.13.29
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/README.md +5 -6
- package/lib/commonjs/core/OxyServices.js +2 -2
- package/lib/commonjs/core/mixins/OxyServices.assets.js +0 -7
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.devices.js +14 -0
- package/lib/commonjs/core/mixins/OxyServices.devices.js.map +1 -1
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileCard.js +5 -1
- package/lib/commonjs/ui/components/ProfileCard.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +8 -0
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/index.js +13 -0
- package/lib/commonjs/ui/hooks/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js +103 -0
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js.map +1 -0
- package/lib/commonjs/ui/hooks/useSessionSocket.js +57 -10
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +20 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js +0 -3
- package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
- package/lib/module/core/OxyServices.js +2 -2
- package/lib/module/core/mixins/OxyServices.assets.js +0 -7
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.devices.js +14 -0
- package/lib/module/core/mixins/OxyServices.devices.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/ProfileCard.js +5 -1
- package/lib/module/ui/components/ProfileCard.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +8 -0
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/index.js +1 -0
- package/lib/module/ui/hooks/index.js.map +1 -1
- package/lib/module/ui/hooks/useFileDownloadUrl.js +97 -0
- package/lib/module/ui/hooks/useFileDownloadUrl.js.map +1 -0
- package/lib/module/ui/hooks/useSessionSocket.js +57 -10
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +20 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/karma/KarmaFAQScreen.js +1 -4
- package/lib/module/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
- package/lib/typescript/core/OxyServices.d.ts +2 -2
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +0 -4
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts +10 -0
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +6 -1
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/ProfileCard.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/index.d.ts +1 -0
- package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useFileDownloadUrl.d.ts +19 -0
- package/lib/typescript/ui/hooks/useFileDownloadUrl.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts +2 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts +3 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/OxyServices.ts +2 -2
- package/src/core/mixins/OxyServices.assets.ts +0 -7
- package/src/core/mixins/OxyServices.devices.ts +19 -0
- package/src/index.ts +1 -0
- package/src/ui/components/ProfileCard.tsx +4 -1
- package/src/ui/context/OxyContext.tsx +8 -0
- package/src/ui/hooks/index.ts +2 -1
- package/src/ui/hooks/useFileDownloadUrl.ts +118 -0
- package/src/ui/hooks/useSessionSocket.ts +64 -11
- package/src/ui/screens/AccountOverviewScreen.tsx +4 -1
- package/src/ui/screens/AccountSettingsScreen.tsx +20 -1
- package/src/ui/screens/karma/KarmaFAQScreen.tsx +1 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OxyServices } from '../../core/OxyServices';
|
|
2
|
+
export declare const setOxyFileUrlInstance: (instance: OxyServices) => void;
|
|
3
|
+
export interface UseFileDownloadUrlOptions {
|
|
4
|
+
variant?: string;
|
|
5
|
+
expiresIn?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UseFileDownloadUrlResult {
|
|
8
|
+
url: string | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hook to resolve a file's download URL asynchronously.
|
|
14
|
+
*
|
|
15
|
+
* Prefers `getFileDownloadUrlAsync` and falls back to the synchronous
|
|
16
|
+
* `getFileDownloadUrl` helper if the async call fails.
|
|
17
|
+
*/
|
|
18
|
+
export declare const useFileDownloadUrl: (fileId?: string | null, options?: UseFileDownloadUrlOptions) => UseFileDownloadUrlResult;
|
|
19
|
+
//# sourceMappingURL=useFileDownloadUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFileDownloadUrl.d.ts","sourceRoot":"","sources":["../../../../src/ui/hooks/useFileDownloadUrl.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAIrD,eAAO,MAAM,qBAAqB,GAAI,UAAU,WAAW,SAE1D,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,GAAG,IAAI,EACtB,UAAU,yBAAyB,KAClC,wBAoFF,CAAC"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
interface UseSessionSocketProps {
|
|
2
2
|
userId: string | null | undefined;
|
|
3
3
|
activeSessionId: string | null | undefined;
|
|
4
|
+
currentDeviceId: string | null | undefined;
|
|
4
5
|
refreshSessions: () => Promise<void>;
|
|
5
6
|
logout: () => Promise<void>;
|
|
6
7
|
baseURL: string;
|
|
7
8
|
onRemoteSignOut?: () => void;
|
|
8
9
|
}
|
|
9
|
-
export declare function useSessionSocket({ userId, activeSessionId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps): void;
|
|
10
|
+
export declare function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps): void;
|
|
10
11
|
export {};
|
|
11
12
|
//# sourceMappingURL=useSessionSocket.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSessionSocket.d.ts","sourceRoot":"","sources":["../../../../src/ui/hooks/useSessionSocket.ts"],"names":[],"mappings":"AAIA,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"useSessionSocket.d.ts","sourceRoot":"","sources":["../../../../src/ui/hooks/useSessionSocket.ts"],"names":[],"mappings":"AAIA,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,qBAAqB,QAiJtJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountOverviewScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/AccountOverviewScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAa9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;
|
|
1
|
+
{"version":3,"file":"AccountOverviewScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/AccountOverviewScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAa9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;AAyqB3D,wBAAiD"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { BaseScreenProps } from '../navigation/types';
|
|
3
|
-
declare const _default: React.NamedExoticComponent<BaseScreenProps
|
|
3
|
+
declare const _default: React.NamedExoticComponent<BaseScreenProps & {
|
|
4
|
+
initialField?: string;
|
|
5
|
+
}>;
|
|
4
6
|
export default _default;
|
|
5
7
|
//# sourceMappingURL=AccountSettingsScreen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAcjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;
|
|
1
|
+
{"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAcjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;mBAqBc,MAAM;;AAqhE/E,wBAAiD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KarmaFAQScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/karma/KarmaFAQScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;;
|
|
1
|
+
{"version":3,"file":"KarmaFAQScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/karma/KarmaFAQScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;;AAgL9D,wBAA0C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "5.13.
|
|
3
|
+
"version": "5.13.29",
|
|
4
4
|
"description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
package/src/core/OxyServices.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* const file = fileInput.files[0];
|
|
23
23
|
* await oxy.uploadRawFile(file);
|
|
24
24
|
*
|
|
25
|
-
* // Get a file
|
|
26
|
-
* const url = oxy.
|
|
25
|
+
* // Get a file download URL for <img src>
|
|
26
|
+
* const url = oxy.getFileDownloadUrl('fileId', 'thumb');
|
|
27
27
|
* ```
|
|
28
28
|
*
|
|
29
29
|
* ## Node.js (CommonJS/TypeScript)
|
|
@@ -66,13 +66,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Get file stream URL (direct Oxy Cloud/CDN URL, no token)
|
|
71
|
-
*/
|
|
72
|
-
getFileStreamUrl(fileId: string): string {
|
|
73
|
-
return `${this.getCloudURL()}/files/${fileId}/stream`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
69
|
/**
|
|
77
70
|
* List user files
|
|
78
71
|
*/
|
|
@@ -98,6 +98,25 @@ export function OxyServicesDevicesMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
98
98
|
throw this.handleError(error);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get security information (TOTP status, backup codes count)
|
|
104
|
+
* @returns Security information object
|
|
105
|
+
*/
|
|
106
|
+
async getSecurityInfo(): Promise<{
|
|
107
|
+
twoFactorEnabled: boolean;
|
|
108
|
+
totpCreatedAt: string | null;
|
|
109
|
+
backupCodesCount: number;
|
|
110
|
+
recoveryEmail: string | null;
|
|
111
|
+
}> {
|
|
112
|
+
try {
|
|
113
|
+
return await this.makeRequest('GET', '/api/devices/security', undefined, {
|
|
114
|
+
cache: false,
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw this.handleError(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
101
120
|
};
|
|
102
121
|
}
|
|
103
122
|
|
package/src/index.ts
CHANGED
|
@@ -97,6 +97,7 @@ export { useAuthStore } from './ui/stores/authStore';
|
|
|
97
97
|
export { useAssetStore, useAssets as useAssetsStore, useAsset, useUploadProgress, useAssetLoading, useAssetErrors, useAssetsByApp, useAssetsByEntity, useAssetUsageCount, useIsAssetLinked } from './ui/stores/assetStore';
|
|
98
98
|
export { useSessionSocket } from './ui/hooks/useSessionSocket';
|
|
99
99
|
export { useAssets, setOxyAssetInstance } from './ui/hooks/useAssets';
|
|
100
|
+
export { useFileDownloadUrl, setOxyFileUrlInstance } from './ui/hooks/useFileDownloadUrl';
|
|
100
101
|
|
|
101
102
|
// UI components
|
|
102
103
|
export { OxySignInButton } from './ui/components/OxySignInButton';
|
|
@@ -4,6 +4,7 @@ import { Ionicons } from '@expo/vector-icons';
|
|
|
4
4
|
import { useI18n } from '../hooks/useI18n';
|
|
5
5
|
import Avatar from './Avatar';
|
|
6
6
|
import { useOxy } from '../context/OxyContext';
|
|
7
|
+
import { useFileDownloadUrl } from '../hooks';
|
|
7
8
|
import { fontFamilies } from '../styles/fonts';
|
|
8
9
|
|
|
9
10
|
interface ProfileCardProps {
|
|
@@ -33,6 +34,8 @@ const ProfileCard: React.FC<ProfileCardProps> = ({
|
|
|
33
34
|
const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#FFFFFF';
|
|
34
35
|
const primaryColor = '#0066CC';
|
|
35
36
|
|
|
37
|
+
const avatarUrl = useFileDownloadUrl(user?.avatar, { variant: 'thumb' }).url || undefined;
|
|
38
|
+
|
|
36
39
|
return (
|
|
37
40
|
<View style={styles.headerSection}>
|
|
38
41
|
<View style={[
|
|
@@ -43,7 +46,7 @@ const ProfileCard: React.FC<ProfileCardProps> = ({
|
|
|
43
46
|
]}>
|
|
44
47
|
<View style={styles.userProfile}>
|
|
45
48
|
<Avatar
|
|
46
|
-
uri={user?.avatar ?
|
|
49
|
+
uri={user?.avatar ? avatarUrl : undefined}
|
|
47
50
|
name={user?.name?.full || user?.username}
|
|
48
51
|
size={60}
|
|
49
52
|
theme={theme}
|
|
@@ -894,10 +894,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
894
894
|
}
|
|
895
895
|
}, [bottomSheetRef]);
|
|
896
896
|
|
|
897
|
+
// Get current deviceId from active session
|
|
898
|
+
const currentDeviceId = useMemo(() => {
|
|
899
|
+
if (!activeSessionId || !sessions.length) return null;
|
|
900
|
+
const activeSession = sessions.find(s => s.sessionId === activeSessionId);
|
|
901
|
+
return activeSession?.deviceId || null;
|
|
902
|
+
}, [activeSessionId, sessions]);
|
|
903
|
+
|
|
897
904
|
// Integrate socket for real-time session updates
|
|
898
905
|
useSessionSocket({
|
|
899
906
|
userId: user?.id,
|
|
900
907
|
activeSessionId,
|
|
908
|
+
currentDeviceId,
|
|
901
909
|
refreshSessions,
|
|
902
910
|
logout,
|
|
903
911
|
baseURL: oxyServices.getBaseURL(),
|
package/src/ui/hooks/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { useFollow, useFollowerCounts } from './useFollow';
|
|
1
|
+
export { useFollow, useFollowerCounts } from './useFollow';
|
|
2
|
+
export { useFileDownloadUrl, setOxyFileUrlInstance } from './useFileDownloadUrl';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { OxyServices } from '../../core/OxyServices';
|
|
3
|
+
|
|
4
|
+
let oxyInstance: OxyServices | null = null;
|
|
5
|
+
|
|
6
|
+
export const setOxyFileUrlInstance = (instance: OxyServices) => {
|
|
7
|
+
oxyInstance = instance;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface UseFileDownloadUrlOptions {
|
|
11
|
+
variant?: string;
|
|
12
|
+
expiresIn?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseFileDownloadUrlResult {
|
|
16
|
+
url: string | null;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to resolve a file's download URL asynchronously.
|
|
23
|
+
*
|
|
24
|
+
* Prefers `getFileDownloadUrlAsync` and falls back to the synchronous
|
|
25
|
+
* `getFileDownloadUrl` helper if the async call fails.
|
|
26
|
+
*/
|
|
27
|
+
export const useFileDownloadUrl = (
|
|
28
|
+
fileId?: string | null,
|
|
29
|
+
options?: UseFileDownloadUrlOptions
|
|
30
|
+
): UseFileDownloadUrlResult => {
|
|
31
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [error, setError] = useState<Error | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!fileId) {
|
|
37
|
+
setUrl(null);
|
|
38
|
+
setLoading(false);
|
|
39
|
+
setError(null);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!oxyInstance) {
|
|
44
|
+
// Fail silently but don't crash the UI – caller can decide what to do with null URL.
|
|
45
|
+
setUrl(null);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
|
|
53
|
+
const load = async () => {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
// Store instance in local variable for TypeScript null checking
|
|
58
|
+
const instance = oxyInstance;
|
|
59
|
+
if (!instance) {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { variant, expiresIn } = options || {};
|
|
67
|
+
let resolvedUrl: string | null = null;
|
|
68
|
+
|
|
69
|
+
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
70
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(fileId, variant, expiresIn);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
74
|
+
resolvedUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setUrl(resolvedUrl || null);
|
|
79
|
+
}
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
// Fallback to sync URL on error where possible
|
|
82
|
+
try {
|
|
83
|
+
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
84
|
+
const { variant, expiresIn } = options || {};
|
|
85
|
+
const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setUrl(fallbackUrl || null);
|
|
88
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore secondary failure, we'll surface the original error below
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!cancelled) {
|
|
97
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
load();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [fileId, options?.variant, options?.expiresIn]);
|
|
112
|
+
|
|
113
|
+
return { url, loading, error };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
@@ -5,13 +5,14 @@ import { toast } from '../../lib/sonner';
|
|
|
5
5
|
interface UseSessionSocketProps {
|
|
6
6
|
userId: string | null | undefined;
|
|
7
7
|
activeSessionId: string | null | undefined;
|
|
8
|
+
currentDeviceId: string | null | undefined;
|
|
8
9
|
refreshSessions: () => Promise<void>;
|
|
9
10
|
logout: () => Promise<void>;
|
|
10
11
|
baseURL: string;
|
|
11
12
|
onRemoteSignOut?: () => void;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function useSessionSocket({ userId, activeSessionId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps) {
|
|
15
|
+
export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps) {
|
|
15
16
|
const socketRef = useRef<any>(null);
|
|
16
17
|
const joinedRoomRef = useRef<string | null>(null);
|
|
17
18
|
|
|
@@ -20,6 +21,7 @@ export function useSessionSocket({ userId, activeSessionId, refreshSessions, log
|
|
|
20
21
|
const logoutRef = useRef(logout);
|
|
21
22
|
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
22
23
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
24
|
+
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
23
25
|
|
|
24
26
|
// Update refs when callbacks change
|
|
25
27
|
useEffect(() => {
|
|
@@ -27,7 +29,8 @@ export function useSessionSocket({ userId, activeSessionId, refreshSessions, log
|
|
|
27
29
|
logoutRef.current = logout;
|
|
28
30
|
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
29
31
|
activeSessionIdRef.current = activeSessionId;
|
|
30
|
-
|
|
32
|
+
currentDeviceIdRef.current = currentDeviceId;
|
|
33
|
+
}, [refreshSessions, logout, onRemoteSignOut, activeSessionId, currentDeviceId]);
|
|
31
34
|
|
|
32
35
|
useEffect(() => {
|
|
33
36
|
if (!userId || !baseURL) {
|
|
@@ -71,22 +74,72 @@ export function useSessionSocket({ userId, activeSessionId, refreshSessions, log
|
|
|
71
74
|
}
|
|
72
75
|
};
|
|
73
76
|
|
|
74
|
-
const handleSessionUpdate = (data: {
|
|
77
|
+
const handleSessionUpdate = (data: {
|
|
78
|
+
type: string;
|
|
79
|
+
sessionId?: string;
|
|
80
|
+
deviceId?: string;
|
|
81
|
+
sessionIds?: string[]
|
|
82
|
+
}) => {
|
|
75
83
|
if (__DEV__) {
|
|
76
84
|
console.log('Received session_update:', data);
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
const currentActiveSessionId = activeSessionIdRef.current;
|
|
88
|
+
const currentDeviceId = currentDeviceIdRef.current;
|
|
81
89
|
|
|
82
|
-
//
|
|
83
|
-
if (data.
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
// Handle different event types
|
|
91
|
+
if (data.type === 'session_removed') {
|
|
92
|
+
// If the removed sessionId matches the current activeSessionId, immediately logout
|
|
93
|
+
if (data.sessionId === currentActiveSessionId) {
|
|
94
|
+
if (onRemoteSignOutRef.current) {
|
|
95
|
+
onRemoteSignOutRef.current();
|
|
96
|
+
} else {
|
|
97
|
+
toast.info('You have been signed out remotely.');
|
|
98
|
+
}
|
|
99
|
+
logoutRef.current();
|
|
86
100
|
} else {
|
|
87
|
-
|
|
101
|
+
// Otherwise, just refresh the sessions list
|
|
102
|
+
refreshSessionsRef.current();
|
|
103
|
+
}
|
|
104
|
+
} else if (data.type === 'device_removed') {
|
|
105
|
+
// If the removed deviceId matches the current device, immediately logout
|
|
106
|
+
if (data.deviceId && data.deviceId === currentDeviceId) {
|
|
107
|
+
if (onRemoteSignOutRef.current) {
|
|
108
|
+
onRemoteSignOutRef.current();
|
|
109
|
+
} else {
|
|
110
|
+
toast.info('This device has been removed. You have been signed out.');
|
|
111
|
+
}
|
|
112
|
+
logoutRef.current();
|
|
113
|
+
} else {
|
|
114
|
+
// Otherwise, refresh sessions and device list
|
|
115
|
+
refreshSessionsRef.current();
|
|
116
|
+
}
|
|
117
|
+
} else if (data.type === 'sessions_removed') {
|
|
118
|
+
// If the current activeSessionId is in the removed sessionIds list, immediately logout
|
|
119
|
+
if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
|
|
120
|
+
if (onRemoteSignOutRef.current) {
|
|
121
|
+
onRemoteSignOutRef.current();
|
|
122
|
+
} else {
|
|
123
|
+
toast.info('You have been signed out remotely.');
|
|
124
|
+
}
|
|
125
|
+
logoutRef.current();
|
|
126
|
+
} else {
|
|
127
|
+
// Otherwise, refresh sessions list
|
|
128
|
+
refreshSessionsRef.current();
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
132
|
+
refreshSessionsRef.current();
|
|
133
|
+
|
|
134
|
+
// If the current session was logged out (legacy behavior), handle it specially
|
|
135
|
+
if (data.sessionId === currentActiveSessionId) {
|
|
136
|
+
if (onRemoteSignOutRef.current) {
|
|
137
|
+
onRemoteSignOutRef.current();
|
|
138
|
+
} else {
|
|
139
|
+
toast.info('You have been signed out remotely.');
|
|
140
|
+
}
|
|
141
|
+
logoutRef.current();
|
|
88
142
|
}
|
|
89
|
-
logoutRef.current();
|
|
90
143
|
}
|
|
91
144
|
};
|
|
92
145
|
|
|
@@ -420,7 +420,10 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
|
|
|
420
420
|
<>
|
|
421
421
|
<View style={styles.userIcon}>
|
|
422
422
|
{account.avatar ? (
|
|
423
|
-
<Image
|
|
423
|
+
<Image
|
|
424
|
+
source={{ uri: oxyServices.getFileDownloadUrl(account.avatar as string, 'thumb') }}
|
|
425
|
+
style={styles.accountAvatarImage}
|
|
426
|
+
/>
|
|
424
427
|
) : (
|
|
425
428
|
<View style={styles.accountAvatarFallback}>
|
|
426
429
|
<Text style={styles.accountAvatarText}>
|
|
@@ -33,11 +33,12 @@ const locationSearchCache = new TTLCache<any[]>(60 * 60 * 1000); // 1 hour cache
|
|
|
33
33
|
registerCacheForCleanup(linkMetadataCache);
|
|
34
34
|
registerCacheForCleanup(locationSearchCache);
|
|
35
35
|
|
|
36
|
-
const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
36
|
+
const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string }> = ({
|
|
37
37
|
onClose,
|
|
38
38
|
theme,
|
|
39
39
|
goBack,
|
|
40
40
|
navigate,
|
|
41
|
+
initialField,
|
|
41
42
|
}) => {
|
|
42
43
|
const { user: userFromContext, oxyServices, isLoading: authLoading, isAuthenticated, showBottomSheet, activeSessionId } = useOxy();
|
|
43
44
|
const { t } = useI18n();
|
|
@@ -241,6 +242,24 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
241
242
|
}
|
|
242
243
|
}, [user, avatarFileId, isUpdatingAvatar, optimisticAvatarId]);
|
|
243
244
|
|
|
245
|
+
// Set initial editing field if provided via props (e.g., from navigation)
|
|
246
|
+
// Use a ref to track if we've already set the initial field to avoid loops
|
|
247
|
+
const hasSetInitialFieldRef = useRef(false);
|
|
248
|
+
const previousInitialFieldRef = useRef<string | undefined>(undefined);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
// If initialField changed, reset the flag
|
|
251
|
+
if (previousInitialFieldRef.current !== initialField) {
|
|
252
|
+
hasSetInitialFieldRef.current = false;
|
|
253
|
+
previousInitialFieldRef.current = initialField;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Set the editing field if initialField is provided and we haven't set it yet
|
|
257
|
+
if (initialField && !hasSetInitialFieldRef.current) {
|
|
258
|
+
setEditingField(initialField);
|
|
259
|
+
hasSetInitialFieldRef.current = true;
|
|
260
|
+
}
|
|
261
|
+
}, [initialField]);
|
|
262
|
+
|
|
244
263
|
const handleSave = async () => {
|
|
245
264
|
if (!user) return;
|
|
246
265
|
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, ScrollView, Platform, TouchableOpacity, TextInput, LayoutAnimation
|
|
2
|
+
import { View, Text, StyleSheet, ScrollView, Platform, TouchableOpacity, TextInput, LayoutAnimation } from 'react-native';
|
|
3
3
|
import type { BaseScreenProps } from '../../navigation/types';
|
|
4
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
5
5
|
import { Header } from '../../components';
|
|
6
6
|
import { useI18n } from '../../hooks/useI18n';
|
|
7
7
|
|
|
8
|
-
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
9
|
-
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
const FAQ_KEYS = ['what', 'earn', 'lose', 'use', 'transfer', 'support'] as const;
|
|
13
9
|
|
|
14
10
|
/**
|