expo-bluesky-swiss-army#
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.
Overview#
This module consolidates several native features into a single Expo module:
- PlatformInfo: Platform-specific accessibility and audio session management
- Referrer: Tracking how users arrive at the app (web referrers, app referrers, Google Play install referrer)
- SharedPrefs: Shared preferences storage using native platform APIs (UserDefaults on iOS, SharedPreferences on Android)
- VisibilityView: A native view component that tracks which view is currently visible on screen
Modules#
PlatformInfo#
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 withfalse, it notifies other apps to resume their audio playback. -
setAudioCategory(category: AudioCategory): void- iOS only. Sets the AVAudioSession category. UseAudioCategory.Playbackfor video/music playback andAudioCategory.Ambientfor audio that mixes with other apps.
Platform Support:
- iOS: Full support for all functions
- Android:
getIsReducedMotionEnabled()only - Web:
getIsReducedMotionEnabled()only
Referrer#
Tracks 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}ornull.- iOS: Reads from SharedPrefs (set by app extensions or deep link handlers)
- Android: Extracts referrer from Intent extras or activity referrer
- Web: Parses
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:
- iOS:
getReferrerInfo()only (reads from SharedPrefs) - Android: Both functions
- Web:
getReferrerInfo()only
SharedPrefs#
Native 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 value
Default Values (Android only): The Android implementation initializes certain keys with default values on first access:
playSoundChat: trueplaySoundFollow: falseplaySoundLike: falseplaySoundMention: falseplaySoundQuote: falseplaySoundReply: falseplaySoundRepost: falsebadgeCount: 0
Platform Support:
- iOS: Full support (uses UserDefaults with App Group)
- Android: Full support (uses SharedPreferences)
- Web: Not implemented
Implementation Notes:
- iOS uses App Group suite
group.app.bskyto share preferences with app extensions - Android stores preferences in
xyz.blueskyweb.app - Both platforms work around a bug where
JavaScriptValue.isString()can cause crashes, so there's a separatesetStringfunction internally
VisibilityView#
A 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 components
Functions:
updateActiveViewAsync(): Promise<void>- Manually trigger recalculation of the active view
How 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":
- A view must be at least 50% visible on screen
- If multiple views meet this threshold, the one closest to the top of the screen wins (specifically, the one with the lowest Y position, but must be at least 150px from the top)
- Only one view can be active at a time - when a new view becomes active, the previous one is deactivated
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:
- iOS: Full support using UIView position tracking
- Android: Full support using View position tracking
- Web: Passthrough component (renders children without tracking)
Architecture#
TypeScript Layer#
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 differs
Native Layer#
iOS:
- Swift implementation using Expo Modules Core
- Files organized by feature in subdirectories (PlatformInfo/, Referrer/, SharedPrefs/, Visibility/)
- Uses standard iOS APIs: UIAccessibility, AVAudioSession, UserDefaults, UIView
Android:
- Kotlin implementation using Expo Modules Core
- Package structure:
expo.modules.blueskyswissarmy.[feature] - Uses standard Android APIs: Settings.Global, InstallReferrerClient, SharedPreferences, View
Key Files#
TypeScript#
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 implementations
iOS#
ios/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 manager
Android#
android/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 manager
Configuration#
Expo Module Config#
The module is registered in expo-module.config.json with all four sub-modules for both iOS and Android.
iOS#
Requires iOS 13.4 or later. Uses the App Group group.app.bsky for SharedPrefs - ensure this is configured in your app's entitlements.
Android#
- Minimum SDK: 21
- Target SDK: 34
- Requires
com.android.installreferrer:installreferrer:2.2dependency for Google Play referrer tracking
Usage Example#
import {
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>
Version#
Current version: 0.6.0