Files
stickercloner/PLAN.md

6.0 KiB

Plan: standalone iOS app + invisible iMessage sticker extension

Goal

  • iOS app on the home screen: paste Telegram URL, download packs, preview stickers, toggle individual stickers on/off
  • iMessage extension is invisible — no custom UI. It serves stickers via MSStickerBrowserViewController so they appear in the system sticker picker (emoji keyboard → stickers tab on iOS 17+)
  • Data flows from the iOS app → shared App Group container → extension reads it

Why the 404 happens

The backend always calls manager.getPack() (Telegram Bot API) before checking its cache. If the bot token is invalid or Telegram is unreachable, it returns None → 404, even when sticker files exist on disk. This is a backend bug — fix separately. The iOS URL construction is correct.

Architecture

┌─────────────────────────┐     ┌──────────────────────────┐
│  iOS App (home screen)  │     │  iMessage Extension      │
│                         │     │  (no custom UI)          │
│  - Paste URL            │     │                          │
│  - Download packs       │ ──► │  MSStickerBrowserVC      │
│  - Preview stickers     │     │  reads shared container  │
│  - Toggle on/off        │     │  → stickers appear in    │
│  - Delete packs         │     │    system sticker picker  │
└─────────────────────────┘     └──────────────────────────┘
         │                                  │
         └──────── App Group ───────────────┘
           UserDefaults(suiteName:)
           shared Caches/stickers/

File layout after changes

StickerCloner/
├── Shared/                          ← NEW synced folder, both targets
│   ├── Models.swift                   moved + updated
│   └── StickerStore.swift             moved + rewritten (no Messages import)
│
├── StickerCloner/                   ← iOS app target (auto-synced)
│   ├── Assets.xcassets/               existing
│   ├── StickerClonerApp.swift         NEW — SwiftUI @main entry
│   ├── PackListView.swift             NEW — list of packs + URL input
│   ├── PackDetailView.swift           NEW — sticker grid with toggles
│   └── StickerClonerAPI.swift         moved from extension (only app needs it)
│
├── StickerCloner MessagesExtension/ ← extension target (auto-synced)
│   ├── Assets.xcassets/               existing
│   ├── Base.lproj/MainInterface.storyboard  existing
│   ├── Info.plist                     existing
│   ├── MessagesViewController.swift   rewritten (minimal)
│   └── StickerBrowserViewController.swift  rewritten (reads shared data)
│
└── StickerCloner.xcodeproj/

Project file (pbxproj) changes

  1. Change main target product type: com.apple.product-type.application.messagescom.apple.product-type.application
  2. Add Sources build phase to main target (empty — synced groups handle inclusion)
  3. Add Shared/ PBXFileSystemSynchronizedRootGroup to both targets' fileSystemSynchronizedGroups
  4. Add App Group entitlements via CODE_SIGN_ENTITLEMENTS build setting on both targets

Data model changes

Models.swift (Shared/)

// API types (unchanged)
struct StickerSetResponse: Codable { ... }
struct StickerResponse: Codable { ... }

// Local persistence — now tracks enabled stickers
struct SavedPack: Codable {
	let name: String
	let title: String
	let stickerIds: [String]        // all sticker IDs in order
	var enabledStickerIds: Set<String>  // subset the user wants
}

StickerStore.swift (Shared/)

  • Uses UserDefaults(suiteName: "group.de.felixfoertsch.StickerCloner") instead of .standard
  • Uses FileManager.containerURL(forSecurityApplicationGroupIdentifier:) instead of caches directory
  • stickerFileURLs(for pack:) returns [URL] (not MSSticker — no Messages dependency)
  • Returns only enabled stickers

StickerClonerAPI.swift (iOS app only)

Stays in the app target — the extension never calls the API. Unchanged except move location.

iOS App views

StickerClonerApp.swift

  • @main SwiftUI app
  • Single PackListView as root in NavigationStack

PackListView.swift

  • Text field + "Add" button to paste Telegram URL or pack name
  • List of saved packs with swipe-to-delete
  • Navigation link to PackDetailView per pack
  • Loading/error states during download

PackDetailView.swift

  • Grid of sticker thumbnails (loaded from local PNG files)
  • Tap to toggle enabled/disabled (dimmed + checkmark overlay)
  • "Select All" / "Deselect All" toolbar buttons
  • Pack title in navigation bar

Extension changes

MessagesViewController.swift

  • Hosts StickerBrowserViewController as child, fills entire view
  • No expanded mode handling, no SwiftUI views
  • Reloads stickers on willBecomeActive

StickerBrowserViewController.swift

  • Loads enabled sticker file URLs from StickerStore
  • Creates MSSticker from each URL
  • Standard MSStickerBrowserViewController data source

Entitlements

Both targets get identical entitlements:

<key>com.apple.security.application-groups</key>
<array>
    <string>group.de.felixfoertsch.StickerCloner</string>
</array>

Commits

  1. convert to standalone iOS app, add shared folder, app group entitlements
  2. add iOS app ui: pack list, pack detail with sticker filtering
  3. simplify iMessage extension to read-only sticker browser

Verification

  1. Build in Xcode → no compile errors for both targets
  2. iOS app appears on home screen
  3. Paste pack URL → downloads (once backend 404 is fixed)
  4. Tap pack → see sticker grid, toggle individual stickers
  5. Open Messages → stickers appear in sticker picker
  6. Only enabled stickers shown
  7. Kill app, reopen → state persists via App Group