refactor(tv): simplify user profile management with automatic sandboxing

This commit is contained in:
Fredrik Burmester
2026-01-31 17:28:15 +01:00
parent 717186e13e
commit 44b7434cdd
8 changed files with 281 additions and 7 deletions

View File

@@ -0,0 +1,8 @@
{
"name": "tv-user-profile",
"version": "1.0.0",
"platforms": ["apple"],
"apple": {
"modules": ["TvUserProfileModule"]
}
}

View 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,
};

View 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

View 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
}