@ondeinference / klepon / commits / f2abd21

Add watchOS companion app with browse-first guide support

Includes initial Info.plist, content loader, and WatchHomeView. Bundles core guide content for offline browsing. Updates project configuration and entitlements for watchOS target. Adds App Store metadata, release, and TestFlight checklists.

Seto Elkahfi committed May 14, 2026 at 17:16 UTC f2abd211d6ccd9c90f3c3134270df896097ceb78
20 files changed +384 -53
AppStore/README.md new
+14
@@ -0,0 +1,14 @@
1 +# App Store assets
2 +
3 +This folder keeps the App Store-facing copy and planning docs for Klepon.
4 +
5 +## Structure
6 +
7 +- `metadata/en-US/` — English metadata copy
8 +- `screenshots/iphone/` — iPhone screenshot exports
9 +- `screenshots/watch/` — watchOS screenshot exports
10 +- `screenshots/README.md` — screenshot shot list and titles
11 +- `testflight-checklist.md` — pre-TestFlight checklist
12 +- `release-checklist.md` — broader release checklist
13 +
14 +The screenshot folders are intentionally empty in Git until real exports are added.
AppStore/metadata/en-US/description.txt new
+17
@@ -0,0 +1,17 @@
1 +Discover Indonesian food with warmth, clarity, and privacy.
2 +
3 +Klepon helps you explore dishes, ingredients, and food traditions from across Indonesia in a way that feels calm, beautiful, and easy to understand.
4 +
5 +Browse curated guides to iconic dishes, regional favorites, sweet treats, and essential ingredients. Learn what a dish is, what it tastes like, what usually goes into it, and what to try next.
6 +
7 +When you want more context, Klepon lets you ask follow-up questions privately on your iPhone.
8 +
9 +What you can do in Klepon:
10 +- Explore curated Indonesian food guides
11 +- Learn dishes, ingredients, and traditions
12 +- Search quickly when you spot an unfamiliar name
13 +- Save dishes you want to remember
14 +- Ask follow-up questions privately on your iPhone
15 +- Browse quick companion notes on Apple Watch
16 +
17 +Klepon is made for curious eaters, travelers, diaspora families, and anyone who wants a more thoughtful way to discover Indonesian food.
AppStore/metadata/en-US/keywords.txt new
+1
@@ -0,0 +1 @@
1 +indonesian food,cuisine,dishes,rendang,sate,sambal,nasi goreng,street food,travel,guide
AppStore/metadata/en-US/name.txt new
+1
@@ -0,0 +1 @@
1 +Klepon: Taste of Indonesia
AppStore/metadata/en-US/promotional_text.txt new
+1
@@ -0,0 +1 @@
1 +Discover Indonesian dishes, ingredients, and food traditions in a calmer way. Browse curated entries, save favorites, and ask private follow-up questions on-device.
AppStore/metadata/en-US/review_notes.txt new
+5
@@ -0,0 +1,5 @@
1 +Klepon includes an iPhone app and a companion watchOS app.
2 +
3 +The iPhone app is fully usable without preparing the private on-device guide. Private follow-up answers are optional and only appear when the user chooses to enable that guide.
4 +
5 +The watchOS app is browse-first and intentionally lightweight. It does not attempt to run the full private answer experience on watchOS.
AppStore/metadata/en-US/subtitle.txt new
+1
@@ -0,0 +1 @@
1 +Your pocket guide to Indonesian food
AppStore/release-checklist.md new
+32
@@ -0,0 +1,32 @@
1 +# Release checklist
2 +
3 +## Before tagging
4 +
5 +- [ ] iPhone app builds cleanly
6 +- [ ] watchOS companion builds cleanly
7 +- [ ] README is current
8 +- [ ] LICENSE files are present
9 +- [ ] SECURITY.md is present
10 +- [ ] App Store metadata in `AppStore/metadata/en-US/` is current
11 +- [ ] Screenshots are captured and reviewed
12 +
13 +## Product quality
14 +
15 +- [ ] Core browse flows feel polished
16 +- [ ] Ask flow works on device with acceptable latency
17 +- [ ] Watch app content is readable and useful at a glance
18 +- [ ] No obvious sample-app copy remains
19 +
20 +## Signing and distribution
21 +
22 +- [ ] iPhone bundle identifier is correct
23 +- [ ] watch bundle identifier is correct
24 +- [ ] App Group entitlement is correct for the shipping account
25 +- [ ] Provisioning profiles and signing identities are valid
26 +
27 +## Final gate
28 +
29 +- [ ] TestFlight build installed on at least one real iPhone
30 +- [ ] Watch companion installed and tested on at least one real Apple Watch
31 +- [ ] Release notes are written
32 +- [ ] Git status is clean before tagging
AppStore/screenshots/README.md new
+31
@@ -0,0 +1,31 @@
1 +# Screenshot plan
2 +
3 +## iPhone screenshots
4 +
5 +1. `Discover the taste of Indonesia`
6 + - Show the Discover screen with the editorial hero and featured dish.
7 +2. `Browse iconic dishes and local favorites`
8 + - Show collections and a few richer browse cards.
9 +3. `Learn what each dish is really like`
10 + - Show a detail screen with summary, taste, and quick notes.
11 +4. `Search dishes, ingredients, and traditions`
12 + - Show a realistic search result screen.
13 +5. `Ask follow-up questions privately`
14 + - Show the private answer sheet with grounded sections.
15 +6. `Save what you want to try next`
16 + - Show the Saved screen with a short personal list.
17 +
18 +## Apple Watch screenshots
19 +
20 +1. `Browse on your wrist`
21 + - Show the watch home screen with a short starter list.
22 +2. `Quick notes for familiar dishes`
23 + - Show a watch detail view with subtitle, taste notes, and highlights.
24 +
25 +## Capture notes
26 +
27 +- Use real app content, not placeholder lorem ipsum.
28 +- Keep device chrome clean and consistent.
29 +- Prefer the light theme currently used by the app.
30 +- Avoid showing setup errors, loading failures, or half-finished states.
31 +- Use a short, real Saved list before taking the screenshot.
AppStore/screenshots/iphone/.gitkeep new
+1
@@ -0,0 +1 @@
1 +
AppStore/screenshots/watch/.gitkeep new
+1
@@ -0,0 +1 @@
1 +
AppStore/testflight-checklist.md new
+38
@@ -0,0 +1,38 @@
1 +# TestFlight checklist
2 +
3 +## Product sanity
4 +
5 +- [ ] Discover screen feels complete on first launch
6 +- [ ] Search returns sensible results for core dishes
7 +- [ ] Saved items persist across app relaunch
8 +- [ ] Ask flow works without turning into a generic chat experience
9 +- [ ] Watch app launches and browses correctly
10 +
11 +## Technical checks
12 +
13 +- [ ] iPhone target builds on simulator
14 +- [ ] iPhone target runs on a real device
15 +- [ ] Watch target builds on simulator
16 +- [ ] Watch target runs with the paired iPhone app
17 +- [ ] Private guide setup succeeds on a real device
18 +- [ ] Memory behavior is acceptable after enabling the private guide
19 +
20 +## Content checks
21 +
22 +- [ ] `python3 Scripts/validate_content.py` passes
23 +- [ ] Featured entries have good summaries and taste notes
24 +- [ ] Related entries do not point to missing items
25 +- [ ] Watch content looks sensible when loaded from bundled JSON
26 +
27 +## App Store / review checks
28 +
29 +- [ ] App name, subtitle, and description are up to date
30 +- [ ] Review notes mention the watch companion app and optional private guide
31 +- [ ] Privacy wording matches actual app behavior
32 +- [ ] No placeholder images or fake loading states remain
33 +
34 +## Cleanliness
35 +
36 +- [ ] No signing secrets or local credentials are in Git diff
37 +- [ ] No local machine junk files are staged
38 +- [ ] Bundle identifiers and App Group values are intentional for the release build
Klepon.xcodeproj/project.pbxproj
+33 -5
@@ -8,6 +8,7 @@
8
9 /* Begin PBXBuildFile section */
10 019B658F26ABE2706231EC9E /* GuideEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0935AE4B398B281758834 /* GuideEntry.swift */; };
11 + 03D16A2386EB68FED8D92D52 /* GuideEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0935AE4B398B281758834 /* GuideEntry.swift */; };
12 05BF0404B7030EDCD3D8C357 /* GuideArtworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2850226B4B05FF758C1F93 /* GuideArtworkView.swift */; };
13 0977CD6BFFDBDBE3B7795924 /* collections.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FDDB0F88FF430ABD679AD89 /* collections.json */; };
14 0D685E35EE50C926DC6FBE4C /* KleponChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83C4F3C8688072191AA16C6 /* KleponChip.swift */; };
@@ -15,12 +16,14 @@
16 14B7CE6E6A0EB12B709F7B55 /* KleponTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD520C870387E82E9A1533C2 /* KleponTypography.swift */; };
17 1704DDB60B1E23057C0E1094 /* GuideAnswerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3719031080B59898133810F7 /* GuideAnswerService.swift */; };
18 19E282A6347C588E17778195 /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4545DC84863788D55B94D31A /* DiscoverView.swift */; };
19 + 1E7AAEA8007BFE4ED6FE4892 /* WatchContentLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C77AF5EB4B17D087FA522F6 /* WatchContentLoader.swift */; };
20 1F95A454A641C829BD304A89 /* GuideCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17C0DB3BD620AD9F55B7631 /* GuideCollection.swift */; };
21 1FF490E985C31BB08DA29AFF /* guide_entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 248F359109A08A60F792C42D /* guide_entries.json */; };
22 200AE94E7E7E73B5C83869DD /* FavoritesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC0C4C22A5134DB058E41F4 /* FavoritesStore.swift */; };
23 23E22EC56BE48A20BD1229F7 /* AnswerCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02B0EA4B8BA09604D62A62B /* AnswerCard.swift */; };
24 2AB57009245E1BABD1156AFA /* GuideDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7541983FF7CE190C0663294D /* GuideDetailView.swift */; };
25 3349FB46691C944112401D6E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E152344B39D1D77F1B3943 /* SettingsView.swift */; };
26 + 36AA193A3B7EF7CB25B6E0BE /* KleponWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EFBF98118F066D3C7FA14B2 /* KleponWatchApp.swift */; };
27 41F8E173B82809AF87E1B297 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13418896CA28250CEEE7592E /* OnboardingView.swift */; };
28 4609C70D9FB74C55B9DBE832 /* KleponActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DD909BB2EF68103CB46EDA /* KleponActionButton.swift */; };
29 478FFEBDAA7729189B429A6D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E886BAB66E828950DBED52 /* SearchView.swift */; };
@@ -29,6 +32,7 @@
32 7F1E744AAE60BF1FB132D66C /* KleponCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868C58098969278A542893F3 /* KleponCard.swift */; };
33 8401F9C0FA140DC2CF8BA248 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF291E9C2E66209377138E6 /* AppState.swift */; };
34 8804CFBAD707E1FED63BF214 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235AD6792F25476E2A508F0 /* SearchService.swift */; };
35 + 88984B6A8AA504AD200A7F39 /* collections.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FDDB0F88FF430ABD679AD89 /* collections.json */; };
36 910A84B3F28B3F79871FF60E /* SectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC438ECC335175B18778C682 /* SectionHeader.swift */; };
37 926AB007EBCD5315EE77E2A6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF79D8DD27B9CA485140C934 /* Assets.xcassets */; };
38 99071E0CB9F44857C0BFC8C9 /* SavedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA2E425D5454602F78D628 /* SavedView.swift */; };
@@ -37,6 +41,7 @@
41 A7001D58E0F2B1BD5F52FF8A /* Onde in Frameworks */ = {isa = PBXBuildFile; productRef = 855EE8DF62A7820E77C05D1A /* Onde */; };
42 B1F1D1A453150D3BBB0146EF /* RecentlyViewedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45070309D2C7058BAA44CD2 /* RecentlyViewedStore.swift */; };
43 B34377720179454F9B9184C5 /* OndeGuideEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0D53A5F9D5897E15506C20 /* OndeGuideEngine.swift */; };
44 + B7DE9EC8BE998E6719C88E83 /* guide_entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 248F359109A08A60F792C42D /* guide_entries.json */; };
45 C709C82E7F0BCF398BCD9C2F /* ContentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2891A1AFBB59928FC5B1E66 /* ContentRepository.swift */; };
46 D99AAE1CDE51139840A52607 /* KleponMetadataRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E1C50BFBE2EF5F37941590 /* KleponMetadataRow.swift */; };
47 DA915BA8B2F732E7D62CF936 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E7B5E3D9A1906C2FED3032 /* EmptyStateView.swift */; };
@@ -55,11 +60,14 @@
60 3719031080B59898133810F7 /* GuideAnswerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideAnswerService.swift; sourceTree = "<group>"; };
61 42E152344B39D1D77F1B3943 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
62 4545DC84863788D55B94D31A /* DiscoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverView.swift; sourceTree = "<group>"; };
63 + 4C77AF5EB4B17D087FA522F6 /* WatchContentLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchContentLoader.swift; sourceTree = "<group>"; };
64 52DD909BB2EF68103CB46EDA /* KleponActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponActionButton.swift; sourceTree = "<group>"; };
65 5AF291E9C2E66209377138E6 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
60 - 5DE91827A635C0B0B72690EC /* Klepon.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
66 + 5DE91827A635C0B0B72690EC /* Klepon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Klepon.app; sourceTree = BUILT_PRODUCTS_DIR; };
67 6235AD6792F25476E2A508F0 /* SearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = "<group>"; };
68 643D52BCEDF66CC8FE8A1234 /* RecentSearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSearchStore.swift; sourceTree = "<group>"; };
69 + 67FAA69C42F1845ECB79AC16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
70 + 6EFBF98118F066D3C7FA14B2 /* KleponWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KleponWatchApp.swift; sourceTree = "<group>"; };
71 7541983FF7CE190C0663294D /* GuideDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideDetailView.swift; sourceTree = "<group>"; };
72 7F2850226B4B05FF758C1F93 /* GuideArtworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideArtworkView.swift; sourceTree = "<group>"; };
73 85E7B5E3D9A1906C2FED3032 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = "<group>"; };
@@ -127,6 +135,9 @@
135 isa = PBXGroup;
136 children = (
137 054656155630B064F785B0D7 /* Assets.xcassets */,
138 + 67FAA69C42F1845ECB79AC16 /* Info.plist */,
139 + 6EFBF98118F066D3C7FA14B2 /* KleponWatchApp.swift */,
140 + 4C77AF5EB4B17D087FA522F6 /* WatchContentLoader.swift */,
141 EDBB4F5D4C98D036AC52E193 /* WatchHomeView.swift */,
142 );
143 path = KleponWatch;
@@ -365,6 +376,8 @@
376 buildActionMask = 2147483647;
377 files = (
378 A54D028D8F35FE002B0ACA90 /* Assets.xcassets in Resources */,
379 + 88984B6A8AA504AD200A7F39 /* collections.json in Resources */,
380 + B7DE9EC8BE998E6719C88E83 /* guide_entries.json in Resources */,
381 );
382 runOnlyForDeploymentPostprocessing = 0;
383 };
@@ -422,6 +435,9 @@
435 isa = PBXSourcesBuildPhase;
436 buildActionMask = 2147483647;
437 files = (
438 + 03D16A2386EB68FED8D92D52 /* GuideEntry.swift in Sources */,
439 + 36AA193A3B7EF7CB25B6E0BE /* KleponWatchApp.swift in Sources */,
440 + 1E7AAEA8007BFE4ED6FE4892 /* WatchContentLoader.swift in Sources */,
441 4D63E231AC9630B2C225DF22 /* WatchHomeView.swift in Sources */,
442 );
443 runOnlyForDeploymentPostprocessing = 0;
@@ -432,13 +448,18 @@
448 57B34C7365523CA3F80ED770 /* Debug */ = {
449 isa = XCBuildConfiguration;
450 buildSettings = {
451 + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
452 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
453 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
454 CURRENT_PROJECT_VERSION = 1;
455 ENABLE_USER_SCRIPT_SANDBOXING = YES;
456 "EXCLUDED_ARCHS[sdk=watchsimulator*]" = x86_64;
440 - GENERATE_INFOPLIST_FILE = YES;
441 - INFOPLIST_KEY_WKWatchOnly = YES;
457 + GENERATE_INFOPLIST_FILE = NO;
458 + INFOPLIST_FILE = KleponWatch/Info.plist;
459 + LD_RUNPATH_SEARCH_PATHS = (
460 + "$(inherited)",
461 + "@executable_path/Frameworks",
462 + );
463 MARKETING_VERSION = 1.0;
464 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon.watchkitapp;
465 PRODUCT_NAME = "Klepon Watch";
@@ -513,6 +534,7 @@
534 CODE_SIGN_ENTITLEMENTS = Klepon/Klepon.entitlements;
535 CODE_SIGN_IDENTITY = "iPhone Developer";
536 CURRENT_PROJECT_VERSION = 1;
537 + DEVELOPMENT_TEAM = 2TQF86ZACD;
538 ENABLE_USER_SCRIPT_SANDBOXING = YES;
539 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
540 GENERATE_INFOPLIST_FILE = YES;
@@ -533,13 +555,18 @@
555 A3B469B57456DA7C46537A7E /* Release */ = {
556 isa = XCBuildConfiguration;
557 buildSettings = {
558 + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
559 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
560 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
561 CURRENT_PROJECT_VERSION = 1;
562 ENABLE_USER_SCRIPT_SANDBOXING = YES;
563 "EXCLUDED_ARCHS[sdk=watchsimulator*]" = x86_64;
541 - GENERATE_INFOPLIST_FILE = YES;
542 - INFOPLIST_KEY_WKWatchOnly = YES;
564 + GENERATE_INFOPLIST_FILE = NO;
565 + INFOPLIST_FILE = KleponWatch/Info.plist;
566 + LD_RUNPATH_SEARCH_PATHS = (
567 + "$(inherited)",
568 + "@executable_path/Frameworks",
569 + );
570 MARKETING_VERSION = 1.0;
571 PRODUCT_BUNDLE_IDENTIFIER = ai.splitfire.klepon.watchkitapp;
572 PRODUCT_NAME = "Klepon Watch";
@@ -621,6 +648,7 @@
648 CODE_SIGN_ENTITLEMENTS = Klepon/Klepon.entitlements;
649 CODE_SIGN_IDENTITY = "iPhone Developer";
650 CURRENT_PROJECT_VERSION = 1;
651 + DEVELOPMENT_TEAM = 2TQF86ZACD;
652 ENABLE_USER_SCRIPT_SANDBOXING = YES;
653 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
654 GENERATE_INFOPLIST_FILE = YES;
Klepon/Klepon.entitlements
+11 -3
@@ -1,5 +1,13 @@
1 -<?xml version="1.0" encoding="UTF-8"?>
2 -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1 +<?xml version="1.0" encoding="UTF-8" ?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3 + "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4 <plist version="1.0">
4 -<dict/>
5 +<dict>
6 + <key>com.apple.developer.kernel.increased-memory-limit</key>
7 + <true />
8 + <key>com.apple.security.application-groups</key>
9 + <array>
10 + <string>group.com.ondeinference.apps</string>
11 + </array>
12 +</dict>
13 </plist>
KleponWatch/Info.plist new
+26
@@ -0,0 +1,26 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <key>CFBundleDevelopmentRegion</key>
6 + <string>$(DEVELOPMENT_LANGUAGE)</string>
7 + <key>CFBundleExecutable</key>
8 + <string>$(EXECUTABLE_NAME)</string>
9 + <key>CFBundleIdentifier</key>
10 + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11 + <key>CFBundleInfoDictionaryVersion</key>
12 + <string>6.0</string>
13 + <key>CFBundleName</key>
14 + <string>$(PRODUCT_NAME)</string>
15 + <key>CFBundlePackageType</key>
16 + <string>APPL</string>
17 + <key>CFBundleShortVersionString</key>
18 + <string>$(MARKETING_VERSION)</string>
19 + <key>CFBundleVersion</key>
20 + <string>$(CURRENT_PROJECT_VERSION)</string>
21 + <key>WKApplication</key>
22 + <true/>
23 + <key>WKCompanionAppBundleIdentifier</key>
24 + <string>ai.splitfire.klepon</string>
25 +</dict>
26 +</plist>
KleponWatch/KleponWatchApp.swift new
+10
@@ -0,0 +1,10 @@
1 +import SwiftUI
2 +
3 +@main
4 +struct KleponWatchApp: App {
5 + var body: some Scene {
6 + WindowGroup {
7 + WatchHomeView()
8 + }
9 + }
10 +}
KleponWatch/WatchContentLoader.swift new
+17
@@ -0,0 +1,17 @@
1 +import Foundation
2 +
3 +struct WatchContentLoader {
4 + func loadEntries() -> [GuideEntry] {
5 + guard let url = Bundle.main.url(forResource: "guide_entries", withExtension: "json") else {
6 + return []
7 + }
8 +
9 + do {
10 + let data = try Data(contentsOf: url)
11 + let entries = try JSONDecoder().decode([GuideEntry].self, from: data)
12 + return entries.filter { $0.kind == .dish }
13 + } catch {
14 + return []
15 + }
16 + }
17 +}
KleponWatch/WatchHomeView.swift
+131 -43
@@ -1,54 +1,101 @@
1 import SwiftUI
2
3 -private struct WatchGuideEntry: Identifiable {
4 - let id: String
5 - let title: String
6 - let subtitle: String
7 - let notes: [String]
8 -}
3 +private struct WatchGuideStore {
4 + let entries: [GuideEntry] = WatchContentLoader().loadEntries()
5 +
6 + var featuredEntries: [GuideEntry] {
7 + Array(entries.filter(\.isFeatured).prefix(4))
8 + }
9
10 -private let watchEntries: [WatchGuideEntry] = [
11 - .init(
12 - id: "klepon",
13 - title: "Klepon",
14 - subtitle: "Sweet rice cake with palm sugar",
15 - notes: ["Chewy", "Coconut", "Palm sugar"]
16 - ),
17 - .init(
18 - id: "rendang",
19 - title: "Rendang",
20 - subtitle: "Slow-cooked beef with deep spice",
21 - notes: ["Rich", "Savory", "West Sumatra"]
22 - ),
23 - .init(
24 - id: "nasi-goreng",
25 - title: "Nasi goreng",
26 - subtitle: "Indonesia’s beloved fried rice",
27 - notes: ["Smoky", "Comforting", "Kecap manis"]
28 - ),
29 - .init(
30 - id: "sate",
31 - title: "Sate",
32 - subtitle: "Skewered grilled meat with sauce",
33 - notes: ["Grilled", "Street food", "Peanut sauce"]
34 - ),
35 -]
10 + var browseEntries: [GuideEntry] {
11 + let featuredIDs = Set(featuredEntries.map(\.id))
12 + let remaining = entries.filter { !featuredIDs.contains($0.id) }
13 + return Array((featuredEntries + remaining).prefix(8))
14 + }
15 +}
16
17 struct WatchHomeView: View {
18 + private let store = WatchGuideStore()
19 +
20 var body: some View {
21 NavigationStack {
40 - List(watchEntries) { entry in
41 - NavigationLink(entry.title) {
42 - WatchEntryDetailView(entry: entry)
22 + if store.entries.isEmpty {
23 + ScrollView {
24 + VStack(spacing: 10) {
25 + Image(systemName: "fork.knife.circle")
26 + .font(.title2)
27 + .foregroundStyle(.secondary)
28 +
29 + Text("Guide unavailable")
30 + .font(.headline)
31 +
32 + Text(
33 + "Open Klepon on iPhone first if you want the fuller guide and private follow-up answers."
34 + )
35 + .font(.footnote)
36 + .foregroundStyle(.secondary)
37 + .multilineTextAlignment(.center)
38 + }
39 + .padding()
40 }
41 + .navigationTitle("Klepon")
42 + } else {
43 + List {
44 + if !store.featuredEntries.isEmpty {
45 + Section("Start here") {
46 + ForEach(store.featuredEntries) { entry in
47 + NavigationLink {
48 + WatchEntryDetailView(entry: entry)
49 + } label: {
50 + EntryRow(entry: entry)
51 + }
52 + }
53 + }
54 + }
55 +
56 + Section("Browse") {
57 + ForEach(store.browseEntries) { entry in
58 + NavigationLink {
59 + WatchEntryDetailView(entry: entry)
60 + } label: {
61 + EntryRow(entry: entry)
62 + }
63 + }
64 + }
65 +
66 + Section {
67 + Text(
68 + "Browse on watch. Use the iPhone app for the full guide and private follow-up answers."
69 + )
70 + .font(.footnote)
71 + .foregroundStyle(.secondary)
72 + }
73 + }
74 + .navigationTitle("Klepon")
75 }
45 - .navigationTitle("Klepon")
76 }
77 }
78 }
79
80 +private struct EntryRow: View {
81 + let entry: GuideEntry
82 +
83 + var body: some View {
84 + VStack(alignment: .leading, spacing: 4) {
85 + Text(entry.title)
86 + .font(.headline)
87 +
88 + Text(entry.subtitle)
89 + .font(.footnote)
90 + .foregroundStyle(.secondary)
91 + .lineLimit(2)
92 + }
93 + .padding(.vertical, 2)
94 + }
95 +}
96 +
97 private struct WatchEntryDetailView: View {
51 - let entry: WatchGuideEntry
98 + let entry: GuideEntry
99
100 var body: some View {
101 ScrollView {
@@ -60,15 +107,52 @@ private struct WatchEntryDetailView: View {
107 .font(.subheadline)
108 .foregroundStyle(.secondary)
109
63 - ForEach(entry.notes, id: \.self) { note in
64 - Text("• \(note)")
65 - .font(.footnote)
110 + if let region = entry.region {
111 + Text(region)
112 + .font(.footnote.weight(.semibold))
113 + .foregroundStyle(.secondary)
114 }
115
68 - Text("Watch support is browse-first for now.")
116 + Divider()
117 +
118 + Text(entry.summary)
119 .font(.footnote)
70 - .foregroundStyle(.secondary)
71 - .padding(.top, 6)
120 +
121 + if !entry.tasteNotes.isEmpty {
122 + VStack(alignment: .leading, spacing: 8) {
123 + Text("Taste")
124 + .font(.footnote.weight(.semibold))
125 + .foregroundStyle(.secondary)
126 +
127 + ForEach(Array(entry.tasteNotes.prefix(3)), id: \.self) { note in
128 + Text("• \(note)")
129 + .font(.footnote)
130 + }
131 + }
132 + }
133 +
134 + if !entry.highlights.isEmpty {
135 + Divider()
136 +
137 + VStack(alignment: .leading, spacing: 8) {
138 + Text("Quick notes")
139 + .font(.footnote.weight(.semibold))
140 + .foregroundStyle(.secondary)
141 +
142 + ForEach(Array(entry.highlights.prefix(3)), id: \.self) { note in
143 + Text("• \(note)")
144 + .font(.footnote)
145 + }
146 + }
147 + }
148 +
149 + Divider()
150 +
151 + Text(
152 + "Open Klepon on iPhone if you want the fuller guide and on-device follow-up answers."
153 + )
154 + .font(.footnote)
155 + .foregroundStyle(.secondary)
156 }
157 .frame(maxWidth: .infinity, alignment: .leading)
158 .padding()
@@ -76,3 +160,7 @@ private struct WatchEntryDetailView: View {
160 .navigationTitle(entry.title)
161 }
162 }
163 +
164 +#Preview {
165 + WatchHomeView()
166 +}
README.md
+8
@@ -20,6 +20,7 @@ This repo is under active development, but the V1 direction is already in place:
20 - curated Indonesian food content bundled locally
21 - private on-device follow-up answers powered by [Onde Inference](https://ondeinference.com).
22 - iPhone-first SwiftUI UI and interaction model
23 +- watchOS companion support for quick browse-first dish notes
24 - local search, saved items, recent searches, and recently viewed continuity
25
26 ## Repository layout
@@ -28,6 +29,7 @@ This repo is under active development, but the V1 direction is already in place:
29 - `Klepon.xcodeproj/` — the Xcode project
30 - `Scripts/validate_content.py` — validates bundled content integrity
31 - `SECURITY.md` — security handling and disclosure guidance
32 +- `AppStore/` — App Store metadata, screenshot plan, and release checklists
33
34 ## Tech stack
35
@@ -56,6 +58,12 @@ Open:
58 xcodebuild -project Klepon.xcodeproj -scheme Klepon -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
59 ```
60
61 +### Build the watchOS target
62 +
63 +```/dev/null/sh#L1-1
64 +xcodebuild -project Klepon.xcodeproj -scheme 'Klepon Watch' -destination 'generic/platform=watchOS Simulator' CODE_SIGNING_ALLOWED=NO build
65 +```
66 +
67 ### Validate bundled content
68
69 ```/dev/null/sh#L1-1
project.yml
+5 -2
@@ -73,17 +73,20 @@ targets:
73 platform: watchOS
74 sources:
75 - path: KleponWatch
76 + - path: Klepon/Domain/GuideEntry.swift
77 + - path: Klepon/Resources/Content
78 + buildPhase: resources
79 settings:
80 base:
81 PRODUCT_BUNDLE_IDENTIFIER: ai.splitfire.klepon.watchkitapp
82 PRODUCT_NAME: Klepon Watch
83 MARKETING_VERSION: "1.0"
84 CURRENT_PROJECT_VERSION: 1
82 - GENERATE_INFOPLIST_FILE: YES
85 + GENERATE_INFOPLIST_FILE: NO
86 + INFOPLIST_FILE: KleponWatch/Info.plist
87 ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
88 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
89 ENABLE_USER_SCRIPT_SANDBOXING: YES
86 - INFOPLIST_KEY_WKCompanionAppBundleIdentifier: ""
90 SDKROOT: watchos
91 TARGETED_DEVICE_FAMILY: 4
92 SKIP_INSTALL: YES