Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
48b0720
docs: native give-feedback app design (Architecture C)
apple-techie May 29, 2026
8b48421
docs: implementation plans for native give-feedback app (Tracks 1 & 2)
apple-techie May 29, 2026
f2317e4
feat(app): scaffold FeedbackKit library + test target
apple-techie May 29, 2026
570bb6d
feat(app): Codable models + JSON envelopes
apple-techie May 29, 2026
a133ca0
feat(app): FeedbackAPI protocol + APIError + test mock
apple-techie May 29, 2026
f2cc75f
feat(app): TokenStore (in-memory + Keychain)
apple-techie May 29, 2026
564ee05
feat(app): FeedViewModel with loading/error states
apple-techie May 29, 2026
d39c8fb
feat(app): AuthStore + email-OTP AuthService protocol
apple-techie May 29, 2026
bd9f293
feat(app): PostDetailViewModel (vote/comment with sign-in gate)
apple-techie May 29, 2026
be5b5e2
feat(app): SubmitViewModel (validation + sign-in gate)
apple-techie May 29, 2026
f6f9b7b
feat(app): Changelog + Help view models
apple-techie May 29, 2026
e3780d5
feat(app): HTTPFeedbackAPI URLSession client (bearer + error mapping)
apple-techie May 29, 2026
73b90f5
feat(app): HTTPAuthService (better-auth email-OTP)
apple-techie May 29, 2026
ab55058
feat(app): offline content cache (read-only fallback)
apple-techie May 29, 2026
4702605
feat(app): SwiftUI 4-tab FeedbackPortalApp shell
apple-techie May 29, 2026
e91b329
ci: build FeedbackKit + FeedbackPortalApp for iOS, run FeedbackKit tests
apple-techie May 29, 2026
d1b2eef
fix(ci): concurrency-safe SDK event tests (Swift 5.10) + FeedbackPort…
apple-techie May 29, 2026
73b42a6
fix(ci): rename 1-char identifier to satisfy swiftlint --strict
apple-techie May 29, 2026
6b5901a
Potential fix for pull request finding
BunsDev May 29, 2026
42462c9
Potential fix for pull request finding
BunsDev May 29, 2026
139c3de
Potential fix for pull request finding
BunsDev May 29, 2026
def3914
Potential fix for pull request finding
BunsDev May 29, 2026
5f71b89
Potential fix for pull request finding
BunsDev May 29, 2026
f4669a7
Potential fix for pull request finding
BunsDev May 29, 2026
5d41c4d
fix: address native feedback app review findings
BunsDev May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,21 @@ jobs:
run: swiftlint lint --strict --config .swiftlint.yml

ios-build:
name: Build SDK for iOS
name: Build SDK and FeedbackPortalApp for iOS
runs-on: macos-14
steps:
- uses: actions/checkout@v4

- name: Show Xcode
run: xcodebuild -version

- name: Build FeedbackKit for iOS Simulator (SPM)
run: |
xcodebuild build \
-scheme FeedbackKit \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO

- name: Install XcodeGen
run: brew install xcodegen

Expand All @@ -49,3 +56,12 @@ jobs:
-scheme FeedbackApp \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO

- name: Build FeedbackPortalApp (iOS Simulator)
run: |
xcodebuild build \
-project FeedbackApp.xcodeproj \
-scheme FeedbackPortalApp \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ build/

# ── Swift PM ─────────────────────────────────────────────────────────────────
.swiftpm/

# ── Brainstorm companion artifacts ───────────────────────────────────────────
.superpowers/
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ included:
- FeedbackApp/
- Sources/
- Tests/
- App/

excluded:
- .build/
Expand Down
47 changes: 47 additions & 0 deletions App/FeedbackPortalApp/AccountTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import FeedbackKit
import SwiftUI

struct AccountTabView: View {
@EnvironmentObject private var auth: AuthStore

@State private var isShowingSignIn = false

var body: some View {
NavigationStack {
List {
if auth.isSignedIn {
Section {
Label("Signed in", systemImage: "checkmark.seal.fill")
.foregroundStyle(.green)
}

Section {
Button(role: .destructive) {
auth.signOut()
} label: {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
}
}
} else {
Section {
Label("Not signed in", systemImage: "person.crop.circle.badge.xmark")
.foregroundStyle(.secondary)
}

Section {
Button {
isShowingSignIn = true
} label: {
Label("Sign In", systemImage: "person.crop.circle.badge.checkmark")
}
}
}
}
.listStyle(.insetGrouped)
.navigationTitle("Account")
.sheet(isPresented: $isShowingSignIn) {
SignInSheet()
}
}
}
}
77 changes: 77 additions & 0 deletions App/FeedbackPortalApp/ChangelogTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import FeedbackKit
import SwiftUI

struct ChangelogTabView: View {
@EnvironmentObject private var env: AppEnvironment

@State private var vm: ChangelogViewModel?

var body: some View {
NavigationStack {
Group {
if let vm {
ChangelogListView(vm: vm)
} else {
ProgressView()
}
}
.navigationTitle("Changelog")
}
.task {
let model = ChangelogViewModel(api: env.api)
vm = model
await model.load()
}
}
}

private struct ChangelogListView: View {
@ObservedObject var vm: ChangelogViewModel

var body: some View {
List {
if vm.isLoading && vm.entries.isEmpty {
ProgressView()
.frame(maxWidth: .infinity)
.listRowSeparator(.hidden)
} else if let error = vm.errorMessage, vm.entries.isEmpty {
ContentUnavailableView(
"Couldn't Load",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
.listRowSeparator(.hidden)
} else if vm.entries.isEmpty {
ContentUnavailableView(
"No Entries Yet",
systemImage: "clock.arrow.circlepath",
description: Text("Check back for updates.")
)
.listRowSeparator(.hidden)
} else {
ForEach(vm.entries) { entry in
VStack(alignment: .leading, spacing: 6) {
Text(entry.title)
.font(.headline)
if let publishedAt = entry.publishedAt {
Text(publishedAt, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
if let content = entry.content, !content.isEmpty {
Text(content)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(3)
}
}
.padding(.vertical, 4)
}
}
}
.listStyle(.plain)
.refreshable {
await vm.load()
}
}
}
27 changes: 27 additions & 0 deletions App/FeedbackPortalApp/Environment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import FeedbackKit
import Foundation

@MainActor
final class AppEnvironment: ObservableObject {
let api: FeedbackAPI
let auth: AuthStore

private let tokenStore: KeychainTokenStore

init() {
let raw = Bundle.main.object(forInfoDictionaryKey: "FEEDBACK_INSTANCE_URL") as? String
?? "http://localhost:3000"
let instanceURL = URL(string: raw) ?? URL(string: "http://localhost:3000")!

let store = KeychainTokenStore()
let authService = HTTPAuthService(baseURL: instanceURL)
let authStore = AuthStore(service: authService, tokenStore: store)

self.tokenStore = store
self.auth = authStore
self.api = HTTPFeedbackAPI(
baseURL: instanceURL,
tokenProvider: { store.token }
)
}
}
108 changes: 108 additions & 0 deletions App/FeedbackPortalApp/FeedTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import FeedbackKit
import SwiftUI

struct FeedTabView: View {
@EnvironmentObject private var env: AppEnvironment
@EnvironmentObject private var auth: AuthStore

@State private var vm: FeedViewModel?
@State private var isShowingSubmit = false

var body: some View {
NavigationStack {
Group {
if let vm {
FeedListView(vm: vm, isShowingSubmit: $isShowingSubmit)
} else {
ProgressView()
}
}
.navigationTitle("Feedback")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isShowingSubmit = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isShowingSubmit) {
if let vm {
SubmitView(api: env.api, auth: auth)
}
}
}
.task {
let model = FeedViewModel(api: env.api)
vm = model
await model.load()
}
}
}

private struct FeedListView: View {
@ObservedObject var vm: FeedViewModel
@Binding var isShowingSubmit: Bool

var body: some View {
List {
if vm.isLoading && vm.posts.isEmpty {
ProgressView()
.frame(maxWidth: .infinity)
.listRowSeparator(.hidden)
} else if let error = vm.errorMessage, vm.posts.isEmpty {
ContentUnavailableView(
"Couldn't Load",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
.listRowSeparator(.hidden)
} else if vm.posts.isEmpty {
ContentUnavailableView(
"No Posts Yet",
systemImage: "bubble.left",
description: Text("Be the first to submit feedback.")
)
.listRowSeparator(.hidden)
} else {
ForEach(vm.posts) { post in
NavigationLink(value: post.id) {
PostRowView(post: post)
}
}
}
}
.listStyle(.plain)
.refreshable {
await vm.load()
}
.navigationDestination(for: String.self) { postId in
PostDetailView(postId: postId)
}
}
}

private struct PostRowView: View {
let post: PostSummary

var body: some View {
HStack(spacing: 12) {
VStack(spacing: 2) {
Image(systemName: post.hasVoted ? "arrowtriangle.up.fill" : "arrowtriangle.up")
.foregroundStyle(post.hasVoted ? .blue : .secondary)
Text("\(post.voteCount)")
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
.frame(width: 36)

Text(post.title)
.font(.body)
.lineLimit(2)

Spacer()
}
.padding(.vertical, 4)
}
}
15 changes: 15 additions & 0 deletions App/FeedbackPortalApp/FeedbackPortalApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import FeedbackKit
import SwiftUI

@main
struct FeedbackPortalApp: App {
@StateObject private var env = AppEnvironment()

var body: some Scene {
WindowGroup {
RootTabView()
.environmentObject(env)
.environmentObject(env.auth)
}
}
}
Loading
Loading