managarten/apps-archived/memoro/apps/mobile/docs/features/ios-widget-implementation-plan.md
Till-JS 61d181fbc2 chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 07:03:59 +01:00

37 KiB

iOS Widget Implementation Plan für Memoro

Überblick

Dieses Dokument beschreibt die geplante Implementierung von iOS Home Screen Widgets und Live Activities für die Memoro App. Widgets ermöglichen es Nutzern, schnellen Zugriff auf ihre wichtigsten Memos direkt vom Home Screen aus zu haben, ohne die App öffnen zu müssen.

Status: Planning Phase Ziel-Release: TBD Minimum iOS Version: iOS 16.0+ Expo SDK: 54+ (current: SDK 54 ✓)


Motivation & Use Cases

Warum Widgets für Memoro?

  1. Quick Access - Sofortiger Zugriff auf wichtige Memos
  2. Glanceable Information - Überblick über letzte Aufnahmen
  3. Recording Shortcut - Direkt zur Aufnahme-Funktion springen
  4. Engagement - Erhöhte App-Nutzung durch Visibility
  5. iOS Ecosystem Integration - Native iOS Experience

Primäre Use Cases

  1. Latest Memo Widget - Zeigt das zuletzt erstellte Memo
  2. Pinned Memos Widget - Schnellzugriff auf gepinnte Memos
  3. Quick Record Widget - One-Tap Aufnahme-Button
  4. Statistics Widget - Memo-Count und Nutzungsstatistiken
  5. Daily Inspiration Widget - Zufälliges Memo aus der History

Technische Anforderungen

System Requirements

  • macOS 15 Sequoia (Development)
  • Xcode 16 oder höher
  • Expo SDK 54 (bereits vorhanden)
  • iOS 16+ Target Deployment
  • Apple Team ID: ZB76J8YWG6 (bereits konfiguriert)
  • ⚠️ Development Build erforderlich (kein Expo Go Support)

Dependencies

Neue Packages (zu installieren)

# Widget Plugin (empfohlen von Expo Team)
npm install @bacons/apple-targets

# Shared Data zwischen App & Widget
npm install react-native-shared-group-preferences

Existing Dependencies (bereits vorhanden)

  • expo-router - Deep Linking Support
  • zustand mit persist - State Management
  • AsyncStorage - Data Persistence
  • expo-secure-store - Sensitive Data

Geplante Widget-Typen

1. Latest Memo Widget

Beschreibung: Zeigt das zuletzt erstellte Memo mit Titel, Datum und Quick Actions.

Größen:

  • Small (2x2): Nur Titel + Datum
  • Medium (4x2): Titel + Intro + Datum + Space Badge
  • Large (4x4): Titel + Vollständiger Intro + Transcript Preview + Metadata

Daten:

interface LatestMemoWidget {
	memoId: string;
	title: string;
	intro?: string;
	transcriptPreview?: string; // First 200 chars
	createdAt: Date;
	spaceName?: string;
	spaceColor?: string;
	audioLength?: number;
	hasTranscript: boolean;
}

Deep Links:

  • Widget Tap → Öffnet Memo Detail (memoro://memo/{id})
  • Space Badge → Öffnet Space (memoro://space/{id})

2. Pinned Memos Widget

Beschreibung: Grid-Layout mit gepinnten Memos für schnellen Zugriff.

Größen:

  • Small (2x2): 1 Memo (Featured)
  • Medium (4x2): 2-3 Memos (Horizontal Stack)
  • Large (4x4): 4-6 Memos (Grid Layout)

Daten:

interface PinnedMemosWidget {
	memos: Array<{
		id: string;
		title: string;
		snippet: string; // First 50 chars
		isPinned: boolean;
		spaceColor?: string;
		createdAt: Date;
	}>;
	totalCount: number;
}

Features:

  • Tappable Cards → Öffnet jeweiliges Memo
  • Empty State → "No pinned memos" mit Link zu App
  • Sortierung nach Pin-Datum (neueste zuerst)

3. Quick Record Widget

Beschreibung: One-Tap Recording Start mit minimalistischem Design.

Größen:

  • Small (2x2): Großer Aufnahme-Button + Mikrofon Icon
  • Medium (nicht sinnvoll für diesen Type)
  • Large (nicht sinnvoll für diesen Type)

Daten:

interface QuickRecordWidget {
	selectedSpaceId?: string;
	selectedBlueprintId?: string;
	recordingLanguages?: string[];
}

Deep Links:

  • Widget Tap → Öffnet Recording Screen mit vorausgewählten Settings

Visual Design:

  • Großes Mikrofon-Icon (SF Symbol)
  • Gradient Background (Theme Colors)
  • Text: "Start Recording" oder "Aufnehmen"
  • Badge mit letztem Space (optional)

4. Statistics Widget

Beschreibung: Übersicht über Nutzungsstatistiken und Achievements.

Größen:

  • Small (2x2): Memo Count + Trend Icon
  • Medium (4x2): Count + Duration + Last Week Stats
  • Large (4x4): Detailed Stats + Chart + Breakdown by Space

Daten:

interface StatisticsWidget {
	totalMemos: number;
	totalDuration: number; // seconds
	thisWeekCount: number;
	thisMonthCount: number;
	favoriteCount: number;
	spaceBreakdown?: Array<{
		spaceName: string;
		count: number;
		color: string;
	}>;
	longestMemo?: {
		title: string;
		duration: number;
	};
}

Features:

  • Trend Indicators (↑↓)
  • Weekly Progress Bar
  • Tap → Öffnet Statistics Screen (neu zu bauen)

5. Random Inspiration Widget (Phase 2)

Beschreibung: Zeigt ein zufälliges Memo zur Inspiration.

Größen: Alle (Small, Medium, Large)

Update Frequency: Täglich um Mitternacht

Daten:

interface InspirationWidget {
	randomMemo: {
		id: string;
		title: string;
		snippet: string;
		createdAt: Date;
		tags?: string[];
	};
	refreshDate: Date;
}

Implementierungsphasen

Phase 1: Grundlagen (Woche 1-2)

Ziel: Widget Infrastructure & Latest Memo Widget

Tasks:

  1. Setup & Configuration

    • Install @bacons/apple-targets
    • Create widget target: npx create-target widget
    • Configure App Groups in app.json
    • Update bundle identifiers
    • Add entitlements
  2. Data Sharing Layer

    • Create features/widgets/services/widgetDataManager.ts
    • Implement SharedGroupPreferences wrapper
    • Create widget data types
    • Test data sharing in development build
  3. Swift Widget - Latest Memo

    • Create LatestMemoWidget.swift
    • Implement Timeline Provider
    • Design Small/Medium/Large layouts
    • Add placeholder data
    • Test in Simulator
  4. React Native Integration

    • Update memoStore.ts to sync with widget
    • Add widget refresh trigger
    • Implement deep link handling
    • Test end-to-end flow

Deliverable: Functional Latest Memo Widget in all 3 sizes


Phase 2: Additional Widgets (Woche 3-4)

Ziel: Quick Record & Pinned Memos Widgets

Tasks:

  1. Quick Record Widget

    • Create QuickRecordWidget.swift
    • Design small widget layout
    • Implement recording preferences sharing
    • Add intent configuration (optional space/blueprint)
    • Test deep link to recording screen
  2. Pinned Memos Widget

    • Create PinnedMemosWidget.swift
    • Implement grid layout for large widget
    • Add pin/unpin sync from app
    • Design empty state
    • Test with multiple pinned memos
  3. Widget Gallery Assets

    • Create widget preview screenshots
    • Design widget icons
    • Add localized descriptions

Deliverable: 3 functional widget types


Phase 3: Statistics & Polish (Woche 5-6)

Ziel: Statistics Widget & UX Improvements

Tasks:

  1. Statistics Widget

    • Create StatisticsWidget.swift
    • Implement data aggregation in React Native
    • Design chart/visualization for large widget
    • Add trend calculations
    • Build statistics screen in app (new)
  2. Performance Optimization

    • Optimize widget timeline generation
    • Reduce data size for widget sharing
    • Add caching layer
    • Test memory usage
    • Profile widget rendering
  3. UX & Polish

    • Add animations/transitions
    • Implement dark mode support
    • Add localization (DE, EN)
    • Error state handling
    • Offline functionality

Deliverable: Production-ready widgets


Phase 4: Live Activities (Optional - Woche 7-8)

Ziel: Live Activities für aktive Aufnahmen

Features:

  • Live recording duration
  • Pause/Resume controls
  • Waveform visualization
  • Recording language indicator

Status: Nice-to-have, nicht kritisch für v1


Architektur

Datenfluss

┌─────────────────────────────────────────────────────┐
│                  Memoro React Native App             │
│  ┌───────────────────────────────────────────────┐  │
│  │  Zustand Stores (memoStore, spaceStore, etc) │  │
│  └──────────────┬───────────────────────────────┘  │
│                 │                                    │
│  ┌──────────────▼──────────────────────────────┐   │
│  │   WidgetDataManager (TypeScript)            │   │
│  │   - Prepares data for widget                │   │
│  │   - Calls SharedGroupPreferences            │   │
│  │   - Triggers widget refresh                 │   │
│  └──────────────┬──────────────────────────────┘   │
└─────────────────┼──────────────────────────────────┘
                  │
                  │ SharedGroupPreferences
                  │ (App Group: group.com.memo.beta.widget)
                  │
┌─────────────────▼──────────────────────────────────┐
│          iOS Widget Extension (Swift)              │
│  ┌──────────────────────────────────────────────┐ │
│  │   Timeline Provider                          │ │
│  │   - Reads from UserDefaults (App Group)     │ │
│  │   - Generates timeline entries               │ │
│  │   - Schedules updates                        │ │
│  └──────────────┬───────────────────────────────┘ │
│                 │                                  │
│  ┌──────────────▼───────────────────────────────┐ │
│  │   Widget Views (SwiftUI)                    │ │
│  │   - LatestMemoWidget                        │ │
│  │   - PinnedMemosWidget                       │ │
│  │   - QuickRecordWidget                       │ │
│  │   - StatisticsWidget                        │ │
│  └─────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘

Verzeichnisstruktur

memoro/
├── app.json                         # App Group & Plugin Config
├── targets/                         # Widget Extension
│   └── widget/
│       ├── expo-target.config.js    # Widget Configuration
│       ├── LatestMemoWidget.swift   # Latest Memo Widget
│       ├── PinnedMemosWidget.swift  # Pinned Memos Widget
│       ├── QuickRecordWidget.swift  # Quick Record Widget
│       ├── StatisticsWidget.swift   # Statistics Widget
│       ├── WidgetBundle.swift       # Widget Collection
│       ├── Models.swift             # Shared Data Models
│       ├── WidgetView.swift         # Common UI Components
│       └── Assets.xcassets/         # Widget Assets
│
├── features/
│   └── widgets/
│       ├── services/
│       │   └── widgetDataManager.ts     # Data Sharing Service
│       ├── types/
│       │   └── widget.types.ts          # TypeScript Types
│       ├── hooks/
│       │   ├── useWidgetData.ts         # Widget Data Hook
│       │   └── useWidgetRefresh.ts      # Refresh Hook
│       └── index.ts                     # Public API
│
├── docs/
│   └── features/
│       ├── ios-widget-implementation-plan.md (this file)
│       └── ios-widget-user-guide.md     # End-user documentation
│
└── assets/
    └── widgets/
        ├── widget-icon.png
        ├── widget-icon@2x.png
        └── widget-icon@3x.png

Konfiguration

app.json Updates

{
	"expo": {
		"ios": {
			"bundleIdentifier": "com.memo.beta",
			"appleTeamId": "ZB76J8YWG6",
			"entitlements": {
				"com.apple.security.application-groups": ["group.com.memo.beta.widget"]
			}
		},
		"plugins": [
			// ... existing plugins ...
			[
				"@bacons/apple-targets",
				{
					"appleTeamId": "ZB76J8YWG6"
				}
			]
		]
	}
}

Widget Target Config (targets/widget/expo-target.config.js)

/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module.exports = {
	type: 'widget',
	name: 'MemoroWidget',
	bundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER).widget',
	deploymentTarget: '16.0',
	icon: '../../assets/widgets/widget-icon.png',
	colors: {
		$accent: {
			color: '#007AFF', // iOS Blue
			darkColor: '#0A84FF', // iOS Blue Dark
		},
		$widgetBackground: {
			color: '#FFFFFF',
			darkColor: '#1C1C1E',
		},
	},
	entitlements: {
		'com.apple.security.application-groups': ['group.com.memo.beta.widget'],
	},
	frameworks: ['SwiftUI', 'WidgetKit'],
};

Data Sharing Implementation

WidgetDataManager Service

File: features/widgets/services/widgetDataManager.ts

import SharedGroupPreferences from 'react-native-shared-group-preferences';
import { NativeModules, Platform } from 'react-native';

const APP_GROUP = 'group.com.memo.beta.widget';

export interface WidgetMemoData {
	id: string;
	title: string;
	intro?: string;
	transcriptPreview?: string;
	createdAt: string; // ISO format
	spaceName?: string;
	spaceColor?: string;
	audioLength?: number;
	hasTranscript: boolean;
}

export interface WidgetPinnedMemo {
	id: string;
	title: string;
	snippet: string;
	isPinned: boolean;
	spaceColor?: string;
	createdAt: string;
}

export interface WidgetStatistics {
	totalMemos: number;
	totalDuration: number;
	thisWeekCount: number;
	thisMonthCount: number;
	favoriteCount: number;
	spaceBreakdown?: Array<{
		spaceName: string;
		count: number;
		color: string;
	}>;
}

export class WidgetDataManager {
	/**
	 * Update latest memo data for widget
	 */
	static async updateLatestMemo(memo: WidgetMemoData): Promise<void> {
		try {
			const data = JSON.stringify(memo);
			await SharedGroupPreferences.setItem('latestMemo', data, APP_GROUP);
			this.refreshWidget();
		} catch (error) {
			console.error('[WidgetDataManager] Failed to update latest memo:', error);
		}
	}

	/**
	 * Update pinned memos for widget
	 */
	static async updatePinnedMemos(memos: WidgetPinnedMemo[]): Promise<void> {
		try {
			// Only send top 6 pinned memos (for large widget)
			const limitedMemos = memos.slice(0, 6);
			const data = JSON.stringify(limitedMemos);
			await SharedGroupPreferences.setItem('pinnedMemos', data, APP_GROUP);
			this.refreshWidget();
		} catch (error) {
			console.error('[WidgetDataManager] Failed to update pinned memos:', error);
		}
	}

	/**
	 * Update statistics for widget
	 */
	static async updateStatistics(stats: WidgetStatistics): Promise<void> {
		try {
			const data = JSON.stringify(stats);
			await SharedGroupPreferences.setItem('statistics', data, APP_GROUP);
			this.refreshWidget();
		} catch (error) {
			console.error('[WidgetDataManager] Failed to update statistics:', error);
		}
	}

	/**
	 * Update recording preferences for Quick Record widget
	 */
	static async updateRecordingPreferences(preferences: {
		selectedSpaceId?: string;
		selectedBlueprintId?: string;
		recordingLanguages?: string[];
	}): Promise<void> {
		try {
			const data = JSON.stringify(preferences);
			await SharedGroupPreferences.setItem('recordingPreferences', data, APP_GROUP);
			this.refreshWidget();
		} catch (error) {
			console.error('[WidgetDataManager] Failed to update recording preferences:', error);
		}
	}

	/**
	 * Trigger widget refresh (iOS 14+)
	 */
	static refreshWidget(): void {
		if (Platform.OS === 'ios') {
			try {
				NativeModules.WidgetKit?.reloadAllTimelines();
			} catch (error) {
				console.warn('[WidgetDataManager] Could not reload widget timelines:', error);
			}
		}
	}

	/**
	 * Clear all widget data
	 */
	static async clearWidgetData(): Promise<void> {
		try {
			await SharedGroupPreferences.setItem('latestMemo', '', APP_GROUP);
			await SharedGroupPreferences.setItem('pinnedMemos', '', APP_GROUP);
			await SharedGroupPreferences.setItem('statistics', '', APP_GROUP);
			await SharedGroupPreferences.setItem('recordingPreferences', '', APP_GROUP);
			this.refreshWidget();
		} catch (error) {
			console.error('[WidgetDataManager] Failed to clear widget data:', error);
		}
	}
}

Integration mit bestehenden Stores

Memo Store Integration

File: features/memos/store/memoStore.ts

import { WidgetDataManager } from '~/features/widgets/services/widgetDataManager';

// In setLatestMemo action:
setLatestMemo: (memo: Memo | null) => {
	set({ latestMemo: memo });

	// Update widget data
	if (memo) {
		WidgetDataManager.updateLatestMemo({
			id: memo.id,
			title: memo.title,
			intro: memo.metadata?.intro?.substring(0, 200),
			transcriptPreview: memo.source?.transcript?.substring(0, 200),
			createdAt: memo.created_at || new Date().toISOString(),
			spaceName: memo.space?.name,
			spaceColor: memo.space?.color,
			audioLength: memo.source?.duration_seconds,
			hasTranscript: !!memo.source?.transcript,
		});
	}
};

// In togglePin action:
togglePin: async (memoId: string) => {
	// ... existing pin logic ...

	// Update widget with pinned memos
	const pinnedMemos = state.memos.filter((m) => m.is_pinned);
	WidgetDataManager.updatePinnedMemos(
		pinnedMemos.map((m) => ({
			id: m.id,
			title: m.title,
			snippet: m.source?.transcript?.substring(0, 50) || '',
			isPinned: true,
			spaceColor: m.space?.color,
			createdAt: m.created_at || new Date().toISOString(),
		}))
	);
};

Swift Widget Example - Latest Memo

File: targets/widget/LatestMemoWidget.swift

import WidgetKit
import SwiftUI

// MARK: - Data Models
struct MemoWidgetData: Codable {
    let id: String
    let title: String
    let intro: String?
    let transcriptPreview: String?
    let createdAt: String
    let spaceName: String?
    let spaceColor: String?
    let audioLength: Int?
    let hasTranscript: Bool
}

// MARK: - Timeline Entry
struct LatestMemoEntry: TimelineEntry {
    let date: Date
    let memo: MemoWidgetData?
}

// MARK: - Timeline Provider
struct LatestMemoProvider: TimelineProvider {
    let userDefaults = UserDefaults(suiteName: "group.com.memo.beta.widget")

    func placeholder(in context: Context) -> LatestMemoEntry {
        LatestMemoEntry(
            date: Date(),
            memo: MemoWidgetData(
                id: "placeholder",
                title: "Willkommen bei Memoro",
                intro: "Dein letztes Memo erscheint hier",
                transcriptPreview: nil,
                createdAt: ISO8601DateFormatter().string(from: Date()),
                spaceName: nil,
                spaceColor: nil,
                audioLength: nil,
                hasTranscript: false
            )
        )
    }

    func getSnapshot(in context: Context, completion: @escaping (LatestMemoEntry) -> ()) {
        let entry = loadLatestMemo() ?? placeholder(in: context)
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = loadLatestMemo() ?? placeholder(in: context)

        // Update timeline every hour
        let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
        completion(timeline)
    }

    private func loadLatestMemo() -> LatestMemoEntry? {
        guard let data = userDefaults?.data(forKey: "latestMemo"),
              let memo = try? JSONDecoder().decode(MemoWidgetData.self, from: data) else {
            return nil
        }

        return LatestMemoEntry(date: Date(), memo: memo)
    }
}

// MARK: - Widget Views
struct LatestMemoWidgetView: View {
    var entry: LatestMemoProvider.Entry
    @Environment(\.widgetFamily) var family

    var body: some View {
        switch family {
        case .systemSmall:
            SmallLatestMemoView(entry: entry)
        case .systemMedium:
            MediumLatestMemoView(entry: entry)
        case .systemLarge:
            LargeLatestMemoView(entry: entry)
        default:
            EmptyView()
        }
    }
}

// MARK: - Small Widget (2x2)
struct SmallLatestMemoView: View {
    let entry: LatestMemoEntry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Header
            HStack {
                Image(systemName: "waveform")
                    .font(.system(size: 16))
                    .foregroundColor(.blue)
                Text("Latest")
                    .font(.system(size: 12, weight: .semibold))
                    .foregroundColor(.secondary)
                Spacer()
            }

            // Title
            if let memo = entry.memo {
                Text(memo.title)
                    .font(.system(size: 14, weight: .semibold))
                    .lineLimit(2)
                    .foregroundColor(.primary)

                Spacer()

                // Date
                Text(formatDate(memo.createdAt))
                    .font(.system(size: 11))
                    .foregroundColor(.secondary)
            } else {
                Text("No memo yet")
                    .font(.system(size: 14))
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .containerBackground(for: .widget) {
            Color.clear
        }
    }

    private func formatDate(_ isoString: String) -> String {
        let formatter = ISO8601DateFormatter()
        guard let date = formatter.date(from: isoString) else {
            return ""
        }

        let calendar = Calendar.current
        if calendar.isDateInToday(date) {
            return "Heute"
        } else if calendar.isDateInYesterday(date) {
            return "Gestern"
        } else {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "dd.MM.yy"
            return dateFormatter.string(from: date)
        }
    }
}

// MARK: - Medium Widget (4x2)
struct MediumLatestMemoView: View {
    let entry: LatestMemoEntry

    var body: some View {
        HStack(spacing: 12) {
            VStack(alignment: .leading, spacing: 6) {
                // Header with Space Badge
                HStack {
                    Image(systemName: "waveform")
                        .font(.system(size: 14))
                        .foregroundColor(.blue)

                    if let spaceName = entry.memo?.spaceName {
                        Text(spaceName)
                            .font(.system(size: 10, weight: .semibold))
                            .padding(.horizontal, 6)
                            .padding(.vertical, 2)
                            .background(Color(hex: entry.memo?.spaceColor ?? "#007AFF").opacity(0.2))
                            .cornerRadius(4)
                    }

                    Spacer()
                }

                // Title
                if let memo = entry.memo {
                    Text(memo.title)
                        .font(.system(size: 15, weight: .semibold))
                        .lineLimit(2)
                        .foregroundColor(.primary)

                    // Intro
                    if let intro = memo.intro {
                        Text(intro)
                            .font(.system(size: 12))
                            .lineLimit(2)
                            .foregroundColor(.secondary)
                    }

                    Spacer()

                    // Footer
                    HStack {
                        Text(formatDate(memo.createdAt))
                            .font(.system(size: 11))
                            .foregroundColor(.secondary)

                        if let audioLength = memo.audioLength {
                            Text("•")
                                .foregroundColor(.secondary)
                            Text(formatDuration(audioLength))
                                .font(.system(size: 11))
                                .foregroundColor(.secondary)
                        }

                        if memo.hasTranscript {
                            Spacer()
                            Image(systemName: "checkmark.circle.fill")
                                .font(.system(size: 12))
                                .foregroundColor(.green)
                        }
                    }
                }
            }
        }
        .padding()
        .containerBackground(for: .widget) {
            Color.clear
        }
    }

    private func formatDate(_ isoString: String) -> String {
        // Same as SmallWidget
        let formatter = ISO8601DateFormatter()
        guard let date = formatter.date(from: isoString) else { return "" }

        let calendar = Calendar.current
        if calendar.isDateInToday(date) {
            return "Heute"
        } else if calendar.isDateInYesterday(date) {
            return "Gestern"
        } else {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "dd.MM.yy"
            return dateFormatter.string(from: date)
        }
    }

    private func formatDuration(_ seconds: Int) -> String {
        let minutes = seconds / 60
        let remainingSeconds = seconds % 60
        return String(format: "%d:%02d", minutes, remainingSeconds)
    }
}

// MARK: - Large Widget (4x4)
struct LargeLatestMemoView: View {
    let entry: LatestMemoEntry

    var body: some View {
        // Similar to Medium but with more content
        // Add transcript preview, tags, etc.
        MediumLatestMemoView(entry: entry) // Placeholder
    }
}

// MARK: - Widget Configuration
@main
struct LatestMemoWidget: Widget {
    let kind: String = "LatestMemoWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: LatestMemoProvider()) { entry in
            LatestMemoWidgetView(entry: entry)
        }
        .configurationDisplayName("Letztes Memo")
        .description("Zeigt dein zuletzt erstelltes Memo")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        .contentMarginsDisabled()
    }
}

// MARK: - Helper Extension
extension Color {
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&int)
        let a, r, g, b: UInt64
        switch hex.count {
        case 3: // RGB (12-bit)
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6: // RGB (24-bit)
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8: // ARGB (32-bit)
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (a, r, g, b) = (1, 1, 1, 0)
        }

        self.init(
            .sRGB,
            red: Double(r) / 255,
            green: Double(g) / 255,
            blue: Double(b) / 255,
            opacity: Double(a) / 255
        )
    }
}

Deep Linking

URL Scheme

Format: memoro://[screen]/[id]

Examples:

  • memoro://memo/abc-123 → Opens memo detail
  • memoro://space/xyz-789 → Opens space view
  • memoro://record → Opens recording screen
  • memoro://record?space=xyz → Opens recording with pre-selected space

Implementation in App

File: app/_layout.tsx

import { Linking } from 'react-native';
import { useRouter } from 'expo-router';

export default function RootLayout() {
	const router = useRouter();

	useEffect(() => {
		// Handle deep links from widgets
		const handleDeepLink = (url: string) => {
			const route = url.replace('memoro://', '');

			if (route.startsWith('memo/')) {
				const memoId = route.replace('memo/', '');
				router.push(`/(protected)/(memo)/${memoId}`);
			} else if (route.startsWith('space/')) {
				const spaceId = route.replace('space/', '');
				router.push(`/(protected)/(space)/${spaceId}`);
			} else if (route === 'record') {
				router.push('/(protected)/(tabs)/');
			}
		};

		// Listen for deep links
		const subscription = Linking.addEventListener('url', ({ url }) => {
			handleDeepLink(url);
		});

		// Handle initial URL if app was closed
		Linking.getInitialURL().then((url) => {
			if (url) handleDeepLink(url);
		});

		return () => subscription.remove();
	}, [router]);

	// ... rest of layout
}

Testing Strategy

Unit Tests

File: features/widgets/services/__tests__/widgetDataManager.test.ts

import { WidgetDataManager } from '../widgetDataManager';

describe('WidgetDataManager', () => {
	it('should update latest memo data', async () => {
		const mockMemo = {
			id: 'test-123',
			title: 'Test Memo',
			intro: 'Test intro',
			createdAt: new Date().toISOString(),
			hasTranscript: true,
		};

		await WidgetDataManager.updateLatestMemo(mockMemo);
		// Assert SharedGroupPreferences was called
	});

	it('should limit pinned memos to 6', async () => {
		const manyMemos = Array.from({ length: 10 }, (_, i) => ({
			id: `memo-${i}`,
			title: `Memo ${i}`,
			snippet: 'snippet',
			isPinned: true,
			createdAt: new Date().toISOString(),
		}));

		await WidgetDataManager.updatePinnedMemos(manyMemos);
		// Assert only 6 memos were saved
	});
});

Integration Tests

  1. Widget Data Flow Test

    • Create memo in app
    • Verify widget shows updated data
    • Time threshold: < 2 seconds
  2. Deep Link Test

    • Tap widget
    • Verify correct screen opens
    • Verify correct data is displayed
  3. Performance Test

    • Measure widget refresh time
    • Target: < 100ms
    • Test with large datasets

Performance Optimization

Best Practices

  1. Data Size Limits

    • Latest Memo: < 5 KB
    • Pinned Memos: < 10 KB (6 memos max)
    • Statistics: < 3 KB
    • Total widget data: < 20 KB
  2. Timeline Generation

    • Small widgets: 12 entries (12 hours)
    • Medium/Large: 24 entries (24 hours)
    • Update policy: .after(nextHour)
  3. Image Optimization

    • No images in Phase 1
    • Use SF Symbols for icons
    • Future: Compress images to < 50 KB
  4. Memory Usage

    • Widget process limit: 30 MB
    • Keep under 20 MB for safety
    • Profile with Xcode Instruments

Localization

Supported Languages

  • German (DE) - Primary
  • English (EN) - Secondary

Translation Keys

Widget Titles:

  • widget.latest_memo.title = "Letztes Memo" / "Latest Memo"
  • widget.pinned_memos.title = "Gepinnte Memos" / "Pinned Memos"
  • widget.quick_record.title = "Aufnahme" / "Record"
  • widget.statistics.title = "Statistiken" / "Statistics"

Widget Descriptions:

  • widget.latest_memo.description = "Zeigt dein zuletzt erstelltes Memo"
  • widget.pinned_memos.description = "Schnellzugriff auf gepinnte Memos"
  • widget.quick_record.description = "Starte eine Aufnahme mit einem Tap"
  • widget.statistics.description = "Übersicht deiner Nutzungsstatistiken"

UI Text:

  • widget.common.no_memos = "Noch keine Memos" / "No memos yet"
  • widget.common.today = "Heute" / "Today"
  • widget.common.yesterday = "Gestern" / "Yesterday"

Rollout Plan

Beta Testing (2 Wochen)

Phase 1:

  • Internal team testing (5 developers)
  • TestFlight beta with 10 users
  • Collect feedback on:
    • Widget visibility
    • Deep link functionality
    • Data accuracy
    • Performance

Phase 2:

  • Extended TestFlight beta (50 users)
  • Monitor analytics:
    • Widget add rate
    • Widget interaction rate
    • Deep link success rate
    • Crash reports

Production Release

Criteria for Launch:

  • All widgets functional in 3 sizes
  • Deep links working 100%
  • No memory leaks
  • < 0.1% crash rate in beta
  • Localization complete (DE, EN)
  • Documentation complete

Release Notes:

🎉 Neu: iOS Home Screen Widgets!

- Letztes Memo Widget: Sieh dein neuestes Memo auf einen Blick
- Gepinnte Memos Widget: Schnellzugriff auf wichtige Memos
- Schnellaufnahme Widget: Starte eine Aufnahme mit einem Tap
- Statistiken Widget: Überblick über deine Memo-Nutzung

Tippe auf ein Widget, um direkt zur App zu springen!

Success Metrics

Key Performance Indicators (KPIs)

  1. Widget Adoption Rate

    • Target: 30% of users add at least 1 widget within 30 days
    • Measure: Analytics event "widget_added"
  2. Widget Interaction Rate

    • Target: 50% of users with widgets tap them at least weekly
    • Measure: Deep link events
  3. App Engagement

    • Target: 15% increase in app opens via widget
    • Measure: App launch source tracking
  4. Widget Retention

    • Target: 70% widget retention after 30 days
    • Measure: Weekly widget view events
  5. Performance

    • Target: Widget refresh < 100ms
    • Target: < 20 MB memory usage
    • Target: < 0.1% crash rate

Risks & Mitigation

Technical Risks

Risk Impact Probability Mitigation
Widget doesn't update High Medium Comprehensive testing, fallback to manual refresh
Deep links fail High Low URL validation, error handling, fallback to app home
Memory issues Medium Medium Profile early, set size limits, optimize data
Data sync delays Medium Medium Cache last known state, show loading states
Build complexity Low Medium Follow @bacons/apple-targets docs, seek help early

User Experience Risks

Risk Impact Mitigation
Widget confusion Medium Clear labels, contextual help, onboarding
Data privacy concerns High Document data handling, no sensitive data in widgets
Inconsistent UI Low Follow Apple HIG, use system fonts/colors
Outdated widget data Medium Implement refresh logic, show last update time

Future Enhancements (Post-V1)

Phase 5+: Advanced Features

  1. Interactive Widgets (iOS 17+)

    • Toggle pin/unpin from widget
    • Quick actions (share, delete)
    • Inline editing
  2. Lock Screen Widgets (iOS 16+)

    • Circular memo count widget
    • Inline recording duration
    • Complications for Apple Watch
  3. StandBy Mode Support (iOS 17+)

    • Large clock-style widget
    • Optimized for nightstand
    • High contrast mode
  4. Widget Intents & Configuration

    • User-selectable space filter
    • Custom time ranges
    • Theme selection
  5. Live Activities

    • Active recording progress
    • Real-time transcription status
    • Processing progress indicator
  6. Siri Integration

    • "Hey Siri, show my latest memo"
    • "Hey Siri, start recording"
    • Shortcuts app integration

Resources

Documentation

Internal Resources

  • Design System: features/theme/
  • Memo Types: features/memos/types/memo.types.ts
  • Space Types: features/spaces/types/space.types.ts
  • Analytics: features/analytics/events.ts

Team Contacts

  • iOS Development: [Name]
  • UI/UX Design: [Name]
  • Backend/Data: [Name]
  • QA Testing: [Name]

Appendix

A. Widget Size Guidelines

Small Widget (2x2 Grid)

  • Dimensions: 158x158 pts (iPhone 14)
  • Safe Area: 16pt padding all sides
  • Max Content: ~3 lines of text

Medium Widget (4x2 Grid)

  • Dimensions: 338x158 pts (iPhone 14)
  • Safe Area: 16pt padding all sides
  • Max Content: ~5 lines of text, 1 image

Large Widget (4x4 Grid)

  • Dimensions: 338x354 pts (iPhone 14)
  • Safe Area: 16pt padding all sides
  • Max Content: Full memo preview, multiple elements

B. Color Palette

Primary Colors:

  • Blue: #007AFF (Light), #0A84FF (Dark)
  • Green: #34C759 (Success)
  • Red: #FF3B30 (Error)
  • Gray: #8E8E93 (Secondary Text)

Space Colors: (from existing Memoro theme)

  • Use existing space color palette
  • Ensure WCAG AA contrast ratios

C. SF Symbols Used

  • waveform - Audio recording
  • checkmark.circle.fill - Completed
  • pin.fill - Pinned memo
  • mic.fill - Recording button
  • chart.bar.fill - Statistics
  • sparkles - Inspiration
  • arrow.right.circle - Navigation

Document Version: 1.0 Last Updated: 2025-01-30 Author: Claude Code Status: Planning Phase Next Review: Before Phase 1 Start