mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-01 16:08:04 +00:00
refactor(tv): simplify user profile management with automatic sandboxing
This commit is contained in:
1
app.json
1
app.json
@@ -76,6 +76,7 @@
|
||||
"expo-router",
|
||||
"expo-font",
|
||||
"./plugins/withExcludeMedia3Dash.js",
|
||||
"./plugins/withTVUserManagement.js",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as Device from "expo-device";
|
||||
import { Platform } from "react-native";
|
||||
import { GlobalModal } from "@/components/GlobalModal";
|
||||
import { enableTVMenuKeyInterception } from "@/hooks/useTVBackHandler";
|
||||
|
||||
import i18n from "@/i18n";
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
|
||||
|
||||
@@ -390,12 +390,28 @@ export const TVLogin: React.FC = () => {
|
||||
pinCode?: string,
|
||||
) => {
|
||||
setShowSaveModal(false);
|
||||
if (pendingLogin) {
|
||||
await performLogin(pendingLogin.username, pendingLogin.password, {
|
||||
saveAccount: true,
|
||||
securityType,
|
||||
pinCode,
|
||||
});
|
||||
|
||||
if (pendingLogin && currentServer) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(pendingLogin.username, pendingLogin.password, serverName, {
|
||||
saveAccount: true,
|
||||
securityType,
|
||||
pinCode,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(t("login.connection_failed"), error.message);
|
||||
} else {
|
||||
Alert.alert(
|
||||
t("login.connection_failed"),
|
||||
t("login.an_unexpected_error_occured"),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setPendingLogin(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
8
modules/tv-user-profile/expo-module.config.json
Normal file
8
modules/tv-user-profile/expo-module.config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "tv-user-profile",
|
||||
"version": "1.0.0",
|
||||
"platforms": ["apple"],
|
||||
"apple": {
|
||||
"modules": ["TvUserProfileModule"]
|
||||
}
|
||||
}
|
||||
103
modules/tv-user-profile/index.ts
Normal file
103
modules/tv-user-profile/index.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { EventSubscription } from "expo-modules-core";
|
||||
import { Platform, requireNativeModule } from "expo-modules-core";
|
||||
|
||||
interface TvUserProfileModuleEvents {
|
||||
onProfileChange: (event: { profileId: string | null }) => void;
|
||||
}
|
||||
|
||||
interface TvUserProfileModuleType {
|
||||
getCurrentProfileId(): string | null;
|
||||
isProfileSwitchingSupported(): boolean;
|
||||
presentUserPicker(): Promise<boolean>;
|
||||
addListener<K extends keyof TvUserProfileModuleEvents>(
|
||||
eventName: K,
|
||||
listener: TvUserProfileModuleEvents[K],
|
||||
): EventSubscription;
|
||||
}
|
||||
|
||||
// Only load the native module on Apple platforms
|
||||
const TvUserProfileModule: TvUserProfileModuleType | null =
|
||||
Platform.OS === "ios"
|
||||
? requireNativeModule<TvUserProfileModuleType>("TvUserProfile")
|
||||
: null;
|
||||
|
||||
/**
|
||||
* Get the current tvOS profile identifier.
|
||||
* Returns null on non-tvOS platforms or if no profile is active.
|
||||
*/
|
||||
export function getCurrentProfileId(): string | null {
|
||||
if (!TvUserProfileModule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return TvUserProfileModule.getCurrentProfileId() ?? null;
|
||||
} catch (error) {
|
||||
console.error("[TvUserProfile] Error getting profile ID:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tvOS profile switching is supported on this device.
|
||||
* Returns true only on tvOS.
|
||||
*/
|
||||
export function isProfileSwitchingSupported(): boolean {
|
||||
if (!TvUserProfileModule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return TvUserProfileModule.isProfileSwitchingSupported();
|
||||
} catch (error) {
|
||||
console.error("[TvUserProfile] Error checking profile support:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to profile change events.
|
||||
* The callback receives the new profile ID (or null if no profile).
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
export function addProfileChangeListener(
|
||||
callback: (profileId: string | null) => void,
|
||||
): () => void {
|
||||
if (!TvUserProfileModule) {
|
||||
// Return no-op unsubscribe on unsupported platforms
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const subscription = TvUserProfileModule.addListener(
|
||||
"onProfileChange",
|
||||
(event) => {
|
||||
callback(event.profileId);
|
||||
},
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the system user picker panel.
|
||||
* Returns true if successful, false otherwise.
|
||||
*/
|
||||
export async function presentUserPicker(): Promise<boolean> {
|
||||
if (!TvUserProfileModule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await TvUserProfileModule.presentUserPicker();
|
||||
} catch (error) {
|
||||
console.error("[TvUserProfile] Error presenting user picker:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getCurrentProfileId,
|
||||
isProfileSwitchingSupported,
|
||||
addProfileChangeListener,
|
||||
presentUserPicker,
|
||||
};
|
||||
23
modules/tv-user-profile/ios/TvUserProfile.podspec
Normal file
23
modules/tv-user-profile/ios/TvUserProfile.podspec
Normal file
@@ -0,0 +1,23 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'TvUserProfile'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'tvOS User Profile Management for Expo'
|
||||
s.description = 'Native tvOS module to get current user profile and listen for profile changes using TVUserManager'
|
||||
s.author = ''
|
||||
s.homepage = 'https://docs.expo.dev/modules/'
|
||||
s.platforms = { :ios => '15.6', :tvos => '15.0' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# TVServices framework is only available on tvOS
|
||||
s.tvos.frameworks = 'TVServices'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
103
modules/tv-user-profile/ios/TvUserProfileModule.swift
Normal file
103
modules/tv-user-profile/ios/TvUserProfileModule.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import ExpoModulesCore
|
||||
#if os(tvOS)
|
||||
import TVServices
|
||||
#endif
|
||||
|
||||
public class TvUserProfileModule: Module {
|
||||
#if os(tvOS)
|
||||
private let userManager = TVUserManager()
|
||||
private var profileObservation: NSKeyValueObservation?
|
||||
#endif
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("TvUserProfile")
|
||||
|
||||
// Define event that can be sent to JavaScript
|
||||
Events("onProfileChange")
|
||||
|
||||
// Get current tvOS profile identifier
|
||||
Function("getCurrentProfileId") { () -> String? in
|
||||
#if os(tvOS)
|
||||
let identifier = self.userManager.currentUserIdentifier
|
||||
print("[TvUserProfile] Current profile ID: \(identifier ?? "nil")")
|
||||
return identifier
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if running on tvOS with profile support
|
||||
Function("isProfileSwitchingSupported") { () -> Bool in
|
||||
#if os(tvOS)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
// Present the system user picker
|
||||
AsyncFunction("presentUserPicker") { () -> Bool in
|
||||
#if os(tvOS)
|
||||
return await withCheckedContinuation { continuation in
|
||||
DispatchQueue.main.async {
|
||||
self.userManager.presentProfilePreferencePanel { error in
|
||||
if let error = error {
|
||||
print("[TvUserProfile] Error presenting user picker: \(error)")
|
||||
continuation.resume(returning: false)
|
||||
} else {
|
||||
print("[TvUserProfile] User picker presented, new ID: \(self.userManager.currentUserIdentifier ?? "nil")")
|
||||
continuation.resume(returning: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
OnCreate {
|
||||
#if os(tvOS)
|
||||
self.setupProfileObserver()
|
||||
#endif
|
||||
}
|
||||
|
||||
OnDestroy {
|
||||
#if os(tvOS)
|
||||
self.profileObservation?.invalidate()
|
||||
self.profileObservation = nil
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private func setupProfileObserver() {
|
||||
// Debug: Print all available info about TVUserManager
|
||||
print("[TvUserProfile] TVUserManager created")
|
||||
print("[TvUserProfile] currentUserIdentifier: \(userManager.currentUserIdentifier ?? "nil")")
|
||||
if #available(tvOS 16.0, *) {
|
||||
print("[TvUserProfile] shouldStorePreferencesForCurrentUser: \(userManager.shouldStorePreferencesForCurrentUser)")
|
||||
}
|
||||
|
||||
// Set up KVO observation on currentUserIdentifier
|
||||
profileObservation = userManager.observe(\.currentUserIdentifier, options: [.new, .old, .initial]) { [weak self] manager, change in
|
||||
guard let self = self else { return }
|
||||
|
||||
let newProfileId = change.newValue ?? nil
|
||||
let oldProfileId = change.oldValue ?? nil
|
||||
|
||||
print("[TvUserProfile] KVO fired - old: \(oldProfileId ?? "nil"), new: \(newProfileId ?? "nil")")
|
||||
|
||||
// Only send event if the profile actually changed
|
||||
if newProfileId != oldProfileId {
|
||||
print("[TvUserProfile] Profile changed from \(oldProfileId ?? "nil") to \(newProfileId ?? "nil")")
|
||||
self.sendEvent("onProfileChange", [
|
||||
"profileId": newProfileId as Any
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
print("[TvUserProfile] Profile observer set up successfully")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
21
plugins/withTVUserManagement.js
Normal file
21
plugins/withTVUserManagement.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { withEntitlementsPlist } = require("expo/config-plugins");
|
||||
|
||||
/**
|
||||
* Expo config plugin to add User Management entitlement for tvOS profile linking
|
||||
*/
|
||||
const withTVUserManagement = (config) => {
|
||||
return withEntitlementsPlist(config, (config) => {
|
||||
// Only add for tvOS builds (check if building for TV)
|
||||
// The entitlement is needed for TVUserManager.currentUserIdentifier to work
|
||||
config.modResults["com.apple.developer.user-management"] = [
|
||||
"runs-as-current-user",
|
||||
"get-current-user",
|
||||
];
|
||||
|
||||
console.log("[withTVUserManagement] Added user-management entitlement");
|
||||
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = withTVUserManagement;
|
||||
Reference in New Issue
Block a user