modules/expo-bluesky-swiss-army/README.md
A collection of native utilities for the Bluesky Social app. This Expo module provides platform-specific functionality that is not available through standard React Native APIs.
This module consolidates several native features into a single Expo module:
Provides platform-specific information and audio session control.
Functions:
getIsReducedMotionEnabled(): boolean - Returns whether the user has enabled reduced motion in system settings. Works on all platforms (iOS uses UIAccessibility, Android checks transition animation scale, Web checks CSS media query).
setAudioActive(active: boolean): void - iOS only. Controls whether the app's audio session is active. When deactivated with false, it notifies other apps to resume their audio playback.
setAudioCategory(category: AudioCategory): void - iOS only. Sets the AVAudioSession category. Use AudioCategory.Playback for video/music playback and AudioCategory.Ambient for audio that mixes with other apps.
Platform Support:
getIsReducedMotionEnabled() onlygetIsReducedMotionEnabled() onlyTracks how users arrive at the app from external sources.
Functions:
getReferrerInfo(): ReferrerInfo | null - Returns information about the source that launched the app. Returns {referrer: string, hostname: string} or null.
document.referrer (excludes bsky.app domain)getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> - Android only. Retrieves Google Play install referrer information including install timestamp and click timestamp. Uses the Google Play Install Referrer API.
Platform Support:
getReferrerInfo() only (reads from SharedPrefs)getReferrerInfo() onlyNative key-value storage that persists across app restarts. Uses iOS App Groups (group.app.bsky) for sharing data with extensions, and Android SharedPreferences.
Functions:
setValue(key: string, value: string | number | boolean | null | undefined): void - Store a valueremoveValue(key: string): void - Remove a valuegetString(key: string): string | undefined - Get a string valuegetNumber(key: string): number | undefined - Get a number valuegetBool(key: string): boolean | undefined - Get a boolean valueaddToSet(key: string, value: string): void - Add a value to a setremoveFromSet(key: string, value: string): void - Remove a value from a setsetContains(key: string, value: string): boolean - Check if a set contains a valueDefault Values (Android only): The Android implementation initializes certain keys with default values on first access:
playSoundChat: trueplaySoundFollow: falseplaySoundLike: falseplaySoundMention: falseplaySoundQuote: falseplaySoundReply: falseplaySoundRepost: falsebadgeCount: 0Platform Support:
Implementation Notes:
group.app.bsky to share preferences with app extensionsxyz.blueskyweb.appJavaScriptValue.isString() can cause crashes, so there's a separate setString function internallyA React Native view component that detects which view is currently "active" based on visibility and position on screen. Only one view can be active at a time across the entire app.
Component:
<VisibilityView
enabled={boolean}
onChangeStatus={(isActive: boolean) => void}
>
{children}
</VisibilityView>
Props:
enabled: boolean - Whether this view participates in visibility trackingonChangeStatus: (isActive: boolean) => void - Callback fired when the view becomes active or inactivechildren: React.ReactNode - Child componentsFunctions:
updateActiveViewAsync(): Promise<void> - Manually trigger recalculation of the active viewHow It Works:
The module maintains a global registry of all VisibilityView instances. When views are added/removed or when explicitly updated, it calculates which view is "most visible":
This is useful for features like video autoplay, where you want to know which video is currently the "primary" one the user is viewing.
Platform Support:
The module uses platform-specific file extensions to provide appropriate implementations:
index.ts - Throws NotImplementedError (base/fallback)index.native.ts - Calls native modules via Expo Modules Coreindex.web.ts - Web-specific implementations or stubsindex.ios.ts / index.android.ts - Platform-specific implementations when behavior differsiOS:
Android:
expo.modules.blueskyswissarmy.[feature]index.ts - Main module exportssrc/NotImplemented.ts - Error thrown when functionality is not available on current platformsrc/[Feature]/types.ts - TypeScript type definitions for each featuresrc/[Feature]/index.*.ts - Platform-specific implementationsios/ExpoBlueskySwissArmy.podspec - CocoaPods specificationios/[Feature]/Expo*Module.swift - Expo module definitionsios/SharedPrefs/SharedPrefs.swift - Shared preference manager (usable from other native code)ios/Visibility/VisibilityViewManager.swift - Global view tracking managerandroid/build.gradle - Gradle build configuration (includes installreferrer dependency)android/src/main/java/expo/modules/blueskyswissarmy/[feature]/Expo*Module.kt - Expo module definitionsandroid/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt - Shared preference managerandroid/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt - Global view tracking managerThe module is registered in expo-module.config.json with all four sub-modules for both iOS and Android.
Requires iOS 13.4 or later. Uses the App Group group.app.bsky for SharedPrefs - ensure this is configured in your app's entitlements.
com.android.installreferrer:installreferrer:2.2 dependency for Google Play referrer trackingimport {
PlatformInfo,
AudioCategory,
Referrer,
SharedPrefs,
VisibilityView
} from 'expo-bluesky-swiss-army'
// Check for reduced motion
const isReducedMotion = PlatformInfo.getIsReducedMotionEnabled()
// Set audio category for video playback (iOS)
PlatformInfo.setAudioCategory(AudioCategory.Playback)
PlatformInfo.setAudioActive(true)
// Check how user arrived at the app
const referrer = Referrer.getReferrerInfo()
if (referrer) {
console.log('User came from:', referrer.hostname)
}
// Store a preference
SharedPrefs.setValue('lastOpenedAt', Date.now())
SharedPrefs.setValue('hasSeenOnboarding', true)
// Track visible view
<VisibilityView
enabled={true}
onChangeStatus={(isActive) => {
if (isActive) {
// This view is now the primary visible view
video.play()
} else {
video.pause()
}
}}
>
<VideoPlayer />
</VisibilityView>
Current version: 0.6.0