share button research (#623)

authored by zzstoatzz.io and committed by GitHub ea491745 b1a2d2de

+157
docs/research/2025-12-19-ios-share-extension-minimal-app.md
··· 1 + # research: minimal iOS app with share extension 2 + 3 + **date**: 2025-12-19 4 + **question**: how to build a minimal iOS app with share extension for uploading audio to plyr.fm API, for someone with no iOS experience 5 + 6 + ## summary 7 + 8 + **yes, it's doable.** native Swift is the best approach - simpler than cross-platform for this use case. you need a minimal "host app" (Apple requires it) but it can just be login + settings. the share extension does the real work. estimated 4-8 weeks for someone learning Swift as they go. 9 + 10 + ## the reality check 11 + 12 + | requirement | detail | 13 + |-------------|--------| 14 + | cost | $99/year Apple Developer account + Mac | 15 + | timeline | 4-8 weeks (learning + building) | 16 + | complexity | medium - Swift is approachable | 17 + | app store approval | achievable if main app has real UI | 18 + 19 + ## architecture 20 + 21 + ``` 22 + plyr-ios/ 23 + ├── PlyrApp/ # main app target (~200 lines) 24 + │ ├── AuthView.swift # OAuth login 25 + │ ├── SettingsView.swift # preferences 26 + │ └── ProfileView.swift # account info 27 + 28 + ├── PlyrShareExtension/ # share extension (~300 lines) 29 + │ ├── ShareViewController.swift 30 + │ └── AudioUploadManager.swift 31 + 32 + └── PlyrShared/ # shared code 33 + ├── APIClient.swift # HTTP requests 34 + ├── KeychainManager.swift # token storage 35 + └── Constants.swift 36 + ``` 37 + 38 + ## how it works 39 + 40 + 1. **user installs app** → opens once → logs in via OAuth 41 + 2. **token stored** in iOS Keychain (shared between app + extension) 42 + 3. **user records voice memo** → taps share → sees "plyr.fm" 43 + 4. **share extension** reads token from Keychain, uploads audio to API 44 + 5. **done** - no need to open main app again 45 + 46 + ## key constraints 47 + 48 + | constraint | value | implication | 49 + |------------|-------|-------------| 50 + | memory limit | ~120 MB | stream file, don't load into memory | 51 + | time limit | ~30 seconds | fine for most audio, use background upload for large files | 52 + | extension can't do OAuth | - | main app handles login, extension reads stored token | 53 + 54 + ## authentication flow 55 + 56 + ``` 57 + Main App: 58 + 1. User taps "Log in with Bluesky" 59 + 2. OAuth flow via browser/webview 60 + 3. Receive token, store in shared Keychain 61 + 62 + Share Extension: 63 + 1. Check Keychain for token 64 + 2. If missing → "Open app to log in" button 65 + 3. If present → upload directly to plyr.fm API 66 + ``` 67 + 68 + ## two implementation paths 69 + 70 + ### path A: direct upload (simpler, 4-6 weeks) 71 + 72 + - extension uploads file directly via URLSession 73 + - shows progress UI in extension 74 + - good for files under ~20MB 75 + - risk: 30-second timeout on slow connections 76 + 77 + ### path B: background upload (robust, 6-8 weeks) 78 + 79 + - extension saves file to shared container 80 + - hands off to background URLSession 81 + - main app can show upload queue 82 + - handles large files, retries on failure 83 + - professional quality 84 + 85 + **recommendation**: start with path A, upgrade to B if needed. 86 + 87 + ## app store approval 88 + 89 + Apple requires the main app to have "independent value" - can't be empty shell. minimum viable: 90 + - login/logout UI 91 + - settings screen 92 + - profile/account view 93 + - maybe upload history 94 + 95 + this is enough to pass review. many apps do exactly this pattern. 96 + 97 + ## what you need to learn 98 + 99 + 1. **Swift basics** - 1-2 weeks of tutorials 100 + 2. **SwiftUI** - for building UI (modern, easier than UIKit) 101 + 3. **URLSession** - for HTTP requests 102 + 4. **Keychain** - for secure token storage 103 + 5. **App Groups** - for sharing data between app + extension 104 + 105 + ## example code: share extension upload 106 + 107 + ```swift 108 + class ShareViewController: SLComposeServiceViewController { 109 + override func didSelectPost() { 110 + guard let item = extensionContext?.inputItems.first as? NSExtensionItem, 111 + let attachment = item.attachments?.first else { return } 112 + 113 + attachment.loadFileRepresentation(forTypeIdentifier: "public.audio") { url, error in 114 + guard let url = url else { return } 115 + 116 + // Get auth token from shared Keychain 117 + let token = KeychainManager.shared.getToken() 118 + 119 + // Upload to plyr.fm API 120 + APIClient.shared.uploadTrack(fileURL: url, token: token) { result in 121 + self.extensionContext?.completeRequest(returningItems: nil) 122 + } 123 + } 124 + } 125 + } 126 + ``` 127 + 128 + ## plyr.fm specific considerations 129 + 130 + - **OAuth**: plyr.fm uses ATProto OAuth - need to handle in main app 131 + - **API endpoint**: `POST /api/tracks/upload` (or similar) 132 + - **token refresh**: share extension should handle expired tokens gracefully 133 + - **metadata**: could add title/artist fields in share extension UI 134 + 135 + ## alternative: iOS Shortcuts 136 + 137 + if native app feels too heavy, you could create a Shortcut that: 138 + 1. user selects audio file 139 + 2. shortcut calls plyr.fm upload API 140 + 3. user shares shortcut via iCloud link 141 + 142 + **pros**: no app store, no $99/year 143 + **cons**: users must manually install shortcut, less discoverable, clunkier UX 144 + 145 + ## open questions 146 + 147 + - does plyr.fm API support multipart file upload from iOS? 148 + - what metadata should share extension collect? (title, tags, etc.) 149 + - should extension show upload progress or dismiss immediately? 150 + - worth building Android version too? (share target works there) 151 + 152 + ## learning resources 153 + 154 + - [Apple: App Extension Programming Guide](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Share.html) 155 + - [AppCoda: Building Share Extension](https://www.appcoda.com/ios8-share-extension-swift/) 156 + - [Hacking with Swift: 100 Days of SwiftUI](https://www.hackingwithswift.com/100/swiftui) (free) 157 + - [Stanford CS193p: iOS Development](https://cs193p.sites.stanford.edu/) (free)
+63
docs/research/2025-12-19-web-share-target-ios-pwa.md
··· 1 + # research: Web Share Target API for iOS PWAs 2 + 3 + **date**: 2025-12-19 4 + **question**: can a PWA receive shared audio files from the iOS share sheet (like SoundCloud does)? 5 + 6 + ## summary 7 + 8 + **No, iOS Safari does not support Web Share Target API.** This is a known limitation with an open WebKit bug (#194593) since February 2019 - no timeline for implementation. Android Chrome fully supports it. The SoundCloud iOS share integration you saw works because SoundCloud has a native iOS app, not a PWA. 9 + 10 + ## findings 11 + 12 + ### iOS Safari limitations 13 + 14 + - Web Share Target API (receiving files) - **NOT supported** 15 + - Web Share API (sending/sharing out) - supported 16 + - WebKit bug #194593 tracks this, last activity May 2025, no assignee 17 + - Apple mandates WebKit for all iOS browsers, so no workaround via Chrome/Firefox 18 + - This is why plyr.fm can't appear in the iOS share sheet as a destination 19 + 20 + ### Android support 21 + 22 + - Chrome fully supports Web Share Target since ~2019 23 + - manifest.json `share_target` field enables receiving shared files 24 + - Would work for Android users installing plyr.fm PWA 25 + 26 + ### current plyr.fm state 27 + 28 + - `frontend/static/manifest.webmanifest` - basic PWA config, no share_target 29 + - `frontend/src/lib/components/ShareButton.svelte` - clipboard copy only 30 + - no `navigator.share()` usage (even though iOS supports sharing OUT) 31 + 32 + ### workaround options 33 + 34 + 1. **native iOS app** - only real solution for iOS share sheet integration 35 + 2. **Web Share API for outbound** - can add "share to..." button that opens iOS share sheet 36 + 3. **improved in-app UX** - better upload flow, drag-drop on desktop 37 + 4. **PWABuilder wrapper** - publish to App Store, gains URL scheme support 38 + 39 + ## code references 40 + 41 + - `frontend/static/manifest.webmanifest` - would add `share_target` here for Android 42 + - `frontend/src/lib/components/ShareButton.svelte:8-15` - current clipboard-only implementation 43 + 44 + ## potential quick wins 45 + 46 + even without iOS share target support: 47 + 48 + 1. **add navigator.share() for outbound sharing** - let users share tracks TO other apps via native share sheet 49 + 2. **add share_target for Android users** - Android PWA installs would get share sheet integration 50 + 3. **improve upload UX** - streamline the in-app upload flow for mobile 51 + 52 + ## open questions 53 + 54 + - is Android share target support worth implementing given iOS is primary user base? 55 + - would a lightweight native iOS app (just for share extension) be worth maintaining? 56 + - any appetite for PWABuilder/App Store distribution? 57 + 58 + ## sources 59 + 60 + - [WebKit Bug #194593](https://bugs.webkit.org/show_bug.cgi?id=194593) 61 + - [MDN: share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target) 62 + - [web.dev: OS Integration](https://web.dev/learn/pwa/os-integration) 63 + - [firt.dev: iOS PWA Compatibility](https://firt.dev/notes/pwa-ios/)