Native client packages for Loomup, preserved in a repository separate from the server and JavaScript SDK repositories.
Package.swift/swift/— Swift Package Manager productsLoomupandLoomupAppIntegrity(iOS 16+).kotlin/— Kotlin/JVM core plus the Android integrity integration.flutter/— Dart/Flutter client retained for a later integrity rollout.conformance/— language-neutral client fixtures.
These packages are source-available and continuously tested. Swift is released
from the root package through semantic-version Git tags; the current release is
0.1.9. The release workflow verifies tagged Swift source but does not publish
to Swift Package Index or a Swift package-registry server. Kotlin and Dart do
not yet have Maven Central or pub.dev releases.
Mobile applications must never embed a Loomup service key. User access tokens remain the authorization principal; app integrity is an additional anti-abuse signal for projects that opt into it.
Add the released package from its Git URL and link both products:
dependencies: [
.package(
url: "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/bluppco/loomup-native.git",
from: "0.1.9"
)
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "Loomup", package: "loomup-native"),
.product(name: "LoomupAppIntegrity", package: "loomup-native"),
]
)
]Use an exact 0.1.9 requirement when automatic patch updates are not desired.
For local development, replace the URL dependency with
.package(path: "../loomup-native").
For an iOS 16+ App Store or TestFlight app, construct the client with the manifest app identifier, not a backend credential:
import Loomup
import LoomupAppIntegrity
let loomup = createMobileClient(
url: URL(string: "https://api.example.com")!,
appID: "ios_main"
)createMobileClient uses App Attest for each mutation, stores only the refresh
token in a ThisDeviceOnly Keychain item, and keeps the access token in memory.
The appID must match an [app_integrity.apps.<id>] entry on the server. Keep
the core createClient factory for macOS/server-side tools and tests; it also
has no service-key input.
For Google, Apple, or GitHub sign-in, use the system authentication session helper. It keeps the one-use verifier in memory and exchanges the deep-link code through the same integrity-protected client:
let tokens = try await signInWithOAuth(
client: loomup,
provider: .google,
redirectTo: URL(string: "com.example.app:/auth/callback")!,
presentationContextProvider: windowProvider
)The kotlin/ build contains the JVM core and :android library. Android apps
use createAndroidClient(context, url, appId, cloudProjectNumber). It requests
standard Play Integrity tokens bound to the exact mutation and stores only the
refresh token using a non-exportable Android Keystore key. The project number
and appId are identifiers, not secrets.
The repository CI builds and tests all native packages. Swift releases use Git tags consumable by Swift Package Manager; they are not separately listed or uploaded to Swift Package Index, a Swift package-registry server, or CocoaPods. Kotlin and Dart are not published to Maven Central or pub.dev.
Android exposes signInWithOAuth(client, provider, redirectTo, launcher) in
the Android library; the app supplies its Custom Tab/deep-link launcher. Flutter
exposes the same authorize/exchange flow with an injected URL launcher so no
browser plugin is forced on applications.
Swift and Android mobile constructors reconnect realtime automatically when
the app returns to the foreground. Flutter apps call resumeRealtime() from
their AppLifecycleState.resumed handler so active subscriptions are preserved,
re-subscribed, and resynchronized after a background suspension.
The released Swift client also verifies application-level realtime liveness
while at least one subscription is active. It sends correlated JSON ping
text frames every 25 seconds and expects the matching JSON pong within 12
seconds. A missing or mismatched response retires the socket even if it still
reports OPEN; the normal jittered reconnect path then authenticates,
re-subscribes, and refetches current authorized state. These messages complement
the WebSocket protocol Ping/Pong frames and do not replace or disable them.
Release 0.1.9 automatically sends uploads above 8 MiB as bounded resumable chunks. Deploy the backend resumable-upload routes first. The project must allow the desired completed object size (up to 1 GiB) and have sufficient storage quota. Transient chunk and completion failures are retried; terminal failures abort staged uploads. Cancellation is checked between chunks.
Prefer client.storage.from("files").upload(path: "movie.mp4", fileURL: url, contentType: "video/mp4") for large files: it reads only one chunk at a time.
Existing data: callers also use chunked transfer for large Data values.
Use downloadFile(path:) to receive a temporary-file URL for large downloads.
The caller must remove the file after previewing or sharing it. Built-in URLSession
transports download directly to disk; custom HTTP transports must implement
download(for:) to support this operation. Authentication refresh applies as
for ordinary downloads.
client.inbox performs online, recipient-owned bulk actions atomically:
let scope: [String: JSONValue] = ["workspace_id": .string(workspaceID)]
let selection = try await client.inbox.select(scope: scope, expectedRecipientId: userID)
let request = InboxActionRequest(
action: .markRead, scope: scope,
target: .selection(id: selection.selectionId, excludedIds: [], includedIds: []),
expectedRecipientId: userID
)
let result = try await client.inbox.act(request)Retain and reuse the same request after an ambiguous transport failure: its
idempotency key prevents duplicate work for 24 hours. Actions are .markRead,
.delete, and .deleteRead; targets are .matching, .ids, and .selection.
Select-all snapshots include unloaded pages, exclude later arrivals, and expire
after 30 minutes. Use client.inbox.members(selectionId:ids:scope:) to resolve
loaded checkboxes. Limits: 50,000 snapshot members, 1,000 explicit IDs, additions,
or exclusions. The server checks recipient ownership and current permissions.
Results report changed/skipped counts and the cutoff/change timestamps.
See the inbox contract for full semantics.