FeedbackJar

iOS (Swift)

The FeedbackJar iOS SDK is a data layer plus an optional prebuilt board. Use just the data layer if you’re building your own UI, or drop in FeedbackJarBoard for a complete list → detail → new feedback flow with voting and comments already wired up.

Installation

In Xcode: File → Add Package Dependencies, enter the repository URL, and add FeedbackJar to your target.

Or in Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/feedbackjar/swift-sdk", from: "1.4.6"),
],
targets: [
    .target(name: "MyApp", dependencies: ["FeedbackJar"]),
]

Setup

Configure once before use — typically in your SwiftUI App.init:

swift
import FeedbackJar

@main
struct MyApp: App {
    init() {
        FeedbackJar.configure(widgetId: "your-widget-id")
    }
}

Get your widget ID from Settings → Widgets in your FeedbackJar project.

Prebuilt board (optional)

Skip building your own screens — FeedbackJarBoard is a complete board built only on SwiftUI, a system framework, not a dependency:

swift
import FeedbackJar
import SwiftUI

struct FeedbackTab: View {
    var body: some View {
        FeedbackJarBoard()
        // or: FeedbackJarBoard(accentColor: .blue, boardId: "board_123")
    }
}

Gives you list, upvote, detail, comments, and a new-feedback form, built only on the API methods below. Nothing throws; failures render inline.

Driving “New feedback” from your own UI

The board shows its own “New” button and Cancel button by default. If your screen has its own button/toolbar action to start a submission, or its own way to back out, turn either off and drive the sheet with your own binding:

swift
@State private var isPresentingNewFeedback = false

FeedbackJarBoard(
    showNewButton: false,
    showCancelButton: false,
    isPresentingNewFeedback: $isPresentingNewFeedback
)
.toolbar {
    Button("New") { isPresentingNewFeedback = true }
}

The board flips isPresentingNewFeedback back to false on cancel or a successful send, so your button and the board’s state stay in sync.

Identify the submitter (optional)

Remember a name/email so later submit, vote, and addComment calls attribute to the same person, and so their name/email are synced to the server against this install’s anonymous id:

swift
FeedbackJar.shared.setIdentity(name: user.name, email: user.email)
let identity = FeedbackJar.shared.getIdentity()

// On logout:
FeedbackJar.shared.clearIdentity()

Skip this and submissions stay fully anonymous — setIdentity is optional, not required.

Verified identity (signed-in users)

If your app has its own signed-in users, attribute their votes/comments to a real, verifiable account — consistent across every device they use — instead of the unverified name/email above. Your backend computes the signature:

swift
// Your backend, once the user is authenticated:
//   timestamp = Int(Date().timeIntervalSince1970 * 1000)
//   signature = HMAC-SHA256(orgSecretKey, "\(userId):\(email):\(timestamp)")
// (org secret key is in the FeedbackJar dashboard)

FeedbackJar.shared.setIdentity(
    userId: user.id,
    email: user.email,
    signature: signatureFromYourBackend,
    timestamp: timestampFromYourBackend
)

Once set, vote, addComment, submit, and hasVoted all attach to this real user on every device — immediately, and any votes/comments already made anonymously on this device are folded in automatically. timestamp must be the exact value your backend signed, and expires after 7 days — call setIdentity again on each sign-in.

Submit feedback

Each submission automatically includes device metadata (iOS version, device model, screen size, app version, locale).

swift
let result = await FeedbackJar.shared.submit(userText)
switch result {
case .success(let response):
    print("Submitted: \(response.postId)")
case .failure(let error):
    print("Failed: \(error)")
}

A callback variant is available when you are not in an async context:

swift
FeedbackJar.shared.submit(userText) { result in
    switch result {
    case .success(let response): // show success state
    case .failure(let error):    // show error state
    }
}

Pass email/name for a headless submission where identity isn’t already known, and properties for your own key/value context merged into the auto-collected app metadata:

swift
await FeedbackJar.shared.submit(userText, properties: ["plan": "pro"])

List feedback

Fetch the public feedback feed with optional pagination:

swift
let result = await FeedbackJar.shared.listFeedback(limit: 20)
if case .success(let page) = result {
    for post in page.posts {
        print("\(post.title) — \(post.upvotes) upvotes")
    }
}

Pass boardId to filter by board, or cursor for pagination.

FeedbackJar.shared.getPost(postId) fetches a single public post the same way.

Voting

Guest upvoting as this install’s anonymous guest — idempotent, and gated by the project’s Allow guest votes setting (WidgetConfig.allowVotes, see Get widget config):

swift
await FeedbackJar.shared.vote(postId: postId)              // upvote
await FeedbackJar.shared.unvote(postId: postId)             // remove the upvote
let state = await FeedbackJar.shared.voteState(postId: postId) // current count + hasVoted

Comments

Anonymous, two-level threads — gated by Allow guest comments (WidgetConfig.allowComments):

swift
let comments = await FeedbackJar.shared.listComments(postId: postId, limit: 20)

// Reply to a post, or to a root comment via parentId (replies-to-replies aren't allowed)
await FeedbackJar.shared.addComment(postId: postId, content: "Same issue here!")

name/email fall back to the remembered identity (setIdentity) when omitted.

Get widget config

Check whether the org asks for name/email, and whether voting/comments are enabled, before building your own UI around them:

swift
if case .success(let config) = await FeedbackJar.shared.getConfig() {
    if config.collectEmail { /* show an email field */ }
    if config.allowVotes { /* show your vote button */ }
}
FieldMeaning
collectNameOrg asks submitters for their name (“Ask for Name”)
collectEmailOrg asks submitters for their email (“Ask for Email”)
allowVotesGuest upvoting is enabled for this project
allowCommentsGuest commenting is enabled for this project

API reference

MethodDescription
configure(widgetId:)Configure the SDK. Call once before anything else.
setIdentity(name:email:)Remember a submitter’s name/email for later calls.
setIdentity(userId:email:signature:timestamp:...)Set a verified, org-signed identity — see Verified identity.
getIdentity()The currently remembered identity, if any.
clearIdentity()Forget the remembered identity (e.g. on logout).
submit(_:email:name:properties:) asyncSubmit feedback. Returns Result<FeedbackResponse, Error>.
listFeedback(boardId:limit:cursor:) asyncList public feedback. limit is clamped to 1–50.
getPost(_:) asyncFetch a single public post.
getConfig() asyncFetch this project’s WidgetConfig.
vote(postId:) async / unvote(postId:) asyncCast or remove this install’s upvote. Idempotent.
voteState(postId:) asyncCurrent upvote count and whether this install voted.
listComments(postId:limit:cursor:) asyncList a post’s comments (two levels).
addComment(postId:content:parentId:name:email:) asyncAdd a comment or reply.

All async methods have a callback overload (completion: @escaping (Result<T, Error>) -> Void) safe to call from the main thread.

Notes

  • Submission, voting, and comments are all anonymous by default — setIdentity attaches a name/email, it does not require a full account or portal login.
  • Voting and commenting only work when the project has them enabled — check getConfig() before showing that UI.
  • The server rate-limits submissions (5 per 15 minutes per IP).
  • Private boards and non-public posts are never returned by listFeedback.