Compare commits

..

23 Commits
2.0.0 ... 2.1.0

Author SHA1 Message Date
b1f370f7db Merge pull request '[sub-spm-optional-products] LCFeatureControl review fixes: JWT log leak, platforms, request coalescing' (#13) from feature/spm/lcfeaturecontrol into main
Reviewed-on: #13
Reviewed-by: Daniel Arantes Loverde <developer@loverde.com.br>
2026-09-16 13:31:38 -03:00
Daniel Arantes Loverde
c6e9803c75 [lcfeaturecontrol-review-fixes] Fix JWT log leak, drop macOS/tvOS, coalesce requests
- FeatureControlManager/NotificationsClient: pass debug: false at every
  api.request(...) call site — the API default (debug: true) was printing
  Authorization: Bearer <jwt> on every request, release builds included.
- Package.swift: drop macOS/tvOS from platforms — LCEssentials.API (used by
  LCFeatureControl) is iOS/watchOS-only and the vendored xcframework has no
  macOS/tvOS slice.
- FeatureControlManager: coalesce concurrent evaluate() calls on a cold cache
  into a single in-flight request per key instead of firing one per caller.
- FeatureControlManager/NotificationsClient: treat a 200 response carrying
  {"error": true} as a failure instead of caching/returning it as success.
- FeatureControlNotificationsClient: thread a cursor param through list() so
  the already-decoded nextCursor can actually be used to page.
- Tests: 9 new tests (coalescing, envelope-error-on-200, cursor param, a real
  object payload decoded through a full evaluate response, date-decode
  failures) and removed the 7 remaining force-unwraps in test scaffolding.
- Documentation/FeatureControl.md: new guide for the LCFeatureControl product,
  cross-linked from README.md and Extensions.md.
2026-09-16 13:21:10 -03:00
Daniel Arantes Loverde
2ba487a6e8 [sub-spm-optional-products] Add LCECryptoKit and LCFeatureControl as optional sub-SPM products
LCEssentials installs standalone; each sub target-depends on it so linking a
sub's product always pulls LCEssentials in too, without the consumer having to
declare it separately.

- LCECryptoKit: internalized from the remote LCECryptoKitBinary git dependency
  (embedded token URL removed) into a local binaryTarget vendoring
  Frameworks/LCECryptoKit.xcframework. LCECryptoKitManager moved out of
  LCEssentials core into its own LCECryptoKitManager target/product; the
  no-op fallback for when the binary wasn't linked is gone (breaking change
  for existing consumers, see decisions/2026-09-15-sub-spm-optional-products.md).
- LCFeatureControl: new product wrapping Atomenta's Feature Control API
  (flag evaluation with TTL cache + safe-degrade fallback to defaults,
  notifications inbox, batched exposure telemetry). 45 new tests.
2026-09-16 09:23:57 -03:00
Daniel Arantes Loverde
d067791930 fix new token 2026-09-10 20:47:13 -03:00
1d5067212a Merge pull request '[fix] LCENavigationView: remove phantom glass button, center title safely' (#11) from fix/navigation/glass-duplicate-button into main
Reviewed-on: #11
2026-09-01 16:26:03 -03:00
Daniel Arantes Loverde
72a5b59c44 [fix] LCENavigationView: remove phantom glass button, center title safely
Dummy mirrored button used to balance title centering rendered as a
visible empty glass pill under iOS 26's automatic button chrome. Side
buttons now only render when actually configured, title centers via
ZStack independent of button widths, and title padding is derived from
measured button width so long titles truncate instead of overlapping.
Glass button style is opt-in via setGlassButtonsEnabled(_:), off by
default.
2026-09-01 16:20:19 -03:00
Daniel Arantes Loverde
d678965154 [docs] Trim README to Documentation links; fix signature image 2026-08-30 10:02:19 -03:00
Daniel Arantes Loverde
0cceda1ad9 [docs] Trim README to intro + install + Documentation links
Remove the legacy inline usage snippets (background thread, nav completion
handler, networking example, 'Another components'). Replace the dead
signature GIF with the public GitHub mark linking to the resume repo.
2026-08-30 10:02:18 -03:00
Daniel Arantes Loverde
2cd52d3d12 [docs] Split reference docs into Extensions / SwiftUI / UIKit guides
Full per-symbol reference for every public extension, SwiftUI component, and
UIKit-era helper, as collapsible sections. README gets a Documentation table
linking all four guides; footer trimmed to the resume link. Docs only - no
code changes.
2026-08-29 21:27:49 -03:00
Daniel Arantes Loverde
1743bc2d24 [docs] Cross-link API.md to the other reference guides 2026-08-29 19:36:03 -03:00
Daniel Arantes Loverde
872d70dc21 [docs] UIKit.md: Media & Components section (UIKit.md complete) 2026-08-29 19:34:51 -03:00
Daniel Arantes Loverde
6eb57f5c44 [docs] UIKit.md: Collections & Tables section 2026-08-29 19:33:59 -03:00
Daniel Arantes Loverde
ac9d010b8e [docs] UIKit.md: Navigation & Controllers section 2026-08-29 19:33:22 -03:00
Daniel Arantes Loverde
255e5cfe1a [docs] UIKit.md: Views & Controls section 2026-08-29 19:32:31 -03:00
Daniel Arantes Loverde
f57dc50975 [docs] UIKit.md: Layout & Constraints section 2026-08-29 19:31:43 -03:00
Daniel Arantes Loverde
971278fa3c [docs] SwiftUI.md: LCENavigationView (complete) 2026-08-29 19:30:30 -03:00
Daniel Arantes Loverde
ec3442ed1c [docs] Extensions.md: Encoding & Errors + Core sections (Extensions.md complete) 2026-08-29 19:29:42 -03:00
Daniel Arantes Loverde
4f6a83556d [docs] Extensions.md: Date, Data & Files section 2026-08-29 19:28:35 -03:00
Daniel Arantes Loverde
d16a42fddc [docs] Extensions.md: Numbers & Geometry section 2026-08-29 19:27:31 -03:00
Daniel Arantes Loverde
c459c9eaf6 [docs] Extensions.md: Collections & Sequences section 2026-08-29 19:26:46 -03:00
Daniel Arantes Loverde
1d2c595c78 [docs] Extensions.md: Strings & Text section 2026-08-29 19:25:15 -03:00
Daniel Arantes Loverde
9f1db7c121 [docs] Extensions.md: API overview + Crypto section 2026-08-29 19:23:23 -03:00
Daniel Arantes Loverde
705639b2f5 [docs] Scaffold reference guides + README Documentation section
- Documentation/{Extensions,SwiftUI,UIKit}.md shells with category headings + TOC
- README: Documentation table linking all 4 guides
- footer: drop the second github badge, keep the resume link
2026-08-29 19:22:12 -03:00
53 changed files with 7642 additions and 296 deletions

View File

@@ -1,5 +1,8 @@
# `API` — networking for LCEssentials
Part of the reference set: [Extensions.md](Extensions.md) ·
[SwiftUI.md](SwiftUI.md) · [UIKit.md](UIKit.md).
`API` is an `actor` that wraps `URLSession` for JSON REST calls and multipart
uploads. One line to send a typed request, decode the response, and get a
consistent error — instead of re-writing the same `URLRequest` / status-code /

2031
Documentation/Extensions.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,175 @@
# LCEssentials — Feature Control
A typed Swift client for Atomenta's Feature Control API: flag evaluation with a
TTL cache and safe-degrade fallback, an in-app notifications inbox, and batched
exposure telemetry. Lives in its own SPM product, `LCFeatureControl`, not in
`LCEssentials` itself — an optional sub-package that target-depends on
`LCEssentials` (same shape as [`LCECryptoKit`](Extensions.md), see its section
there for the sub-SPM rationale).
Every section is a collapsible block — click a heading to expand it.
## Contents
- [Setup](#setup)
- [Evaluating flags](#evaluating-flags)
- [Notifications inbox](#notifications-inbox)
- [Exposure telemetry](#exposure-telemetry)
- [Errors](#errors)
---
## Setup
Add the product explicitly alongside `LCEssentials` to a consumer's `Package.swift`:
```swift
.product(name: "LCEssentials", package: "LCEssentials"),
.product(name: "LCFeatureControl", package: "LCEssentials"),
```
```swift
import LCFeatureControl
```
<details>
<summary><b>FeatureControlConfiguration</b> — wiring for one module deployment</summary>
```swift
let configuration = FeatureControlConfiguration(
baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlBearerAuth { await session.currentJWT() }
)
```
| Property | Default | Notes |
| --- | --- | --- |
| `cacheTTL` | `45` seconds | In-memory client-side cache window |
| `requestTimeout` | `3` seconds | Per-request timeout budget |
| `evaluatePath` / `notificationsPath` / `telemetryPath` | `/api/feature-control/...` | Override only for a non-default BFF mount |
</details>
<details>
<summary><b>Auth strategies</b> — <code>FeatureControlAuthorizing</code></summary>
No `Atomenta-Token` (module token) type exists in this package on purpose — that
token must never be embedded in a customer-facing app. Pick one:
```swift
// Internal/admin app hitting Atomenta directly with a panel-role JWT.
let auth = FeatureControlBearerAuth { await session.currentJWT() }
// Customer app calling its own BFF, which enforces its own auth
// adds exactly the given headers, never synthesizes Authorization.
let auth = FeatureControlHeaderAuth(headers: ["X-BFF-Session": sessionToken])
```
</details>
## Evaluating flags
<details open>
<summary><b>FeatureControlManager</b> — cache, safe-degrade, invalidation</summary>
```swift
let manager = FeatureControlManager(
configuration: configuration,
defaults: ["fc.checkout_v2": FeatureControlFlag(enabled: false, variant: nil,
payload: nil, reason: "default")]
)
let context = FeatureControlContext(subjectType: .customer, subjectId: user.id,
storeId: store.id, platform: "ios",
appVersion: appVersion)
let snapshot = await manager.evaluate(keys: ["fc.checkout_v2"], context: context)
snapshot.flags["fc.checkout_v2"]?.enabled // Bool
```
`evaluate(keys:context:)` **never throws** — fresh fetch → stale cache → the
`defaults` passed at init, in that order. Use it everywhere a flag gates real
product behaviour. `evaluateOrThrow(keys:context:)` surfaces the real failure
instead, for admin/debug tooling only.
Concurrent calls for the same key on a cold cache are coalesced onto a single
in-flight request — three callers evaluating the same key at once produce one
network call, not three.
```swift
await manager.invalidateCache() // call on user/store change
```
The cache key is `environment + subjectId + sortedKeys + platform + appVersion`
— it does **not** include `storeId` or `attributes`. If a subject's flags can
legitimately differ by store, call `invalidateCache()` on store switch.
</details>
## Notifications inbox
<details>
<summary><b>FeatureControlNotificationsClient</b> — JWT-only</summary>
The OpenAPI fragment declares `security: [bearerAuth]` for every notifications
route, no module-token alternative — configure with `FeatureControlBearerAuth`.
```swift
let client = FeatureControlNotificationsClient(configuration: configuration)
let (items, nextCursor) = try await client.list(status: .unread, limit: 20)
let (more, _) = try await client.list(status: .unread, limit: 20, cursor: nextCursor)
try await client.markRead(id: notification.id)
try await client.markAllRead()
```
</details>
## Exposure telemetry
<details>
<summary><b>recordExposure(_:)</b> / <b>flushExposures()</b> — best-effort, batched</summary>
```swift
await manager.recordExposure(FeatureControlExposureEvent(
featureKey: "fc.checkout_v2", variant: "on",
subjectType: .customer, storeId: store.id
))
// ... later, e.g. on app background or a periodic timer:
await manager.flushExposures()
```
Buffers locally — nothing is sent until `flushExposures()` is called. Splits
into batches of ≤100 (server `maxItems: 100`). Not a critical-path operation: a
failed batch is dropped, never retried indefinitely.
</details>
## Errors
<details>
<summary><b>FeatureControlError</b> — from <code>evaluateOrThrow</code> and the notifications client</summary>
```swift
public enum FeatureControlError: Error, Sendable, Equatable {
case invalidContext(code: String) // 400
case unauthorized(code: String) // 401
case forbidden(code: String) // 403
case rateLimited(code: String) // 429
case server(code: String, status: Int)
case transport(message: String) // decode failure / non-HTTP error
}
```
The safe `evaluate(...)` path never surfaces this — it degrades to cache/defaults
instead. `evaluateOrThrow` also throws `.server(code:status: 200)` if the server
responds `200` with an `{"error": true}` envelope (a degraded-but-200 response).
</details>
---
See [Extensions.md](Extensions.md) for the rest of `LCEssentials`' Foundation
helpers, including the sibling `LCECryptoKit` sub-product.

117
Documentation/SwiftUI.md Normal file
View File

@@ -0,0 +1,117 @@
# LCEssentials — SwiftUI
SwiftUI components and `View` helpers. Foundation/value-type helpers are in
[Extensions.md](Extensions.md); UIKit-era helpers in [UIKit.md](UIKit.md).
Every section is a collapsible block — click a heading to expand it.
## Contents
- [Navigation](#navigation)
- [View helpers](#view-helpers)
---
## Navigation
<details>
<summary><b>LCENavigationView</b> — customizable navigation bar (iOS 15+)</summary>
A drop-in replacement for the system navigation bar with left/right buttons,
title + subtitle, background colour, and a hide toggle. Configuration methods
return `self`, so they chain. **Per the workspace iOS standards, this component
is mandatory on new SwiftUI screens instead of a hand-rolled bar.**
`@available(iOS 15, *)`, iOS only.
### `init(title: (any View) = Text(""), subTitle: (any View) = Text(""), @ViewBuilder content: () -> Content)`
The `content` closure is everything shown **below** the bar.
```swift
LCENavigationView(title: Text("Profile")) {
ScrollView {
ProfileForm()
}
}
```
### `func setTitle(text: (any View) = Text(""), subTitle: (any View)? = nil) -> LCENavigationView`
Set (or replace) the title and optional subtitle. Passing no `subTitle` hides
the subtitle row.
```swift
LCENavigationView { Content() }
.setTitle(text: Text("Orders"), subTitle: Text("32 open"))
```
### `func setLeftButton(text: Text = Text(""), image: (any View)? = nil, action: @escaping () -> Void) -> LCENavigationView`
Configure the leading button. If the trailing button has no text/image yet, a
transparent placeholder is added on that side so the title stays centred.
```swift
.setLeftButton(text: Text("Back"), image: Image(systemName: "chevron.left")) {
dismiss()
}
```
### `func setRightButton(text: Text = Text(""), image: (any View)? = nil, action: @escaping () -> Void) -> LCENavigationView`
Configure the trailing button (same placeholder behaviour for the leading side).
```swift
.setRightButton(image: Image(systemName: "plus")) {
showingNewItem = true
}
```
### `func hideNavigationView(_ hide: Bool) -> LCENavigationView`
Show or hide the whole bar (the content stays).
```swift
.hideNavigationView(isFullScreenMedia)
```
### `func setNavigationBarBackgroundColor(_ color: Color) -> LCENavigationView`
Bar background colour (extends into the top safe area). Defaults to `.clear`.
```swift
.setNavigationBarBackgroundColor(.blue.opacity(0.1))
```
### Full example
```swift
struct OrdersScreen: View {
@Environment(\.dismiss) private var dismiss
@State private var showingNew = false
var body: some View {
LCENavigationView(title: Text("Orders")) {
OrdersList()
}
.setLeftButton(text: Text("Back"),
image: Image(systemName: "chevron.left")) { dismiss() }
.setRightButton(image: Image(systemName: "plus")) { showingNew = true }
.setNavigationBarBackgroundColor(Color(.systemBackground))
}
}
```
> `LCENavigationState` (the `@Published` backing store) and the reflection-based
> `Text.string` / tag helpers in `View+Ext.swift` are `internal` implementation
> details — not part of the public API.
</details>
---
## View helpers
There are currently no public standalone `View` extensions — the `getTag` /
`extractTag` reflection utilities in `SwiftUI/View+Ext.swift` are `internal` and
exist only to support `LCENavigationView`'s subtitle handling.

815
Documentation/UIKit.md Normal file
View File

@@ -0,0 +1,815 @@
# LCEssentials — UIKit
UIKit-era helpers: programmatic layout & constraints, view/control extensions,
navigation, table/collection helpers, and drop-in components (image picker, image
zoom, snackbar, GIF loading).
Foundation/value-type helpers are in [Extensions.md](Extensions.md); SwiftUI
helpers in [SwiftUI.md](SwiftUI.md).
Every section is a collapsible block — click a heading to expand it.
## Contents
- [Layout & Constraints](#layout--constraints)
- [Views & Controls](#views--controls)
- [Navigation & Controllers](#navigation--controllers)
- [Collections & Tables](#collections--tables)
- [Media & Components](#media--components)
---
## Layout & Constraints
<details>
<summary><b>UIView</b> — programmatic constraints, chainable anchors</summary>
### `enum AnchorType`
The anchor to pin when using `setConstraintsTo`. Cases: `all`, `top`, `bottom`,
`leading`, `trailing`, `left`, `right`, `centerX`, `centerY`, `width`, `heigth`
*(sic)*, `topToBottom`, `bottomToTop`, `leadingToTrailing`, `trailingToLeading`,
plus `…GreaterThanOrEqualTo` / `…LessThanOrEqualTo` variants.
### `@discardableResult func setConstraintsTo(parentView: UIView, anchorType: AnchorType, value: CGFloat, safeArea: Bool = false) -> Self`
Activate one constraint against `parentView`. Remembers `parentView` (in
`viewReference`) so follow-up calls can omit it. `safeArea: true` pins top/bottom
to the safe-area guide (iOS 11+).
```swift
card.addSubview(label)
label.setConstraintsTo(parentView: card, anchorType: .top, value: 12, safeArea: false)
.setConstraints(.leading, 16)
.setConstraints(.trailing, -16)
```
### `@discardableResult func setConstraintsTo(_ parentView:_ anchorType:_ value:_ safeArea: Bool = false) -> Self`
Positional-argument shorthand for the above.
### `@discardableResult func setConstraintsTo(anchorType: AnchorType, value: CGFloat, safeArea: Bool = false) -> UIView`
Reuse the last `parentView` (`viewReference`). **Traps** with `fatalError` if no
`parentView` was set first.
### `@discardableResult func setConstraints(_ anchorType: AnchorType, _ value: CGFloat, _ safeArea: Bool = false) -> UIView`
Positional shorthand for the reuse form — the one you chain.
### `func setConstraints(_ toScrollView: UIScrollView, direction: UICollectionView.ScrollDirection = .vertical)`
Pin `self` as the single content view of a scroll view (edges + matching
width/height, with a low-priority constraint on the scroll axis). Traps if
`self` is itself a `UIScrollView`.
```swift
scrollView.addSubview(content)
content.setConstraints(scrollView, direction: .vertical)
```
### Constraint accessors
`widthConstraint`, `heightConstraint`, `leadingConstraint`, `trailingConstraint`,
`topConstraint`, `bottomConstraint`, `centerXConstraints`, `centerYConstraints`
the first matching `NSLayoutConstraint` found by walking up the hierarchy.
```swift
box.widthConstraint?.constant = 120
UIView.animate(withDuration: 0.2) { self.view.layoutIfNeeded() }
```
### `func findConstraint(attribute: NSLayoutConstraint.Attribute, for view: UIView) -> NSLayoutConstraint?`
Search self and ancestors for a constraint on `attribute` involving `view`.
### `func constraints(on anchor:) -> [NSLayoutConstraint]`
Constraints touching a given `NSLayoutYAxisAnchor` / `XAxisAnchor` / `Dimension` of `self`.
### `@discardableResult func setHeight(size:) -> Self` / `setHeight(min:)` / `setWidth(size:)` / `setWidth(min:)`
Activate a fixed or minimum dimension constraint.
```swift
avatar.setWidth(size: 44).setHeight(size: 44)
```
### `func setWidth(_ toView: UIView? = nil, constant: CGFloat, _ multiplier: CGFloat = 0) -> Self` / `setHeight(...)`
Dimension relative to another view (`multiplier`) or fixed (`multiplier == 0`).
### Frame setters
`setX(x:)`, `setY(y:)`, `setFrameWidth(width:)`, `setFrameHeight(height:)` — mutate
`self.frame` directly (manual layout, not Auto Layout).
</details>
<details>
<summary><b>UIView</b> — hierarchy, appearance, effects</summary>
### `static var className: String`
`String(describing: self)`.
```swift
UIButton.className // "UIButton"
```
### `func addSubview(_ subview:, translatesAutoresizingMaskIntoConstraints: Bool = false)` / `addSubviews(_ subviews: [UIView], …)`
Add a view (or many) and set `translatesAutoresizingMaskIntoConstraints` in one call.
```swift
view.addSubviews([header, body, footer]) // all ready for Auto Layout
```
### `func subviews<T>(ofType _: T.Type) -> [T]`
All descendants of a type, recursively.
```swift
formView.subviews(ofType: UITextField.self)
```
### `func findAView<T>(_ ofType: T.Type) -> T?`
First descendant of a type.
### `var parentViewController: UIViewController?`
Nearest owning view controller (walks the responder chain).
### `var borderColor: UIColor?` / `var borderWidth: CGFloat` / `var cornerRadius: CGFloat`
Layer border/corner shortcuts (`cornerRadius` also sets `masksToBounds`).
```swift
card.cornerRadius = 12
card.borderWidth = 1
card.borderColor = .separator
```
### `func setRadius(top: Bool, bottom: Bool, radius: CGFloat = 8)`
Round only the top and/or bottom corners.
```swift
sheet.setRadius(top: true, bottom: false, radius: 16)
```
### `func applyShadow(color:offSet:radius:opacity:shouldRasterize: Bool = true, rasterizationScaleTo: = UIScreen.main.scale)` / `func removeShadow()`
Layer shadow with rasterisation on by default.
```swift
card.applyShadow(color: .black, offSet: CGSize(width: 0, height: 2),
radius: 6, opacity: 0.15)
```
### `func insertBlurView(style: UIBlurEffect.Style, color: UIColor = .black, alpha: CGFloat = 0.9)`
Insert a full-bleed `UIVisualEffectView` behind the content.
### `func drawCircle(inCoord x:y:with radius:strokeColor: = .red, fillColor: = .gray, isEmpty: Bool = false) -> [String: Any]`
Add a circular `CAShapeLayer`; returns `["path": UIBezierPath, "layer": CAShapeLayer]`.
### `var isRightToLeft: Bool`
Effective RTL layout direction.
### `var screenshot: UIImage?` / `func asImage() -> UIImage`
Render the view to an image (`screenshot` via the legacy context, `asImage` via `UIGraphicsImageRenderer`).
```swift
let png = chartView.asImage().pngData()
```
### `var globalPoint: CGPoint?` / `var globalFrame: CGRect?` / `var absolutePosition: CGRect`
Origin/frame converted to window coordinates.
### `func fadeIn(withDuration: TimeInterval = 1, withDelay: TimeInterval = 0, completionHandler: @escaping (Bool) -> Void)` / `func fadeOut(...)`
Animate `alpha` to 1 / 0. *(Completion-handler API — pre-dates async/await.)*
```swift
overlay.fadeOut { _ in overlay.removeFromSuperview() }
```
### `var viewReference: UIView?`
Scratch reference used internally by the chained `setConstraintsTo` calls.
</details>
<details>
<summary><b>GradientOrientation / EnumBorderSide</b></summary>
### `enum GradientOrientation`
`topRightBottomLeft`, `topLeftBottomRight`, `horizontal`, `vertical` — maps to a
`CAGradientLayer` start/end point pair. Passed to gradient helpers elsewhere in
the package.
### `enum EnumBorderSide`
`top`, `bottom`, `left`, `right` — which edge to draw a single-side border on.
</details>
<details>
<summary><b>UIStackView</b> — convenience init, bulk arranged-subview ops (iOS 9+)</summary>
### `convenience init(arrangedSubviews: [UIView]? = nil, axis: = .vertical, spacing: = 0, alignment: = .fill, distribution: = .fill, layoutMargins: = .zero, isMarginsRelative: Bool = true)`
One-call configured stack view.
```swift
let stack = UIStackView(arrangedSubviews: [title, subtitle],
axis: .vertical, spacing: 4)
```
### `func addArrangedSubviews(_ views: [UIView], translateAutoresizing: Bool = false)`
Add many arranged subviews, setting their autoresizing flag.
### `func removeAllArrangedSubviews(deactivateConstraints: Bool = true)`
Remove and dispose every arranged subview.
### `func removeSubview(view: UIView, deactivateConstraints: Bool = true)`
Remove one arranged subview (optionally deactivating its constraints and removing from the hierarchy).
### `func addSpace(_ size: CGFloat, backgroundColor: UIColor = .clear)`
Insert a fixed-size spacer view sized along the stack's axis.
```swift
stack.addArrangedSubviews([row1, row2])
stack.addSpace(24)
stack.addArrangedSubview(row3)
```
</details>
> `NSLayoutConstraint` helpers (`constraintWithMultiplier`, `matches(view:anchor:)`)
> and `[NSLayoutConstraint].filtered(view:anchor:)` are `internal` — used by the
> `UIView.constraints(on:)` accessors above, not called directly.
## Views & Controls
<details>
<summary><b>UIButton</b> — per-state accessors, all-state setters</summary>
### Per-state properties
`imageForNormal` / `imageForHighlighted` / `imageForSelected` / `imageForDisabled`,
`titleForNormal` / `…Highlighted` / `…Selected` / `…Disabled`,
`titleColorForNormal` / `…Highlighted` / `…Selected` / `…Disabled` — get/set
shortcuts for the matching `UIControl.State` (also `@IBInspectable`).
```swift
button.titleForNormal = "Save"
button.titleColorForDisabled = .tertiaryLabel
```
### `func setTitleForAllStates(_:)` / `func setTitleColorForAllStates(_:)` / `func setImageForAllStates(_:)`
Apply one value to `.normal`, `.selected`, `.highlighted`, `.disabled` at once.
```swift
button.setTitleColorForAllStates(.white)
```
### `func centerTextAndImage(spacing: CGFloat)`
Balance title/image edge insets so text + icon sit centred with `spacing` between them.
```swift
button.centerTextAndImage(spacing: 8)
```
</details>
<details>
<summary><b>UILabel</b></summary>
### `func lineNumbers() -> Int`
Rendered line count at the current width.
```swift
if bodyLabel.lineNumbers() > 3 { showMoreButton() }
```
### `var getEstimatedHeight: CGFloat`
Height the label would need to show its full text/attributed text unclipped.
</details>
<details>
<summary><b>UITextField</b></summary>
### `var placeholderColor: UIColor`
Get/set the placeholder text colour (rebuilds `attributedPlaceholder`; setter is a
no-op if no placeholder text is set).
```swift
field.placeholder = "Email"
field.placeholderColor = .secondaryLabel
```
### `func addPaddingLeft(_ padding: CGFloat)`
Inset the text from the left with an empty spacer view.
### `func addPaddingLeftIcon(_ image: UIImage, padding: CGFloat)`
Left view = an icon plus trailing padding.
```swift
field.addPaddingLeftIcon(UIImage(systemName: "magnifyingglass")!, padding: 8)
```
</details>
<details>
<summary><b>UIImageView</b> (<code>@MainActor</code>)</summary>
### `func changeColorOfImage(_ color: UIColor, image: UIImage?) -> UIImageView`
Set a template-rendered image tinted to `color`; returns `self`.
```swift
iconView.changeColorOfImage(.systemBlue, image: UIImage(named: "star"))
```
### `var encodeToBase64: String?`
JPEG (quality 0.6) of the current image as a Base64 string.
### `func addAspectRatioConstraint()` / `func removeAspectRatioConstraint()`
Add / remove a width-to-height constraint matching the current image's ratio.
```swift
photoView.image = photo
photoView.addAspectRatioConstraint()
```
</details>
<details>
<summary><b>UIImage</b> — recolour, resize, thumbnails, masks, init</summary>
### `func imageWithColor(color: UIColor) -> UIImage` / `func tintImage(color: UIColor) -> UIImage`
Return a copy filled / tinted with `color` (keeps the alpha shape).
```swift
let redIcon = icon.tintImage(color: .systemRed)
```
### `func backgroundColorTransparent(initialColor: UIColor, finalColor: UIColor) -> UIImage?`
Make pixels in a colour range transparent.
### `class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1) -> UIImage?`
Generate a stroked-ellipse image.
```swift
UIImage.outlinedEllipse(size: CGSize(width: 24, height: 24), color: .label)
```
### `func resizeImage(newWidth: CGFloat) -> UIImage`
Scale to `newWidth`, keeping the aspect ratio.
### `func createThumbnail(_ maxPixelSize: UInt) -> UIImage`
Fast down-sampled thumbnail via `CGImageSource`.
```swift
let thumb = fullImage.createThumbnail(200)
```
### `func maskWithAlphaImage(maskImage: UIImage) -> UIImage`
Use another image's alpha as a mask.
### `func isAnimated() -> Bool`
Whether the image has more than one frame.
### `init?(base64String: String, scale: CGFloat = 1)`
Decode a Base64 string to an image.
### `init(view: UIView)` — *`@MainActor`*
Rasterise a view into an image.
```swift
let snapshot = UIImage(view: cardView)
```
</details>
<details>
<summary><b>UIColor</b> — hex, components</summary>
### `convenience init(hex: String)`
Parse `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` (with or without `#`).
```swift
view.backgroundColor = UIColor(hex: "#1E88E5")
UIColor(hex: "FF0000CC") // red at 80% alpha
```
### `var hexString: String?`
`#RRGGBB` (or `#RRGGBBAA` when alpha < 1).
### `var redValue` / `var greenValue` / `var blueValue` / `var alphaValue`
Individual channel values (`CGFloat`, via `CIColor`).
```swift
UIColor.systemBlue.redValue // 0.01.0
```
</details>
<details>
<summary><b>UIScrollView</b> — snapshot, visible rect, paged scrolling</summary>
### `var snapshot: UIImage?`
Image of the **entire** content size (not just the visible part) — works on
`UITableView` / `UICollectionView` too.
### `var visibleRect: CGRect`
The currently visible content region.
### `var offsetInPage: CGFloat`
Fractional position within the current page height (`0.0``1.0`).
### `func scrollUp(animated:)` / `scrollDown(animated:)` / `scrollLeft(animated:)` / `scrollRight(animated:)`
Move one page (respects `isPagingEnabled`). `animated` defaults to `true`.
```swift
nextButton.onTap = { scrollView.scrollRight() }
```
### `enum orientation`
`horizontal` / `vertical` — helper enum used by scroll utilities.
</details>
<details>
<summary><b>UITapGestureRecognizer</b></summary>
### `func didTapAttributedTextInLabel(label: UILabel, textToTouch: String) -> Bool`
Whether the tap landed on a given substring of a label's attributed text — for
making part of a label tappable.
```swift
@objc func handleTap(_ g: UITapGestureRecognizer) {
if g.didTapAttributedTextInLabel(label: termsLabel, textToTouch: "Terms of Use") {
openTerms()
}
}
```
</details>
## Navigation & Controllers
<details>
<summary><b>UINavigationController</b> — completion-handler push/pop, transparent bar</summary>
### `func pushViewController(_:animated: Bool = true, completion: (() -> Void)? = nil)`
### `func popViewController(animated: Bool = true, _ completion: (() -> Void)? = nil)`
### `func popToViewController(_:animated: Bool = true, _ completion: (() -> Void)? = nil)`
The standard transitions with a completion block (wrapped in a `CATransaction`).
```swift
navigationController?.pushViewController(detail) {
print("detail is on screen")
}
```
### `func pushViewController(_:hidesBottomBar: Bool = false, animated: Bool = true)` — *not tvOS*
Push while setting `hidesBottomBarWhenPushed`.
### `func makeTransparent(withTint tint: UIColor = .white)`
Clear background + shadow, translucent, tinted bar/title.
```swift
navigationController?.makeTransparent(withTint: .label)
```
</details>
<details>
<summary><b>UIViewController</b> — state, instantiation, dismissal, toasts, keyboard, observers</summary>
### `var isVisible: Bool` / `var isLoaded: Bool`
View is loaded **and** in a window. (`isLoaded` is an alias.)
### `var isModal: Bool`
Whether the controller is presented modally (vs. pushed).
### `static var className: String` / `static var identifier: String` / `static var segueID: String`
`"MyVC"`, `"idMyVC"`, `"idSegueMyVC"`.
### `static func instantiate<T>(storyBoard: String, identifier: String? = nil, bundle: Bundle? = …) -> T`
Load a controller from a storyboard by (default) `T.identifier`.
```swift
let vc: ProfileVC = ProfileVC.instantiate(storyBoard: "Main")
```
### `static func instatiate<T>(nibName: String, bundle: Bundle? = nil) -> T` *(sic — "instatiate")*
Load from a nib and force an initial layout pass.
### `func present(viewControllerToPresent:completion: @escaping () -> Void)`
`present(_:animated:)` with a completion block.
### `func closeController(jumpToController: UIViewController? = nil, completion: @escaping () -> Void)`
Dismiss if modal, else pop (to `jumpToController` if given), then call `completion`.
```swift
closeController { self.refreshList() }
```
### `func show(toastWith message:, font: = system 12, toastPosition: ToastPosition, backgroundColor: = .black, textColor: = .white, duration: = 3)`
Show a temporary rounded toast label. `ToastPosition` is `.top` / `.down`
(notch-aware at the top).
```swift
show(toastWith: "Saved", toastPosition: .down)
```
### `func addNotificationObserver(name:selector:)` / `func removeNotificationObserver(name:)` / `func removeNotificationsObserver()`
`NotificationCenter` registration shortcuts (last one removes all).
### `@objc func dismissSystemKeyboard(_ sender: UITapGestureRecognizer)`
Ready-made selector for a tap-to-dismiss-keyboard gesture.
```swift
view.addGestureRecognizer(UITapGestureRecognizer(target: self,
action: #selector(dismissSystemKeyboard(_:))))
```
</details>
<details>
<summary><b>UITabBarController</b> — badges, animated tab switching</summary>
### `func setBadges(badgeValues: [Int], font: UIFont = Helvetica-Light 11)`
Set numeric badges for every tab at once (`0` = no badge). Custom-drawn
(`CustomTabBadge` label), so they position above each tab item.
```swift
tabBarController?.setBadges(badgeValues: [0, 3, 0, 12])
```
### `func addBadge(index:value:color:font:)`
Add a single custom badge.
### `func setSelectedView(atIndex:withAnimation: Bool = false, completion:)`
Select a tab (pops that tab's nav stack to root first).
### `func setSelectedView(withNoPop atIndex:withAnimation: Bool = false, completion:)`
Same, without popping to root.
### `func animateToTab(toIndex: Int)`
Slide-transition between tabs.
### `func changeViewControllerToItem(withViewController:Item:)`
Replace a tab's root controller then switch to that tab.
```swift
tabBarController?.changeViewControllerToItem(withViewController: NewHome(), Item: 0)
```
### `class CustomTabBadge: UILabel`
The badge label type used above (`init(font:)`).
</details>
<details>
<summary><b>UIApplication</b> — environment, app info, open URL</summary>
### `enum Environment` + `static var inferredEnvironment: Environment`
`.debug` / `.testFlight` / `.appStore`, inferred from build config, simulator, and
the provisioning/receipt files.
```swift
if UIApplication.inferredEnvironment == .appStore { enableAnalytics() }
```
### `static var displayName: String?` / `static var buildNumber: String?` / `static var version: String?`
Bundle info values. (`LCEssentials.appVersion` etc. forward to these.)
### `static func openURL(urlStr: String)`
Open a URL string if it can be opened.
```swift
UIApplication.openURL(urlStr: "https://loverde.com.br")
```
</details>
<details>
<summary><b>UIResponder</b></summary>
### `var getParentViewController: UIViewController?`
Walk the responder chain to the owning view controller.
```swift
someView.getParentViewController?.present(alert, animated: true)
```
</details>
<details>
<summary><b>UIDevice</b> — notch metrics, model name</summary>
### `static var topNotch: CGFloat` / `static var bottomNotch: CGFloat` / `static var hasNotch: Bool`
Safe-area top/bottom insets and whether the device has a notch / Dynamic Island.
```swift
let headerY: CGFloat = UIDevice.hasNotch ? 44 : 20
```
### `var modelName: String`
Marketing name from the hardware identifier (`"iPhone 15 Pro"`, `"iPad Air (5th generation)"`, …); falls back to the raw identifier for unknown devices. Reads `SIMULATOR_MODEL_IDENTIFIER` on the simulator.
```swift
UIDevice.current.modelName // "iPhone 16 Pro"
```
</details>
## Collections & Tables
<details>
<summary><b>UITableView</b> — typed dequeue, safe indexing, cell animations</summary>
### `func dequeueReusableCell<T: UITableViewCell>(withClass name: T.Type) -> T` / `…(withClass:for indexPath:) -> T`
Dequeue a cell by its class name as the identifier. **Traps** if the cell isn't registered.
```swift
let cell = tableView.dequeueReusableCell(withClass: OrderCell.self, for: indexPath)
```
### `func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(withClass name: T.Type) -> T`
Same, for section header/footer views.
### `func dequeueCell<T: UITableViewCell>(indexPath: IndexPath) -> T`
Dequeue using `T.identifier` (`"id" + class name`).
### `func reloadData(_ completion: @escaping () -> Void)`
`reloadData()` with a callback for when layout settles.
### `func isValidIndexPath(_:) -> Bool`
Bounds check against the current section/row counts.
### `func safeScrollToRow(at:at scrollPosition:animated:)`
`scrollToRow` that silently no-ops for an out-of-range index path.
### `func makeMoveUpWithFadeAnimation(rowHeight:duration:delayFactor:) -> UITableViewCellAnimation`
Build a staggered slide-up + fade-in cell animation closure.
```swift
let animator = UITableViewAnimator(
animation: tableView.makeMoveUpWithFadeAnimation(rowHeight: 64, duration: 0.35, delayFactor: 0.03)
)
func tableView(_ t: UITableView, willDisplay cell: UITableViewCell, forRowAt ip: IndexPath) {
animator.animate(cell: cell, at: ip, in: t)
}
```
### `typealias UITableViewCellAnimation = (UITableViewCell, IndexPath, UITableView) -> Void`
### `class UITableViewAnimator`
`init(animation:)` + `animate(cell:at:in:)` — runs a cell animation closure.
### `UITableViewCell.identifier` / `UITableViewCell.prepareDisclosureIndicator()`
`"id" + class name`; and re-tint the disclosure chevron to a template image so it
picks up `tintColor`.
</details>
<details>
<summary><b>UICollectionView</b> — setup, counts, safe indexing, carousel layout</summary>
### `static var identifier: String`
`"id" + class name`.
### `func setupCollectionView(flowLayout: = UICollectionViewFlowLayout(), spacings: = 0, direction: = .horizontal, edgesInset: = .zero, allowMulpleSelection: Bool = false, automaticSize: CGSize? = nil)`
Configure layout spacing/direction/insets, multi-selection, and self-sizing in one call.
```swift
collectionView.setupCollectionView(spacings: 8, direction: .vertical,
edgesInset: .init(top: 12, left: 16, bottom: 12, right: 16))
```
### `func reloadData(_ completion: @escaping () -> Void)`
Reload with a completion callback.
### `func numberOfItems() -> Int`
Total items across all sections.
### `var lastSection: Int` / `var indexPathForLastItem: IndexPath?` / `func indexPathForLastItem(inSection:) -> IndexPath?`
Last section index; index path of the last item overall or in a section.
### `func isValidIndexPath(_:) -> Bool` / `func safeScrollToItem(at:at scrollPosition:animated:)`
Bounds check; scroll that no-ops on an invalid index path.
### `enum CollectionViewFlowLayoutSpacingMode`
`.fixed(spacing:)` / `.overlap(visibleOffset:)` — spacing strategy for the carousel layout below.
### `open class CollectionViewFlowLayout: UICollectionViewFlowLayout`
A centred, paginated "cover-flow" style layout: the centred item is full size,
side items scale and fade. Tunables: `sideItemScale` (0.6), `sideItemAlpha`
(0.6), `sideItemShift` (0), `spacingMode` (`.fixed(40)`).
```swift
let layout = CollectionViewFlowLayout()
layout.itemSize = CGSize(width: 240, height: 320)
layout.sideItemScale = 0.7
layout.spacingMode = .overlap(visibleOffset: 30)
collectionView.collectionViewLayout = layout
```
</details>
## Media & Components
<details>
<summary><b>LCSnackBarView</b> — in-app notification banner</summary>
A chainable banner shown from the top or bottom of a controller. Configure with
`configure(...)` calls, then `present()`.
### `init(style: LCSnackBarViewType = .default, orientation: LCSnackBarOrientation = .top, delegate: LCSnackBarViewDelegate? = nil)`
### Enums
- `LCSnackBarViewType``.default` (rectangular) / `.rounded`
- `LCSnackBarOrientation``.top` / `.bottom`
- `LCSnackBarTimer: CGFloat``.infinity` (0, manual dismiss), `.minimum` (2s), `.medium` (5s), `.maximum` (10s)
### Configuration (each returns `Self`)
| Method | Sets |
|---|---|
| `configure(text: String)` | the message |
| `configure(textColor: UIColor)` | text colour |
| `configure(textFont: UIFont, alignment: NSTextAlignment = .center)` | font + alignment |
| `configure(backgroundColor: UIColor)` | banner background |
| `configure(exibition timer: LCSnackBarTimer)` | how long it stays |
| `configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil)` | leading icon |
### `func present(completion: (() -> Void)? = nil)`
Show on the top-most view controller.
### `weak var delegate: LCSnackBarViewDelegate?`
`snackbar(didStartExibition:)`, `snackbar(didTouchOn:)`, `snackbar(didEndExibition:)` — all optional.
### Full example
```swift
LCSnackBarView(style: .rounded, orientation: .bottom)
.configure(text: "Profile saved")
.configure(backgroundColor: .systemGreen)
.configure(exibition: .minimum)
.present()
```
</details>
<details>
<summary><b>ImagePickerController</b> — camera / photo-library picker with permissions</summary>
Wraps `UIImagePickerController`, handling camera & photo-library authorization
(including the "go to Settings" path) and presenting a source-choice alert.
### `init()`
### `weak var delegate: ImagePickerControllerDelegate?`
### `var isEditable: Bool` — allow in-picker cropping (default `false`)
### `func openImagePicker()`
Check permissions, then present the camera/library choice.
### `protocol ImagePickerControllerDelegate: AnyObject`
`imagePicker(didSelect image: UIImage?)` — the picked (or edited) image, or `nil` on cancel/failure.
```swift
let picker = ImagePickerController()
picker.delegate = self
picker.isEditable = true
present(picker, animated: false) { picker.openImagePicker() }
// ImagePickerControllerDelegate
func imagePicker(didSelect image: UIImage?) {
avatarView.image = image
}
```
</details>
<details>
<summary><b>ImageZoomController</b> — full-screen pinch-zoom / pan viewer</summary>
### `init(_ withImage: UIImage)`
### `var minimumZoomScale: CGFloat` (default `1.0`) / `var maximumZoomScale: CGFloat` (default `6.0`)
### `var addGestureToDismiss: Bool` (default `true`) — drag down to close
### `weak var delegate: ImageZoomControllerDelegate?`
### `func present(completion: (() -> Void)? = nil)` / `func dismiss(completion: (() -> Void)? = nil)`
Present from / dismiss to the top-most controller.
### `@objc protocol ImageZoomControllerDelegate`
`imageZoomController(controller:didZoom:)`, `imageZoomController(controller:didClose:)` — both optional.
```swift
let viewer = ImageZoomController(photo)
viewer.maximumZoomScale = 4
viewer.present()
```
</details>
<details>
<summary><b>GifHelper</b> — animated GIF loading</summary>
### `UIImageView.loadGif(name: String)` / `UIImageView.loadGif(asset: String)` *(iOS 9+)*
Decode a bundled `.gif` (or an asset-catalog data set) off the main thread and
assign the resulting animated `UIImage`.
```swift
bannerView.loadGif(name: "loading") // loading.gif in the bundle
bannerView.loadGif(asset: "confetti") // NSDataAsset "confetti"
```
### `UIImage.gif(data: Data) -> UIImage?` / `gif(url: String) -> UIImage?` / `gif(name: String) -> UIImage?` / `gif(asset: String) -> UIImage?`
Build an animated `UIImage` from GIF bytes / a URL string / a bundled file / an
asset-catalog entry. Frame delays are honoured (via the GCD of per-frame delays).
```swift
let spinner = UIImage.gif(name: "spinner")
imageView.image = spinner
```
</details>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>LCECryptoKit.framework/LCECryptoKit</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>LCECryptoKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>LCECryptoKit.framework/LCECryptoKit</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>LCECryptoKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,382 @@
#if 0
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
#ifndef LCECRYPTOKIT_SWIFT_H
#define LCECRYPTOKIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef unsigned char char8_t;
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
# else
# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM_TAG)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_TAG enum
# else
# define SWIFT_ENUM_TAG
# endif
#endif
#if !defined(SWIFT_ENUM_FWD_DECL)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type;
# else
# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name;
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_AVAILABILITY_DOMAIN)
# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if !__has_feature(nullability)
# define _Nonnull
# define _Nullable
# define _Null_unspecified
#elif !defined(__OBJC__)
# pragma clang diagnostic ignored "-Wnullability-extension"
#endif
#if !__has_feature(nullability_nullable_result)
# define _Nullable_result _Nullable
#endif
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__cplusplus)
extern "C" {
#endif
#if defined(__cplusplus)
} // extern "C"
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif // defined(__OBJC__)
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
#endif // defined(__OBJC__)
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#else
#error unsupported Swift architecture
#endif

View File

@@ -0,0 +1,417 @@
{
"ABIRoot": {
"kind": "Root",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Import",
"name": "Foundation",
"printedName": "Foundation",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "Import",
"name": "CommonCrypto",
"printedName": "CommonCrypto",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "TypeDecl",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Function",
"name": "generateRandomAESKeyString",
"printedName": "generateRandomAESKeyString()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "generateSalt",
"printedName": "generateSalt()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ",
"mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeClientHash",
"printedName": "computeClientHash(email:password:salt:)",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeLoginBearerToken",
"printedName": "computeLoginBearerToken(userId:clientHash:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpEncode",
"printedName": "otpEncode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpDecode",
"printedName": "otpDecode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
}
],
"declKind": "Struct",
"usr": "s:12LCECryptoKitAAV",
"mangledName": "$s12LCECryptoKitAAV",
"moduleName": "LCECryptoKit",
"declAttributes": [
"AccessControl"
],
"conformances": [
{
"kind": "Conformance",
"name": "Copyable",
"printedName": "Copyable",
"usr": "s:s8CopyableP",
"mangledName": "$ss8CopyableP"
},
{
"kind": "Conformance",
"name": "Escapable",
"printedName": "Escapable",
"usr": "s:s9EscapableP",
"mangledName": "$ss9EscapableP"
}
]
}
],
"json_format_version": 8
},
"ConstValues": []
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,3 @@
framework module LCECryptoKit {
header "LCECryptoKit-Swift.h"
}

View File

@@ -0,0 +1,760 @@
#if 0
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
#ifndef LCECRYPTOKIT_SWIFT_H
#define LCECRYPTOKIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef unsigned char char8_t;
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
# else
# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM_TAG)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_TAG enum
# else
# define SWIFT_ENUM_TAG
# endif
#endif
#if !defined(SWIFT_ENUM_FWD_DECL)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type;
# else
# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name;
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_AVAILABILITY_DOMAIN)
# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if !__has_feature(nullability)
# define _Nonnull
# define _Nullable
# define _Null_unspecified
#elif !defined(__OBJC__)
# pragma clang diagnostic ignored "-Wnullability-extension"
#endif
#if !__has_feature(nullability_nullable_result)
# define _Nullable_result _Nullable
#endif
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__cplusplus)
extern "C" {
#endif
#if defined(__cplusplus)
} // extern "C"
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif // defined(__OBJC__)
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
#endif // defined(__OBJC__)
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#elif defined(__x86_64__) && __x86_64__
// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
#ifndef LCECRYPTOKIT_SWIFT_H
#define LCECRYPTOKIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef unsigned char char8_t;
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
# else
# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM_TAG)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_TAG enum
# else
# define SWIFT_ENUM_TAG
# endif
#endif
#if !defined(SWIFT_ENUM_FWD_DECL)
# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum)
# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type;
# else
# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name;
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_AVAILABILITY_DOMAIN)
# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if !__has_feature(nullability)
# define _Nonnull
# define _Nullable
# define _Null_unspecified
#elif !defined(__OBJC__)
# pragma clang diagnostic ignored "-Wnullability-extension"
#endif
#if !__has_feature(nullability_nullable_result)
# define _Nullable_result _Nullable
#endif
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__cplusplus)
extern "C" {
#endif
#if defined(__cplusplus)
} // extern "C"
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif // defined(__OBJC__)
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
#endif // defined(__OBJC__)
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#else
#error unsupported Swift architecture
#endif

View File

@@ -0,0 +1,417 @@
{
"ABIRoot": {
"kind": "Root",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Import",
"name": "Foundation",
"printedName": "Foundation",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "Import",
"name": "CommonCrypto",
"printedName": "CommonCrypto",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "TypeDecl",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Function",
"name": "generateRandomAESKeyString",
"printedName": "generateRandomAESKeyString()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "generateSalt",
"printedName": "generateSalt()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ",
"mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeClientHash",
"printedName": "computeClientHash(email:password:salt:)",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeLoginBearerToken",
"printedName": "computeLoginBearerToken(userId:clientHash:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpEncode",
"printedName": "otpEncode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpDecode",
"printedName": "otpDecode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
}
],
"declKind": "Struct",
"usr": "s:12LCECryptoKitAAV",
"mangledName": "$s12LCECryptoKitAAV",
"moduleName": "LCECryptoKit",
"declAttributes": [
"AccessControl"
],
"conformances": [
{
"kind": "Conformance",
"name": "Copyable",
"printedName": "Copyable",
"usr": "s:s8CopyableP",
"mangledName": "$ss8CopyableP"
},
{
"kind": "Conformance",
"name": "Escapable",
"printedName": "Escapable",
"usr": "s:s9EscapableP",
"mangledName": "$ss9EscapableP"
}
]
}
],
"json_format_version": 8
},
"ConstValues": []
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target arm64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target arm64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,417 @@
{
"ABIRoot": {
"kind": "Root",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Import",
"name": "Foundation",
"printedName": "Foundation",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "Import",
"name": "CommonCrypto",
"printedName": "CommonCrypto",
"declKind": "Import",
"moduleName": "LCECryptoKit"
},
{
"kind": "TypeDecl",
"name": "LCECryptoKit",
"printedName": "LCECryptoKit",
"children": [
{
"kind": "Function",
"name": "generateRandomAESKeyString",
"printedName": "generateRandomAESKeyString()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "encodeSeed",
"printedName": "encodeSeed(email:password:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "decodeSeed",
"printedName": "decodeSeed(otpKey:hashKey:)",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "generateSalt",
"printedName": "generateSalt()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ",
"mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeClientHash",
"printedName": "computeClientHash(email:password:salt:)",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "computeLoginBearerToken",
"printedName": "computeLoginBearerToken(userId:clientHash:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpEncode",
"printedName": "otpEncode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl",
"RawDocComment"
],
"funcSelfKind": "NonMutating"
},
{
"kind": "Function",
"name": "otpDecode",
"printedName": "otpDecode(_:)",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Swift.String?",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"usr": "s:Sq"
},
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Func",
"usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ",
"moduleName": "LCECryptoKit",
"static": true,
"declAttributes": [
"AccessControl"
],
"funcSelfKind": "NonMutating"
}
],
"declKind": "Struct",
"usr": "s:12LCECryptoKitAAV",
"mangledName": "$s12LCECryptoKitAAV",
"moduleName": "LCECryptoKit",
"declAttributes": [
"AccessControl"
],
"conformances": [
{
"kind": "Conformance",
"name": "Copyable",
"printedName": "Copyable",
"usr": "s:s8CopyableP",
"mangledName": "$ss8CopyableP"
},
{
"kind": "Conformance",
"name": "Escapable",
"printedName": "Escapable",
"usr": "s:s9EscapableP",
"mangledName": "$ss9EscapableP"
}
]
}
],
"json_format_version": 8
},
"ConstValues": []
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,22 @@
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)
// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit
// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3
import CommonCrypto
import Foundation
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims
public struct LCECryptoKit {
public static func generateRandomAESKeyString() -> Swift.String
public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String?
public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String) -> Swift.String?
public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool
public static func generateSalt() -> Swift.String
public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String
public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String?
public static func otpEncode(_ plainText: Swift.String) -> Swift.String?
public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String?
}

View File

@@ -0,0 +1,3 @@
framework module LCECryptoKit {
header "LCECryptoKit-Swift.h"
}

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/LCECryptoKit-Swift.h</key>
<data>
uIM6/aOz59qnT/jGSBAiOinS2qo=
</data>
<key>Info.plist</key>
<data>
N0tkj+ldmO7dEKF+W1/6CjYNBrg=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<data>
mIejxCLstZ77ufLnGayqr8wDScc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<data>
GJVZwAs/SRyyfGkg1t2Twb8grXE=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<data>
vDvqI7FV3rThZRI4r/zPL877Z04=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<data>
GJVZwAs/SRyyfGkg1t2Twb8grXE=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<data>
0n6YBQBivkPuELjPpXBIj0CQFIc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<data>
mIejxCLstZ77ufLnGayqr8wDScc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<data>
WheGrMD0QDYsEwZ2vOGY+CQwW1o=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<data>
f52HXxbH78iRln4x78whSwyigbc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<data>
WheGrMD0QDYsEwZ2vOGY+CQwW1o=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<data>
FSzeAs9LdtELeWT3VL2PMjOFZcQ=
</data>
<key>Modules/module.modulemap</key>
<data>
OnB7ckFjsSU10c/y8kUFi69Cav4=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/LCECryptoKit-Swift.h</key>
<dict>
<key>hash2</key>
<data>
gHRFTmnGK28MYikS6gvnWErwc/oZ/scYeFssVTBw9MI=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash2</key>
<data>
/2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
Ldu7EDkaW9WinMjRHaBaSxpZoBNDXGvNn04410k9e5k=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
cuezjPqokn+YGP9z//TY4ZUxA1Dcmd6ApUafMMaQPAQ=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash2</key>
<data>
/2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
5S2/W4WoD0L4bl+NBuPQI7sGxqpZ7y2s7tme6Eu2oIQ=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
a5uCPhYWs0qR1YWQZb51vKm+4mYKUgc5ajZr2wEU6tU=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash2</key>
<data>
X+gHfuxKBEgqt+p9I9kR0FYyzml9UPuJpKzZAbPNcc0=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,15 +0,0 @@
{
"originHash" : "6e3fcf8724d6b7d2d0bbffc25b4ec3b9d8b65a6ef53d157498d5447fd26cf5db",
"pins" : [
{
"identity" : "lcecryptokitbinary",
"kind" : "remoteSourceControl",
"location" : "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
"state" : {
"revision" : "11f66b1b9785b4965e027c2fa4742f61e8453403",
"version" : "1.0.4"
}
}
],
"version" : 3
}

View File

@@ -1,46 +1,54 @@
// swift-tools-version: 6.0
import PackageDescription
import Foundation
let isLocalDevelopment = false //FileManager.default.fileExists(atPath: "../LCECryptoKit/PrivateLib/LCECryptoKitBinary")
let enableCryptoBinary = ProcessInfo.processInfo.environment["LCE_ENABLE_CRYPTO_BINARY"] != "0"
let cryptoPackageURL = isLocalDevelopment
? "../LCECryptoKit/PrivateLib/LCECryptoKitBinary"
: "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git"
let packageDependencies: [Package.Dependency] = enableCryptoBinary
? [
.package(url: cryptoPackageURL, exact: "1.0.4")
]
: []
let targetDependencies: [Target.Dependency] = enableCryptoBinary
? [
.product(name: "LCECryptoKit", package: "lcecryptokitbinary")
]
: []
// LCECryptoKit ships as a prebuilt .xcframework (`Frameworks/LCECryptoKit.xcframework`)
// vendored locally in this repo, no remote package dependency. iOS device + simulator slices
// only.
//
// Sub-SPM rule: `LCEssentials` installs standalone (no sub required). Every sub
// (`LCECryptoKit`, `LCFeatureControl`) target-depends on `LCEssentials`, so linking the sub's
// product always pulls `LCEssentials` in too a consumer never has to declare it separately.
//
// Platforms are iOS + watchOS only: `LCEssentials.API` (used by `LCFeatureControl`) is
// `#if os(iOS) || os(watchOS)`, and the vendored xcframework has no macOS/tvOS slice.
let package = Package(
name: "LCEssentials",
platforms: [
.iOS(.v15),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v8)
],
products: [
.library(
name: "LCEssentials",
targets: ["LCEssentials"]),
.library(
name: "LCECryptoKit",
targets: ["LCECryptoKitManager"]),
.library(
name: "LCFeatureControl",
targets: ["LCFeatureControl"]),
],
dependencies: packageDependencies,
targets: [
.target(
name: "LCEssentials",
dependencies: targetDependencies),
name: "LCEssentials"),
.binaryTarget(
name: "LCECryptoKit",
path: "Frameworks/LCECryptoKit.xcframework"),
.target(
name: "LCECryptoKitManager",
dependencies: [
"LCEssentials",
"LCECryptoKit"
]),
.target(
name: "LCFeatureControl",
dependencies: ["LCEssentials"]),
.testTarget(
name: "LCEssentialsTests",
dependencies: ["LCEssentials"]),
.testTarget(
name: "LCFeatureControlTests",
dependencies: ["LCFeatureControl", "LCEssentials"]),
]
)

View File

@@ -1,85 +1,44 @@
![](loverde_company_logo_full.png)
Loverde Co. Essentials Swift Scripts
----
This is a repository of essential scripts written in Swift for Loverde Co. used to save time on re-writing and keeping it on all other projects. So this Cocoapods will evolve with Swift and will improve with every release!
# Loverde Co. Essentials
Essential Swift scripts, extensions, SwiftUI components, and UIKit-era helpers,
shared across Loverde Co. projects. Evolves with Swift, improves every release.
## Requirements
- iOS 15.* or newer, Swift 5.* or newer.
## Features
- [x] Many usefull scripts extensions
- [x] `API` — typed async networking + multipart uploads. See **[Documentation/API.md](Documentation/API.md)**
- iOS 15 or newer · Swift 5 or newer
## Installation — Swift Package Manager
Installation
----
#### Swift Package Manager (SPM)
``` swift
```swift
dependencies: [
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "1.0.0"))
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "2.0.0"))
]
```
You can also add it via XCode SPM editor with URL:
Or add it in Xcode via **File ▸ Add Package Dependencies…** with the URL:
``` swift
```
https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
```
## Usage example
* Background Trhead
```swift
LCEssentials.backgroundThread(delay: 0.6, background: {
//Do something im background
}) {
//When finish, update UI
}
```
* NavigationController with Completion Handler
```swift
self.navigationController?.popViewControllerWithHandler(completion: {
//Do some stuff after pop
})
//or more simple
self.navigationController?.popViewControllerWithHandler {
//Do some stuff after pop
}
```
* Networking with `API`
```swift
struct User: Decodable, Sendable { let id: Int; let name: String }
let user: User = try await API.shared.request(
url: "https://api.example.com/users/{id}",
method: .get,
pathParams: ["id": "42"]
)
```
Full guide — requests, uploads, client certificates, error handling, and why it
beats a hand-rolled `URLSession`: **[Documentation/API.md](Documentation/API.md)**
## Another components
> LCESnackBarView - **great way to send feedback to user**
And then import `LCEssentials ` wherever you import UIKit or SwiftUI
``` swift
import LCEssentials
```
Any question or doubts, please send thru email
## Documentation
Daniel Arantes Loverde - <daniel@loverde.com.br>
| Guide | Covers |
| --- | --- |
| **[API.md](Documentation/API.md)** | `API` networking — typed requests, multipart uploads, client certificates, error handling, and why it beats a hand-rolled `URLSession` |
| **[Extensions.md](Documentation/Extensions.md)** | Foundation / value-type / string / collection / numeric / date / crypto extensions and the `LCEssentials` namespace |
| **[SwiftUI.md](Documentation/SwiftUI.md)** | SwiftUI components (`LCENavigationView`) and `View` helpers |
| **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components (`LCSnackBarView`, image picker/zoom, GIF loading) |
| **[FeatureControl.md](Documentation/FeatureControl.md)** | `LCFeatureControl` — Atomenta Feature Control client: flag evaluation, cache/safe-degrade, notifications inbox, exposure telemetry |
[![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "My Resume")](https://github.com/loverde-co/resume/)
[![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "Loverde Co. Github")](https://github.com/loverde-co)
---
Autor: Daniel Arantes Loverde
Daniel Arantes Loverde — <daniel@loverde.com.br>
[<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="28" alt="GitHub">](https://github.com/loverde-co/resume/)

View File

@@ -19,10 +19,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
#if canImport(LCECryptoKit)
import LCEssentials
import LCECryptoKit
public final class LCECryptoKitManager {
@@ -33,7 +31,7 @@ public final class LCECryptoKitManager {
self.hashKey = ""
}
public init(privateKey: String){
public init(privateKey: String) {
self.hashKey = privateKey
}
@@ -81,60 +79,3 @@ public final class LCECryptoKitManager {
LCECryptoKit.otpDecode(otpEncoded)
}
}
#else
public final class LCECryptoKitManager {
private let hashKey: String
public init() {
self.hashKey = ""
}
public init(privateKey: String){
self.hashKey = privateKey
}
public static func generateKey() -> String {
""
}
public func encodeTP(email: String, password: String) -> String? {
nil
}
public func decodeOTP(_ otpHash: String) -> String? {
nil
}
public func encodeOTPWithKey(email: String, password: String) -> String? {
nil
}
public func decodeOTPWithKey(_ otpHash: String) -> Bool {
false
}
// MARK: - Salted/Iterated/Peppered Login (atomenta-cryptokit-pepper-refactor-sdd.md)
public static func generateSalt() -> String {
""
}
public static func computeClientHash(email: String, password: String, salt: String) -> String {
""
}
public static func computeLoginBearerToken(userId: String, clientHash: String) -> String? {
nil
}
public static func otpEncode(_ plainText: String) -> String? {
nil
}
public static func otpDecode(_ otpEncoded: String) -> String? {
nil
}
}
#endif

View File

@@ -49,6 +49,39 @@ class LCENavigationState: ObservableObject {
@Published var subTitle: (any View) = Text("")
/// The background color of the navigation bar.
@Published var navigationBarBackgroundColor: Color = .clear
/// Whether a left button was actually configured via `setLeftButton`.
@Published var hasLeftButton: Bool = false
/// Whether a right button was actually configured via `setRightButton`.
@Published var hasRightButton: Bool = false
/// Whether buttons should opt into the system Liquid Glass button style (iOS 26+). Off by default.
@Published var useGlassButtons: Bool = false
}
/// `PreferenceKey` used to measure the widest side button so the title can be padded symmetrically and stay centered without overlapping either button.
@available(iOS 15, *)
private struct LCENavButtonWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
@available(iOS 15, *)
private extension View {
/// Applies `.glass` button style on iOS 26+ when enabled, otherwise falls back to `.plain` the navigation bar is not designed around glass, but stays opt-in ready.
@ViewBuilder
func lce_applyGlassIfEnabled(_ enabled: Bool) -> some View {
if enabled {
if #available(iOS 26.0, *) {
self.buttonStyle(.glass)
} else {
self.buttonStyle(.plain)
}
} else {
self.buttonStyle(.plain)
}
}
}
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
@@ -61,6 +94,9 @@ public struct LCENavigationView<Content: View>: View {
/// The content view displayed below the navigation bar.
let content: Content
/// The measured width of the widest side button, used to pad the title so it never overlaps either button.
@State private var maxButtonWidth: CGFloat = 0
/// Initializes a new `LCENavigationView` instance.
/// - Parameters:
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
@@ -92,18 +128,29 @@ public struct LCENavigationView<Content: View>: View {
/// The private `NavigationBarView` that lays out the navigation bar components.
private var NavigationBarView: some View {
HStack {
NavLeftButton
Spacer()
ZStack {
TitleView
.padding(.horizontal, maxButtonWidth)
.lineLimit(1)
.minimumScaleFactor(0.7)
.frame(maxWidth: .infinity)
HStack {
if state.hasLeftButton {
NavLeftButton
}
Spacer()
if state.hasRightButton {
NavRightButton
}
}
}
.font(.headline)
.padding()
.background {
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
}
.onPreferenceChange(LCENavButtonWidthPreferenceKey.self) { maxButtonWidth = $0 }
}
/// The private `TitleView` that displays the title and subtitle.
@@ -116,7 +163,7 @@ public struct LCENavigationView<Content: View>: View {
}
}
/// The private `NavLeftButton` view.
/// The private `NavLeftButton` view. Only rendered when `setLeftButton` was actually called never a hidden placeholder.
private var NavLeftButton: some View {
Button(action: state.leftButtonAction) {
HStack {
@@ -126,9 +173,15 @@ public struct LCENavigationView<Content: View>: View {
state.leftButtonText
}
}
.lce_applyGlassIfEnabled(state.useGlassButtons)
.background(
GeometryReader { proxy in
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
}
)
}
/// The private `NavRightButton` view.
/// The private `NavRightButton` view. Only rendered when `setRightButton` was actually called never a hidden placeholder.
private var NavRightButton: some View {
Button(action: state.rightButtonAction) {
HStack {
@@ -138,6 +191,12 @@ public struct LCENavigationView<Content: View>: View {
}
}
}
.lce_applyGlassIfEnabled(state.useGlassButtons)
.background(
GeometryReader { proxy in
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
}
)
}
/// Sets the configuration for the right button of the navigation bar.
@@ -158,13 +217,7 @@ public struct LCENavigationView<Content: View>: View {
}
state.rightButtonText = text
state.rightButtonAction = action
if let string = state.leftButtonText.string, string.isEmpty {
state.leftButtonText = text.foregroundColor(.clear)
}
if state.leftButtonImage == nil {
state.leftButtonImage = image?.foregroundColor(.clear) as? AnyView
}
state.hasRightButton = true
return self
}
@@ -187,14 +240,17 @@ public struct LCENavigationView<Content: View>: View {
}
state.leftButtonText = text
state.leftButtonAction = action
state.hasLeftButton = true
if let string = state.rightButtonText.string, string.isEmpty {
state.rightButtonText = text.foregroundColor(.clear)
}
if state.rightButtonImage == nil {
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
return self
}
/// Opts the navigation bar's buttons into the system Liquid Glass `.glass` button style on iOS 26+.
/// The navigation bar is designed as a plain, non-glass component by default; call this to enable glass explicitly.
/// - Parameter enabled: Whether buttons should use the glass style.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setGlassButtonsEnabled(_ enabled: Bool) -> LCENavigationView {
state.useGlassButtons = enabled
return self
}
@@ -229,101 +285,6 @@ public struct LCENavigationView<Content: View>: View {
}
}
/// Extension to `FormatStyle` to format any value as a string.
@available(iOS 15.0, *)
extension FormatStyle {
/// Formats an input value if it matches the `FormatInput` type.
/// - Parameter value: The value to format as `Any`.
/// - Returns: The formatted output, or `nil` if the value type does not match.
func format(any value: Any) -> FormatOutput? {
if let v = value as? FormatInput {
return format(v)
}
return nil
}
}
/// Extension to `LocalizedStringKey` to resolve localized strings.
@available(iOS 15.0, *)
extension LocalizedStringKey {
/// Resolves the localized string key into a `String`.
/// - Returns: The resolved string, or `nil` if resolution fails.
var resolved: String? {
let mirror = Mirror(reflecting: self)
guard let key = mirror.descendant("key") as? String else {
return nil
}
guard let args = mirror.descendant("arguments") as? [Any] else {
return nil
}
let values = args.map { arg -> Any? in
let mirror = Mirror(reflecting: arg)
if let value = mirror.descendant("storage", "value", ".0") {
return value
}
guard let format = mirror.descendant("storage", "formatStyleValue", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "formatStyleValue", "input") else {
return nil
}
return format.format(any: input)
}
let va = values.compactMap { arg -> CVarArg? in
switch arg {
case let i as Int: return i
case let i as Int64: return i
case let i as Int8: return i
case let i as Int16: return i
case let i as Int32: return i
case let u as UInt: return u
case let u as UInt64: return u
case let u as UInt8: return u
case let u as UInt16: return u
case let u as UInt32: return u
case let f as Float: return f
case let f as CGFloat: return f
case let d as Double: return d
case let o as NSObject: return o
default: return nil
}
}
if va.count != values.count {
return nil
}
return String.localizedStringWithFormat(key, va)
}
}
/// Extension to `Text` to retrieve its string content.
@available(iOS 15.0, *)
extension Text {
/// Returns the string representation of the `Text` view.
/// - Returns: The string content, or `nil` if it cannot be extracted.
var string: String? {
let mirror = Mirror(reflecting: self)
if let s = mirror.descendant("storage", "verbatim") as? String {
return s
} else if let attrStr = mirror.descendant("storage", "anyTextStorage", "str") as? AttributedString {
return String(attrStr.characters)
} else if let key = mirror.descendant("storage", "anyTextStorage", "key") as? LocalizedStringKey {
return key.resolved
} else if let format = mirror.descendant("storage", "anyTextStorage", "storage", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "anyTextStorage", "storage", "input") {
return format.format(any: input) as? String
} else if let formatter = mirror.descendant("storage", "anyTextStorage", "formatter") as? Formatter,
let object = mirror.descendant("storage", "anyTextStorage", "object") {
return formatter.string(for: object)
}
return nil
}
}
//@available(iOS 15.0, *)
//struct LCENavigationView_Previews: PreviewProvider {
// static var previews: some View {

View File

@@ -0,0 +1,39 @@
import Foundation
/// Pluggable auth for outgoing Feature Control requests. No `Atomenta-Token` type
/// exists here on purpose that module token must never be embedded in a
/// customer-facing app (see FC-060 §1 in Atomenta's `docs/feature-control/`).
/// A consumer who insists on it does so explicitly via `FeatureControlHeaderAuth`.
public protocol FeatureControlAuthorizing: Sendable {
func authorize(_ headers: inout [String: String]) async
}
/// For internal/admin apps hitting Atomenta directly with a panel-role JWT.
public struct FeatureControlBearerAuth: FeatureControlAuthorizing {
private let tokenProvider: @Sendable () async -> String?
public init(tokenProvider: @escaping @Sendable () async -> String?) {
self.tokenProvider = tokenProvider
}
public func authorize(_ headers: inout [String: String]) async {
guard let token = await tokenProvider() else { return }
headers["Authorization"] = "Bearer \(token)"
}
}
/// For a customer app calling its own BFF, which enforces its own auth. Adds
/// exactly the given headers never synthesizes an `Authorization` header.
public struct FeatureControlHeaderAuth: FeatureControlAuthorizing {
private let headers: [String: String]
public init(headers: [String: String]) {
self.headers = headers
}
public func authorize(_ headers: inout [String: String]) async {
for (key, value) in self.headers {
headers[key] = value
}
}
}

View File

@@ -0,0 +1,46 @@
import Foundation
/// Identifies a cached evaluation FC-060 §5: "environment + subjectId + keys(sorted) +
/// platform + appVersion". `sortedKeys` means caller `keys` order never affects hits.
struct FeatureControlCacheKey: Hashable, Sendable {
let environment: String
let subjectId: String
let sortedKeys: [String]
let platform: String?
let appVersion: String?
}
/// In-memory-only TTL cache (no disk persistence see SDD §2 Non-Goals). The clock is
/// injectable so tests can force expiry deterministically instead of sleeping.
actor FeatureControlCache {
private struct Entry {
let snapshot: FeatureControlSnapshot
let storedAt: Date
let ttl: TimeInterval
}
private var entries: [FeatureControlCacheKey: Entry] = [:]
private let now: @Sendable () -> Date
init(now: @escaping @Sendable () -> Date = { Date() }) {
self.now = now
}
/// `allowStale: true` returns an expired entry rather than `nil` used by the
/// safe-degrade fallback path (SDD §4.4/§4.5).
func get(_ key: FeatureControlCacheKey, allowStale: Bool) -> FeatureControlSnapshot? {
guard let entry = entries[key] else { return nil }
let age = now().timeIntervalSince(entry.storedAt)
if age <= entry.ttl { return entry.snapshot }
return allowStale ? entry.snapshot : nil
}
/// Always overwrites a fresh fetch's `configVersion` simply replaces whatever was there.
func set(_ key: FeatureControlCacheKey, snapshot: FeatureControlSnapshot, ttl: TimeInterval) {
entries[key] = Entry(snapshot: snapshot, storedAt: now(), ttl: ttl)
}
func invalidateAll() {
entries.removeAll()
}
}

View File

@@ -0,0 +1,33 @@
import Foundation
/// Wiring for one Atomenta Feature Control module deployment.
public struct FeatureControlConfiguration: Sendable {
public var baseURL: String
public var evaluatePath: String
public var notificationsPath: String
public var telemetryPath: String
public var environment: String
public var auth: any FeatureControlAuthorizing
/// In-memory cache TTL. FC-060 §5 recommends 3060s client-side.
public var cacheTTL: TimeInterval
/// FC-060 §5 recommends 1.53s at the BFF; same budget applies here.
public var requestTimeout: TimeInterval
public init(baseURL: String,
environment: String,
auth: any FeatureControlAuthorizing,
evaluatePath: String = "/api/feature-control/evaluate",
notificationsPath: String = "/api/feature-control/notifications",
telemetryPath: String = "/api/feature-control/telemetry/exposure",
cacheTTL: TimeInterval = 45,
requestTimeout: TimeInterval = 3) {
self.baseURL = baseURL
self.environment = environment
self.auth = auth
self.evaluatePath = evaluatePath
self.notificationsPath = notificationsPath
self.telemetryPath = telemetryPath
self.cacheTTL = cacheTTL
self.requestTimeout = requestTimeout
}
}

View File

@@ -0,0 +1,30 @@
import Foundation
/// Who/what a flag evaluation is for mirrors Atomenta's `context.subjectType` enum.
public enum FeatureControlSubjectType: String, Codable, Sendable {
case user, customer, store, anonymous
}
/// Evaluation context sent as `context` in `POST /api/feature-control/evaluate`.
public struct FeatureControlContext: Encodable, Sendable, Equatable {
public var subjectType: FeatureControlSubjectType
public var subjectId: String
public var storeId: String?
public var platform: String?
public var appVersion: String?
public var attributes: [String: String]?
public init(subjectType: FeatureControlSubjectType,
subjectId: String,
storeId: String? = nil,
platform: String? = nil,
appVersion: String? = nil,
attributes: [String: String]? = nil) {
self.subjectType = subjectType
self.subjectId = subjectId
self.storeId = storeId
self.platform = platform
self.appVersion = appVersion
self.attributes = attributes
}
}

View File

@@ -0,0 +1,18 @@
import Foundation
/// Parses the ISO-8601 timestamps Atomenta sends (`2026-04-16T12:00:00.000Z`, with
/// milliseconds). `LCEssentials.API`'s internal `JSONDecoder` uses the default
/// (`.deferredToDate`, numeric epoch) strategy, so `Date` fields on wire models
/// decode the raw string manually instead of relying on `Decodable`'s default
/// date handling.
enum FeatureControlDateParsing {
static func parse(_ string: String) -> Date? {
let withFractional = ISO8601DateFormatter()
withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = withFractional.date(from: string) { return date }
let plain = ISO8601DateFormatter()
plain.formatOptions = [.withInternetDateTime]
return plain.date(from: string)
}
}

View File

@@ -0,0 +1,51 @@
import Foundation
import LCEssentials
/// Explicit failure for callers that want to see it (`evaluateOrThrow`, the
/// notifications client). The safe `evaluate(...)` path never surfaces this
/// it degrades to cache/defaults instead (FC-060 §4).
public enum FeatureControlError: Error, Sendable, Equatable {
/// 400 `FEATURE_CONTROL_CONTEXT_INVALID`
case invalidContext(code: String)
/// 401 `MISSING_AUTH` / `INVALID_MODULE_TOKEN`
case unauthorized(code: String)
/// 403 `PANEL_ROLE_REQUIRED` / `INSUFFICIENT_PERMISSIONS`
case forbidden(code: String)
/// 429 `FEATURE_CONTROL_RATE_LIMIT`
case rateLimited(code: String)
/// Any other non-2xx status.
case server(code: String, status: Int)
/// Decoding failure, or any error not raised by `LCEssentials.API`'s HTTP path.
case transport(message: String)
}
enum FeatureControlErrorMapper {
/// `LCEssentials.API` throws an `NSError` (domain `LCEssentials.DEFAULT_ERROR_DOMAIN`,
/// `code` = HTTP status, `localizedFailureReason` = pretty-printed body) for non-2xx
/// responses, or a bridged `DecodingError`/`URLError` for anything else. Only the
/// former carries a real HTTP status to map.
static func map(_ error: Error) -> FeatureControlError {
let nsError = error as NSError
guard nsError.domain == LCEssentials.DEFAULT_ERROR_DOMAIN else {
return .transport(message: nsError.localizedDescription)
}
let status = nsError.code
let bodyCode = extractCode(from: nsError.localizedFailureReason) ?? "UNKNOWN"
switch status {
case 400: return .invalidContext(code: bodyCode)
case 401: return .unauthorized(code: bodyCode)
case 403: return .forbidden(code: bodyCode)
case 429: return .rateLimited(code: bodyCode)
default: return .server(code: bodyCode, status: status)
}
}
private static func extractCode(from prettyJSON: String?) -> String? {
guard let prettyJSON,
let data = prettyJSON.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let code = object["code"] as? String else { return nil }
return code
}
}

View File

@@ -0,0 +1,28 @@
import Foundation
/// One exposure event for `POST /api/feature-control/telemetry/exposure`.
public struct FeatureControlExposureEvent: Encodable, Sendable, Equatable {
public let featureKey: String
public let variant: String?
public let subjectType: FeatureControlSubjectType
public let storeId: String?
public init(featureKey: String, variant: String?, subjectType: FeatureControlSubjectType, storeId: String?) {
self.featureKey = featureKey
self.variant = variant
self.subjectType = subjectType
self.storeId = storeId
}
}
/// Request body server caps `events` at 100 (`maxItems: 100`); batching into that
/// limit is the caller's (`FeatureControlManager`'s) job, not this type's.
struct FeatureControlExposureBatchBody: Encodable, Sendable {
let events: [FeatureControlExposureEvent]
}
/// Wire envelope for the telemetry response `{error, code, result: {count}}`.
struct FeatureControlExposureBatchEnvelope: Decodable, Sendable {
let error: Bool
let code: String?
}

View File

@@ -0,0 +1,61 @@
import Foundation
/// One evaluated flag, as returned inside `EvaluateResponse.result.flags[key]`.
public struct FeatureControlFlag: Decodable, Sendable, Equatable {
public let enabled: Bool
public let variant: String?
public let payload: FeatureControlJSON?
public let reason: String?
public init(enabled: Bool, variant: String?, payload: FeatureControlJSON?, reason: String?) {
self.enabled = enabled
self.variant = variant
self.payload = payload
self.reason = reason
}
}
/// `EvaluateResponse.result` the batch evaluation result for a set of keys.
public struct FeatureControlSnapshot: Sendable, Equatable {
public let evaluatedAt: Date
public let configVersion: Int
public let flags: [String: FeatureControlFlag]
public init(evaluatedAt: Date, configVersion: Int, flags: [String: FeatureControlFlag]) {
self.evaluatedAt = evaluatedAt
self.configVersion = configVersion
self.flags = flags
}
}
extension FeatureControlSnapshot: Decodable {
private enum CodingKeys: String, CodingKey {
case evaluatedAt, configVersion, flags
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawDate = try container.decode(String.self, forKey: .evaluatedAt)
guard let date = FeatureControlDateParsing.parse(rawDate) else {
throw DecodingError.dataCorruptedError(forKey: .evaluatedAt, in: container,
debugDescription: "Unrecognized date format: \(rawDate)")
}
self.evaluatedAt = date
self.configVersion = try container.decode(Int.self, forKey: .configVersion)
self.flags = try container.decode([String: FeatureControlFlag].self, forKey: .flags)
}
}
/// Wire envelope for `POST /api/feature-control/evaluate` `{error, code, result}`.
struct FeatureControlEvaluateEnvelope: Decodable, Sendable {
let error: Bool
let code: String?
let result: FeatureControlSnapshot
}
/// Request body for `POST /api/feature-control/evaluate`.
struct FeatureControlEvaluateRequestBody: Encodable, Sendable {
let environment: String
let keys: [String]
let context: FeatureControlContext
}

View File

@@ -0,0 +1,33 @@
import Foundation
/// Minimal "any JSON" box for a flag's `payload`, which the backend declares as
/// `additionalProperties: true` (arbitrary shape). No force operations an
/// unrecognized shape throws a `DecodingError`, it never crashes.
public indirect enum FeatureControlJSON: Decodable, Sendable, Equatable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([FeatureControlJSON])
case object([String: FeatureControlJSON])
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([FeatureControlJSON].self) {
self = .array(value)
} else if let value = try? container.decode([String: FeatureControlJSON].self) {
self = .object(value)
} else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unsupported JSON value for FeatureControlJSON")
}
}
}

View File

@@ -0,0 +1,142 @@
import Foundation
import LCEssentials
/// Protocol seam for DI ViewModels/Interactors depend on this, never on the
/// concrete actor, so a mock can stand in for tests.
public protocol FeatureControlEvaluating: Sendable {
/// Never throws. Fresh fetch stale cache app-supplied `defaults`, in that
/// order (FC-060 §4 "nunca bloquear fluxo crítico"). Use this everywhere a
/// flag gates real product behaviour.
func evaluate(keys: [String], context: FeatureControlContext) async -> FeatureControlSnapshot
/// Same fetch, but surfaces the real failure. For admin/debug tooling only.
func evaluateOrThrow(keys: [String], context: FeatureControlContext) async throws -> FeatureControlSnapshot
/// Drops every cached entry. Call on user/store change, or app foreground if desired
/// this type does not observe app lifecycle itself (SDD §2 Non-Goals).
func invalidateCache() async
}
public actor FeatureControlManager: FeatureControlEvaluating {
private let configuration: FeatureControlConfiguration
private let api: API
private let defaults: [String: FeatureControlFlag]
private let cache: FeatureControlCache
private var exposureBuffer: [FeatureControlExposureEvent] = []
/// Coalesces concurrent cold-cache callers onto one in-flight request per key,
/// instead of firing one POST per caller (which was hammering the 429 limit).
private var inFlight: [FeatureControlCacheKey: Task<FeatureControlSnapshot, Error>] = [:]
/// `configVersion` sentinel returned when no network response and no cache exist
/// distinguishes "never evaluated" from any real server value (server versions are 0).
public static let unresolvedConfigVersion = -1
public init(configuration: FeatureControlConfiguration,
api: API = .shared,
defaults: [String: FeatureControlFlag] = [:],
now: @escaping @Sendable () -> Date = { Date() }) {
self.configuration = configuration
self.api = api
self.defaults = defaults
self.cache = FeatureControlCache(now: now)
}
public func evaluate(keys: [String], context: FeatureControlContext) async -> FeatureControlSnapshot {
do {
return try await evaluateOrThrow(keys: keys, context: context)
} catch {
let key = cacheKey(keys: keys, context: context)
if let stale = await cache.get(key, allowStale: true) { return stale }
return FeatureControlSnapshot(evaluatedAt: Date(),
configVersion: Self.unresolvedConfigVersion,
flags: defaults)
}
}
public func evaluateOrThrow(keys: [String], context: FeatureControlContext) async throws -> FeatureControlSnapshot {
let key = cacheKey(keys: keys, context: context)
if let fresh = await cache.get(key, allowStale: false) { return fresh }
if let inFlightTask = inFlight[key] {
return try await inFlightTask.value
}
let task = Task<FeatureControlSnapshot, Error> {
var headers: [String: String] = [:]
await self.configuration.auth.authorize(&headers)
let body = jsonBody(FeatureControlEvaluateRequestBody(
environment: self.configuration.environment, keys: keys, context: context))
do {
let envelope: FeatureControlEvaluateEnvelope = try await self.api.request(
url: self.configuration.baseURL + self.configuration.evaluatePath,
method: .post,
body: body,
headers: headers,
debug: false,
timeoutInterval: self.configuration.requestTimeout
)
guard !envelope.error else {
throw FeatureControlError.server(code: envelope.code ?? "UNKNOWN", status: 200)
}
await self.cache.set(key, snapshot: envelope.result, ttl: self.configuration.cacheTTL)
return envelope.result
} catch let error as FeatureControlError {
throw error
} catch {
throw FeatureControlErrorMapper.map(error)
}
}
inFlight[key] = task
defer { inFlight[key] = nil }
return try await task.value
}
public func invalidateCache() async {
await cache.invalidateAll()
}
private func cacheKey(keys: [String], context: FeatureControlContext) -> FeatureControlCacheKey {
FeatureControlCacheKey(environment: configuration.environment,
subjectId: context.subjectId,
sortedKeys: keys.sorted(),
platform: context.platform,
appVersion: context.appVersion)
}
}
// MARK: - Exposure telemetry
extension FeatureControlManager {
/// Buffers locally. Nothing is sent until `flushExposures()` is called.
public func recordExposure(_ event: FeatureControlExposureEvent) {
exposureBuffer.append(event)
}
/// Best-effort batches of 100 (server `maxItems: 100`); a failed batch is
/// dropped, never retried indefinitely (not a critical-path operation).
public func flushExposures() async {
guard !exposureBuffer.isEmpty else { return }
let events = exposureBuffer
exposureBuffer.removeAll()
for batch in events.lce_featureControlChunked(into: 100) {
var headers: [String: String] = [:]
await configuration.auth.authorize(&headers)
let body = jsonBody(FeatureControlExposureBatchBody(events: batch))
_ = try? await api.request(
url: configuration.baseURL + configuration.telemetryPath,
method: .post,
body: body,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
) as FeatureControlExposureBatchEnvelope
}
}
}
private extension Array {
func lce_featureControlChunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map { Array(self[$0..<Swift.min($0 + size, count)]) }
}
}

View File

@@ -0,0 +1,65 @@
import Foundation
/// `GET /api/feature-control/notifications` `status` query filter.
public enum FeatureControlNotificationStatus: String, Sendable {
case all, unread
}
/// One item from `NotificationListItem`.
public struct FeatureControlNotification: Sendable, Equatable, Identifiable {
public let id: String
public let title: String
public let body: String
public let severity: String
public let createdAt: Date
public let read: Bool
public let ctaLabel: String?
public let ctaUrl: String?
public init(id: String, title: String, body: String, severity: String,
createdAt: Date, read: Bool, ctaLabel: String?, ctaUrl: String?) {
self.id = id
self.title = title
self.body = body
self.severity = severity
self.createdAt = createdAt
self.read = read
self.ctaLabel = ctaLabel
self.ctaUrl = ctaUrl
}
}
extension FeatureControlNotification: Decodable {
private enum CodingKeys: String, CodingKey {
case id, title, body, severity, createdAt, read, ctaLabel, ctaUrl
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawDate = try container.decode(String.self, forKey: .createdAt)
guard let date = FeatureControlDateParsing.parse(rawDate) else {
throw DecodingError.dataCorruptedError(forKey: .createdAt, in: container,
debugDescription: "Unrecognized date format: \(rawDate)")
}
self.id = try container.decode(String.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.body = try container.decode(String.self, forKey: .body)
self.severity = try container.decode(String.self, forKey: .severity)
self.createdAt = date
self.read = try container.decode(Bool.self, forKey: .read)
self.ctaLabel = try container.decodeIfPresent(String.self, forKey: .ctaLabel)
self.ctaUrl = try container.decodeIfPresent(String.self, forKey: .ctaUrl)
}
}
/// `GET /api/feature-control/notifications` `result` shape.
struct FeatureControlNotificationListResult: Decodable, Sendable {
let items: [FeatureControlNotification]
let nextCursor: String?
}
/// Wire envelope for the notifications list `{error, result}`.
struct FeatureControlNotificationListEnvelope: Decodable, Sendable {
let error: Bool
let result: FeatureControlNotificationListResult
}

View File

@@ -0,0 +1,87 @@
import Foundation
import LCEssentials
/// Protocol seam for DI, mirroring `FeatureControlEvaluating`.
public protocol FeatureControlNotifying: Sendable {
func list(status: FeatureControlNotificationStatus, limit: Int, cursor: String?) async throws
-> (items: [FeatureControlNotification], nextCursor: String?)
func markRead(id: String) async throws
func markAllRead() async throws
}
/// JWT-only the OpenAPI fragment declares `security: [bearerAuth]` for every
/// notifications route, no module-token alternative. Configure `configuration.auth`
/// with `FeatureControlBearerAuth`; anything else gets a `401` from the server,
/// surfaced as `FeatureControlError.unauthorized`.
public actor FeatureControlNotificationsClient: FeatureControlNotifying {
private let configuration: FeatureControlConfiguration
private let api: API
public init(configuration: FeatureControlConfiguration, api: API = .shared) {
self.configuration = configuration
self.api = api
}
public func list(status: FeatureControlNotificationStatus = .all, limit: Int = 20, cursor: String? = nil) async throws
-> (items: [FeatureControlNotification], nextCursor: String?) {
var headers: [String: String] = [:]
await configuration.auth.authorize(&headers)
var query = "?status=\(status.rawValue)&limit=\(limit)"
if let cursor, let encodedCursor = cursor.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
query += "&cursor=\(encodedCursor)"
}
do {
let envelope: FeatureControlNotificationListEnvelope = try await api.request(
url: configuration.baseURL + configuration.notificationsPath + query,
method: .get,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
guard !envelope.error else {
throw FeatureControlError.server(code: "UNKNOWN", status: 200)
}
return (envelope.result.items, envelope.result.nextCursor)
} catch let error as FeatureControlError {
throw error
} catch {
throw FeatureControlErrorMapper.map(error)
}
}
public func markRead(id: String) async throws {
var headers: [String: String] = [:]
await configuration.auth.authorize(&headers)
let encodedId = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
do {
let _: String = try await api.request(
url: configuration.baseURL + configuration.notificationsPath + "/\(encodedId)/read",
method: .post,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
} catch {
throw FeatureControlErrorMapper.map(error)
}
}
public func markAllRead() async throws {
var headers: [String: String] = [:]
await configuration.auth.authorize(&headers)
do {
let _: String = try await api.request(
url: configuration.baseURL + configuration.notificationsPath + "/read-all",
method: .post,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
} catch {
throw FeatureControlErrorMapper.map(error)
}
}
}

View File

@@ -0,0 +1,38 @@
import XCTest
@testable import LCFeatureControl
final class FeatureControlAuthTests: XCTestCase {
func testBearerAuthAddsAuthorizationHeader() async {
let auth = FeatureControlBearerAuth { "jwt-123" }
var headers: [String: String] = [:]
await auth.authorize(&headers)
XCTAssertEqual(headers["Authorization"], "Bearer jwt-123")
}
func testBearerAuthAddsNoHeaderWhenTokenIsNil() async {
let auth = FeatureControlBearerAuth { nil }
var headers: [String: String] = [:]
await auth.authorize(&headers)
XCTAssertNil(headers["Authorization"])
}
func testHeaderAuthAddsExactHeadersOnly() async {
let auth = FeatureControlHeaderAuth(headers: ["X-BFF-Session": "abc"])
var headers: [String: String] = [:]
await auth.authorize(&headers)
XCTAssertEqual(headers, ["X-BFF-Session": "abc"])
XCTAssertNil(headers["Authorization"])
}
func testConfigurationDefaults() {
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlHeaderAuth(headers: [:]))
XCTAssertEqual(config.cacheTTL, 45)
XCTAssertEqual(config.requestTimeout, 3)
XCTAssertEqual(config.evaluatePath, "/api/feature-control/evaluate")
XCTAssertEqual(config.notificationsPath, "/api/feature-control/notifications")
XCTAssertEqual(config.telemetryPath, "/api/feature-control/telemetry/exposure")
}
}

View File

@@ -0,0 +1,70 @@
import XCTest
@testable import LCFeatureControl
/// Test-only mutable clock box a `var` captured directly by the cache's
/// escaping `@Sendable` closure would trip strict-concurrency capture checks;
/// this mirrors the `@unchecked Sendable` pattern `StubURLProtocol` already uses.
private final class MutableClock: @unchecked Sendable {
var value: Date
init(_ value: Date) { self.value = value }
}
final class FeatureControlCacheTests: XCTestCase {
private func snapshot(configVersion: Int) -> FeatureControlSnapshot {
FeatureControlSnapshot(evaluatedAt: Date(), configVersion: configVersion, flags: [:])
}
private func key(_ keys: [String] = ["a", "b"]) -> FeatureControlCacheKey {
FeatureControlCacheKey(environment: "production", subjectId: "cust_1",
sortedKeys: keys.sorted(), platform: "ios", appVersion: "1.0")
}
func testHitWithinTTLReturnsSameSnapshot() async {
let cache = FeatureControlCache(now: { Date() })
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45)
let hit = await cache.get(key(), allowStale: false)
XCTAssertEqual(hit?.configVersion, 1)
}
func testMissAfterTTLExpiryReturnsNilWhenStaleNotAllowed() async {
let clock = MutableClock(Date())
let cache = FeatureControlCache(now: { clock.value })
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1)
clock.value = clock.value.addingTimeInterval(2)
let hit = await cache.get(key(), allowStale: false)
XCTAssertNil(hit)
}
func testStaleAllowedReturnsExpiredEntry() async {
let clock = MutableClock(Date())
let cache = FeatureControlCache(now: { clock.value })
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1)
clock.value = clock.value.addingTimeInterval(2)
let hit = await cache.get(key(), allowStale: true)
XCTAssertEqual(hit?.configVersion, 1)
}
func testKeyDiffersBySortedKeysNotInputOrder() async {
let cache = FeatureControlCache(now: { Date() })
await cache.set(key(["a", "b"]), snapshot: snapshot(configVersion: 1), ttl: 45)
let hit = await cache.get(key(["b", "a"]), allowStale: false)
XCTAssertEqual(hit?.configVersion, 1)
}
func testSetOverwritesPriorConfigVersion() async {
let cache = FeatureControlCache(now: { Date() })
await cache.set(key(), snapshot: snapshot(configVersion: 7), ttl: 45)
await cache.set(key(), snapshot: snapshot(configVersion: 8), ttl: 45)
let hit = await cache.get(key(), allowStale: false)
XCTAssertEqual(hit?.configVersion, 8)
}
func testInvalidateAllClearsEntries() async {
let cache = FeatureControlCache(now: { Date() })
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45)
await cache.invalidateAll()
let hit = await cache.get(key(), allowStale: true)
XCTAssertNil(hit)
}
}

View File

@@ -0,0 +1,76 @@
import XCTest
@testable import LCFeatureControl
@testable import LCEssentials
final class FeatureControlErrorTests: XCTestCase {
private var api = API.lce_featureControlTestInstance()
override func tearDown() {
StubURLProtocol.reset()
super.tearDown()
}
private func makeManager() -> FeatureControlManager {
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlHeaderAuth(headers: [:]))
return FeatureControlManager(configuration: config, api: api)
}
private func context() -> FeatureControlContext {
FeatureControlContext(subjectType: .customer, subjectId: "cust_1")
}
private func expectMappedError(status: Int, bodyCode: String) async throws -> FeatureControlError {
StubURLProtocol.setStub(.init(statusCode: status, body: Data(#"{"code":"\#(bodyCode)"}"#.utf8)))
let manager = makeManager()
do {
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
XCTFail("expected FeatureControlError")
throw FeatureControlError.transport(message: "unreachable")
} catch let error as FeatureControlError {
return error
}
}
func testMaps400ToInvalidContext() async throws {
let error = try await expectMappedError(status: 400, bodyCode: "FEATURE_CONTROL_CONTEXT_INVALID")
XCTAssertEqual(error, .invalidContext(code: "FEATURE_CONTROL_CONTEXT_INVALID"))
}
func testMaps401ToUnauthorized() async throws {
let error = try await expectMappedError(status: 401, bodyCode: "FEATURE_CONTROL_MISSING_AUTH")
XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH"))
}
func testMaps403ToForbidden() async throws {
let error = try await expectMappedError(status: 403, bodyCode: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED")
XCTAssertEqual(error, .forbidden(code: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED"))
}
func testMaps429ToRateLimited() async throws {
let error = try await expectMappedError(status: 429, bodyCode: "FEATURE_CONTROL_RATE_LIMIT")
XCTAssertEqual(error, .rateLimited(code: "FEATURE_CONTROL_RATE_LIMIT"))
}
func testMaps500ToServerWithStatusCode() async throws {
let error = try await expectMappedError(status: 500, bodyCode: "FEATURE_CONTROL_ERROR")
XCTAssertEqual(error, .server(code: "FEATURE_CONTROL_ERROR", status: 500))
}
func testDecodingFailureMapsToTransport() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data("not json at all".utf8)))
let manager = makeManager()
do {
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
XCTFail("expected throw")
} catch let error as FeatureControlError {
guard case .transport = error else {
return XCTFail("expected .transport, got \(error)")
}
} catch {
XCTFail("expected FeatureControlError, got \(error)")
}
}
}

View File

@@ -0,0 +1,75 @@
import XCTest
@testable import LCFeatureControl
@testable import LCEssentials
final class FeatureControlExposureTests: XCTestCase {
private var api = API.lce_featureControlTestInstance()
override func tearDown() {
StubURLProtocol.reset()
super.tearDown()
}
private func makeManager() -> FeatureControlManager {
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlHeaderAuth(headers: [:]))
return FeatureControlManager(configuration: config, api: api)
}
private func event(_ index: Int) -> FeatureControlExposureEvent {
FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on",
subjectType: .customer, storeId: "store_\(index)")
}
func testFlushSendsSingleBatchUnderLimit() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8)))
let manager = makeManager()
for i in 0..<10 { await manager.recordExposure(event(i)) }
await manager.flushExposures()
XCTAssertEqual(StubURLProtocol.requestCount, 1)
}
func testFlushSplitsIntoMultipleBatchesOverLimit() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8)))
let manager = makeManager()
for i in 0..<130 { await manager.recordExposure(event(i)) }
await manager.flushExposures()
XCTAssertEqual(StubURLProtocol.requestCount, 2)
}
func testFlushSplitsExactlyAtTheServerBatchLimit() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8)))
let manager = makeManager()
for i in 0..<101 { await manager.recordExposure(event(i)) } // maxItems: 100, plus 1
await manager.flushExposures()
XCTAssertEqual(StubURLProtocol.requestCount, 2)
let eventCountsPerBatch = StubURLProtocol.capturedBodies.map { body in
String(decoding: body, as: UTF8.self).components(separatedBy: "\"featureKey\"").count - 1
}
XCTAssertEqual(eventCountsPerBatch, [100, 1])
}
func testFailedBatchIsDroppedNotRetriedIndefinitely() async {
StubURLProtocol.setStub(.init(statusCode: 500, body: Data()))
let manager = makeManager()
for i in 0..<10 { await manager.recordExposure(event(i)) }
await manager.flushExposures() // must not throw, must not hang
XCTAssertEqual(StubURLProtocol.requestCount, 1)
// buffer was cleared even though the batch failed a second flush sends nothing new
StubURLProtocol.reset()
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8)))
await manager.flushExposures()
XCTAssertEqual(StubURLProtocol.requestCount, 0)
}
}

View File

@@ -0,0 +1,40 @@
import XCTest
@testable import LCFeatureControl
final class FeatureControlJSONTests: XCTestCase {
private func decode(_ json: String) throws -> FeatureControlJSON {
try JSONDecoder().decode(FeatureControlJSON.self, from: Data(json.utf8))
}
func testDecodesNull() throws {
XCTAssertEqual(try decode("null"), .null)
}
func testDecodesBool() throws {
XCTAssertEqual(try decode("true"), .bool(true))
}
func testDecodesNumber() throws {
XCTAssertEqual(try decode("5"), .number(5))
}
func testDecodesString() throws {
XCTAssertEqual(try decode("\"hello\""), .string("hello"))
}
func testDecodesArray() throws {
XCTAssertEqual(try decode("[\"a\",\"b\"]"), .array([.string("a"), .string("b")]))
}
func testDecodesNestedObject() throws {
let json = #"{"limit": 5, "tags": ["a","b"], "nested": {"x": true}}"#
let value = try decode(json)
guard case let .object(object) = value else {
return XCTFail("expected .object, got \(value)")
}
XCTAssertEqual(object["limit"], .number(5))
XCTAssertEqual(object["tags"], .array([.string("a"), .string("b")]))
XCTAssertEqual(object["nested"], .object(["x": .bool(true)]))
}
}

View File

@@ -0,0 +1,187 @@
import XCTest
@testable import LCFeatureControl
@testable import LCEssentials
private final class MutableClock: @unchecked Sendable {
var value: Date
init(_ value: Date) { self.value = value }
}
final class FeatureControlManagerTests: XCTestCase {
private var api = API.lce_featureControlTestInstance()
override func tearDown() {
StubURLProtocol.reset()
super.tearDown()
}
private func makeManager(defaults: [String: FeatureControlFlag] = [:],
now: @escaping @Sendable () -> Date = { Date() }) -> FeatureControlManager {
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlHeaderAuth(headers: [:]))
return FeatureControlManager(configuration: config, api: api, defaults: defaults, now: now)
}
private func evaluateResponseBody(configVersion: Int) -> Data {
Data("""
{"error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": {
"evaluatedAt": "2026-04-16T12:00:00.000Z",
"configVersion": \(configVersion),
"flags": {"fc.checkout_v2": {"enabled": true, "variant": "on", "payload": null, "reason": "rollout"}}
}}
""".utf8)
}
private func context() -> FeatureControlContext {
FeatureControlContext(subjectType: .customer, subjectId: "cust_1")
}
// MARK: - Happy path (T4)
func testEvaluateFetchesAndCachesOnMiss() async throws {
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager()
let snapshot = try await manager.evaluateOrThrow(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(snapshot.configVersion, 7)
XCTAssertEqual(StubURLProtocol.requestCount, 1)
let sent = StubURLProtocol.capturedRequests.first
XCTAssertEqual(sent?.httpMethod, "POST")
XCTAssertEqual(sent?.url?.absoluteString, "https://api.example.com/api/feature-control/evaluate")
}
func testEvaluateReturnsCacheWithoutNetworkCallOnHit() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager()
_ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
_ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(StubURLProtocol.requestCount, 1)
}
func testConfigVersionBumpReplacesCacheAfterExpiry() async {
let clock = MutableClock(Date())
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager(now: { clock.value })
let first = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(first.configVersion, 7)
clock.value = clock.value.addingTimeInterval(1000) // past default 45s TTL
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 8)))
let second = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(second.configVersion, 8)
XCTAssertEqual(StubURLProtocol.requestCount, 1)
}
func testInvalidateCacheForcesFreshFetch() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager()
_ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
await manager.invalidateCache()
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 9)))
let second = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(second.configVersion, 9)
XCTAssertEqual(StubURLProtocol.requestCount, 1)
}
func testEvaluateOrThrowPropagatesDecodingFailure() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data("not json".utf8)))
let manager = makeManager()
do {
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
XCTFail("expected throw")
} catch {
// any throw is correct evaluateOrThrow must not silently degrade
}
}
// MARK: - Safe-degrade fallback chain (T5)
func testEvaluate429FallsBackToStaleCache() async {
let clock = MutableClock(Date())
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager(now: { clock.value })
_ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
clock.value = clock.value.addingTimeInterval(1000)
StubURLProtocol.setStub(.init(statusCode: 429, body: Data(#"{"code":"FEATURE_CONTROL_RATE_LIMIT"}"#.utf8)))
let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(result.configVersion, 7)
}
func testEvaluate500FallsBackToStaleCache() async {
let clock = MutableClock(Date())
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager(now: { clock.value })
_ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
clock.value = clock.value.addingTimeInterval(1000)
StubURLProtocol.setStub(.init(statusCode: 500, body: Data()))
let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(result.configVersion, 7)
}
func testEvaluateNoCacheAndServerDownReturnsDefaults() async {
StubURLProtocol.setStub(.init(statusCode: 500, body: Data()))
let defaultFlag = FeatureControlFlag(enabled: false, variant: nil, payload: nil, reason: "default")
let manager = makeManager(defaults: ["fc.checkout_v2": defaultFlag])
let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context())
XCTAssertEqual(result.configVersion, FeatureControlManager.unresolvedConfigVersion)
XCTAssertEqual(result.flags["fc.checkout_v2"]?.enabled, false)
}
func testEvaluateNeverThrowsEvenWhenServerAlwaysErrors() async {
StubURLProtocol.setStub(.init(statusCode: 500, body: Data()))
let manager = makeManager()
// No `try` above this line compiles only because `evaluate` truly never throws.
_ = await manager.evaluate(keys: ["x"], context: context())
}
// MARK: - Concurrency & envelope validation
func testConcurrentEvaluateOnColdCacheCoalescesIntoSingleRequest() async {
StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7)))
let manager = makeManager()
let ctx = context()
async let first = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx)
async let second = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx)
async let third = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx)
let results = await [first, second, third]
XCTAssertEqual(Set(results.map(\.configVersion)), [7])
XCTAssertEqual(StubURLProtocol.requestCount, 1)
}
func testEvaluateOrThrowThrowsWhenEnvelopeErrorIsTrueDespite200() async {
let body = """
{"error": true, "code": "FEATURE_CONTROL_DEGRADED", "result": {
"evaluatedAt": "2026-04-16T12:00:00.000Z", "configVersion": 1, "flags": {}
}}
"""
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let manager = makeManager()
do {
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
XCTFail("expected throw")
} catch let error as FeatureControlError {
XCTAssertEqual(error, .server(code: "FEATURE_CONTROL_DEGRADED", status: 200))
} catch {
XCTFail("expected FeatureControlError, got \(error)")
}
}
}

View File

@@ -0,0 +1,164 @@
import XCTest
@testable import LCFeatureControl
/// Exact evaluate-response shape from Atomenta's `docs/feature-control/EXTERNAL_CLIENTS.md` §3.
private let evaluateResponseFixture = """
{
"error": false,
"code": "FEATURE_CONTROL_EVALUATED",
"result": {
"evaluatedAt": "2026-04-16T12:00:00.000Z",
"configVersion": 7,
"flags": {
"fc.checkout_v2": {
"enabled": true,
"variant": "on",
"payload": null,
"reason": "rollout"
},
"fc.search_ranking_v3": {
"enabled": false,
"variant": "off",
"payload": null,
"reason": "default"
}
}
}
}
"""
final class FeatureControlModelsTests: XCTestCase {
func testFlagDecodesFromEvaluateResponseFixture() throws {
let data = Data(evaluateResponseFixture.utf8)
let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data)
XCTAssertFalse(envelope.error)
XCTAssertEqual(envelope.result.configVersion, 7)
let checkout = try XCTUnwrap(envelope.result.flags["fc.checkout_v2"])
XCTAssertTrue(checkout.enabled)
XCTAssertEqual(checkout.variant, "on")
XCTAssertNil(checkout.payload)
XCTAssertEqual(checkout.reason, "rollout")
let ranking = try XCTUnwrap(envelope.result.flags["fc.search_ranking_v3"])
XCTAssertFalse(ranking.enabled)
}
func testEvaluatedAtParsesFractionalSecondsISO8601() throws {
let data = Data(evaluateResponseFixture.utf8)
let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data)
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try XCTUnwrap(TimeZone(identifier: "UTC"))
let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second],
from: envelope.result.evaluatedAt)
XCTAssertEqual(components.year, 2026)
XCTAssertEqual(components.month, 4)
XCTAssertEqual(components.day, 16)
XCTAssertEqual(components.hour, 12)
}
func testEvaluatedAtDecodeFailsOnUnrecognizedDateFormat() {
let json = #"{"evaluatedAt": "not-a-date", "configVersion": 1, "flags": {}}"#
XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlSnapshot.self, from: Data(json.utf8))) { error in
guard case DecodingError.dataCorrupted = error else {
return XCTFail("expected .dataCorrupted, got \(error)")
}
}
}
func testFlagDecodesObjectPayloadThroughEvaluateEnvelope() throws {
let json = """
{"error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": {
"evaluatedAt": "2026-04-16T12:00:00.000Z",
"configVersion": 3,
"flags": {"fc.promo_banner": {"enabled": true, "variant": "on",
"payload": {"title": "Sale", "limit": 5, "tags": ["a", "b"]}, "reason": "rollout"}}
}}
"""
let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: Data(json.utf8))
let flag = try XCTUnwrap(envelope.result.flags["fc.promo_banner"])
guard case let .object(payload) = flag.payload else {
return XCTFail("expected .object payload, got \(String(describing: flag.payload))")
}
XCTAssertEqual(payload["title"], .string("Sale"))
XCTAssertEqual(payload["limit"], .number(5))
XCTAssertEqual(payload["tags"], .array([.string("a"), .string("b")]))
}
func testContextEncodesExactOpenAPIShape() throws {
let context = FeatureControlContext(subjectType: .customer, subjectId: "cust_123",
storeId: "store_001", platform: "ios",
appVersion: "2.3.1",
attributes: ["city": "Belo Horizonte"])
let data = try JSONEncoder().encode(context)
let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any])
XCTAssertEqual(object["subjectType"] as? String, "customer")
XCTAssertEqual(object["subjectId"] as? String, "cust_123")
XCTAssertEqual(object["storeId"] as? String, "store_001")
XCTAssertEqual(object["platform"] as? String, "ios")
XCTAssertEqual(object["appVersion"] as? String, "2.3.1")
let attributes = try XCTUnwrap(object["attributes"] as? [String: String])
XCTAssertEqual(attributes["city"], "Belo Horizonte")
}
func testNotificationDecodesFromOpenAPIShape() throws {
let json = """
{
"id": "n1",
"title": "Manutenção",
"body": "Janela de manutenção às 22h",
"severity": "warning",
"createdAt": "2026-04-16T12:00:00.000Z",
"read": false,
"ctaLabel": "Ver detalhes",
"ctaUrl": "https://example.com"
}
"""
let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8))
XCTAssertEqual(notification.id, "n1")
XCTAssertEqual(notification.severity, "warning")
XCTAssertFalse(notification.read)
XCTAssertEqual(notification.ctaLabel, "Ver detalhes")
}
func testNotificationDecodesWithoutOptionalCTAFields() throws {
let json = """
{
"id": "n2",
"title": "Info",
"body": "Just FYI",
"severity": "info",
"createdAt": "2026-04-16T12:00:00Z",
"read": true
}
"""
let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8))
XCTAssertNil(notification.ctaLabel)
XCTAssertNil(notification.ctaUrl)
}
func testNotificationCreatedAtDecodeFailsOnUnrecognizedDateFormat() {
let json = """
{"id": "n3", "title": "T", "body": "B", "severity": "info",
"createdAt": "not-a-date", "read": false}
"""
XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8))) { error in
guard case DecodingError.dataCorrupted = error else {
return XCTFail("expected .dataCorrupted, got \(error)")
}
}
}
func testExposureEventEncodesExpectedShape() throws {
let event = FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on",
subjectType: .customer, storeId: "store_001")
let data = try JSONEncoder().encode(event)
let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any])
XCTAssertEqual(object["featureKey"] as? String, "fc.checkout_v2")
XCTAssertEqual(object["subjectType"] as? String, "customer")
}
}

View File

@@ -0,0 +1,122 @@
import XCTest
@testable import LCFeatureControl
@testable import LCEssentials
final class FeatureControlNotificationsClientTests: XCTestCase {
private var api = API.lce_featureControlTestInstance()
override func tearDown() {
StubURLProtocol.reset()
super.tearDown()
}
private func makeClient() -> FeatureControlNotificationsClient {
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
environment: "production",
auth: FeatureControlBearerAuth { "jwt-123" })
return FeatureControlNotificationsClient(configuration: config, api: api)
}
func testListDecodesItemsAndNextCursor() async throws {
let body = """
{"error": false, "result": {"items": [
{"id": "n1", "title": "T", "body": "B", "severity": "info",
"createdAt": "2026-04-16T12:00:00.000Z", "read": false}
], "nextCursor": "cursor-2"}}
"""
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let client = makeClient()
let (items, nextCursor) = try await client.list(status: .unread, limit: 20)
XCTAssertEqual(items.count, 1)
XCTAssertEqual(items.first?.id, "n1")
XCTAssertEqual(nextCursor, "cursor-2")
}
func testListPassesStatusAndLimitAsQueryParams() async throws {
let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"#
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let client = makeClient()
_ = try await client.list(status: .unread, limit: 5)
let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString
XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=unread&limit=5")
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.value(forHTTPHeaderField: "Authorization"),
"Bearer jwt-123")
}
func testMarkReadPostsToCorrectPathAndSucceedsOn200() async throws {
StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8)))
let client = makeClient()
try await client.markRead(id: "n1")
let sent = StubURLProtocol.capturedRequests.first
XCTAssertEqual(sent?.httpMethod, "POST")
XCTAssertEqual(sent?.url?.absoluteString, "https://api.example.com/api/feature-control/notifications/n1/read")
}
func testMarkAllReadSucceedsOn200() async throws {
StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8)))
let client = makeClient()
try await client.markAllRead()
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.url?.absoluteString,
"https://api.example.com/api/feature-control/notifications/read-all")
}
func testListIncludesEncodedCursorWhenProvided() async throws {
let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"#
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let client = makeClient()
_ = try await client.list(status: .all, limit: 20, cursor: "cursor abc")
let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString
XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20&cursor=cursor%20abc")
}
func testListOmitsCursorWhenNil() async throws {
let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"#
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let client = makeClient()
_ = try await client.list(status: .all, limit: 20)
let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString
XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20")
}
func testListThrowsWhenEnvelopeErrorIsTrueDespite200() async {
let body = #"{"error": true, "result": {"items": [], "nextCursor": null}}"#
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
let client = makeClient()
do {
_ = try await client.list(status: .all, limit: 20)
XCTFail("expected throw")
} catch let error as FeatureControlError {
XCTAssertEqual(error, .server(code: "UNKNOWN", status: 200))
} catch {
XCTFail("expected FeatureControlError, got \(error)")
}
}
func testUnauthorizedMapsToFeatureControlErrorNotCrash() async {
StubURLProtocol.setStub(.init(statusCode: 401, body: Data(#"{"code":"FEATURE_CONTROL_MISSING_AUTH"}"#.utf8)))
let client = makeClient()
do {
try await client.markAllRead()
XCTFail("expected throw")
} catch let error as FeatureControlError {
XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH"))
} catch {
XCTFail("expected FeatureControlError, got \(error)")
}
}
}

View File

@@ -0,0 +1,137 @@
import Foundation
@testable import LCEssentials
/// Shared factory so test classes don't each declare a force-unwrapped `API!`
/// var for the XCTest setUp/tearDown lifecycle.
extension API {
static func lce_featureControlTestInstance() -> API {
let cfg = URLSessionConfiguration.ephemeral
cfg.protocolClasses = [StubURLProtocol.self]
return API(testConfiguration: cfg)
}
}
/// Test double for `URLProtocol`. Intercepts every request on a `URLSession`
/// configured with it, records the outgoing `URLRequest`, and replays a canned
/// response or error supplied by the test.
///
/// Register via:
/// ```
/// let cfg = URLSessionConfiguration.ephemeral
/// cfg.protocolClasses = [StubURLProtocol.self]
/// let session = URLSession(configuration: cfg)
/// ```
final class StubURLProtocol: URLProtocol, @unchecked Sendable {
struct Stub {
var statusCode: Int = 200
var headers: [String: String] = ["Content-Type": "application/json"]
var body: Data = Data()
var error: Error?
/// Bytes reported through `URLSession`'s upload progress, in order.
var uploadProgressChunks: [Int] = []
}
// MARK: - Test-facing state (guarded)
private static let lock = NSLock()
// Access is serialised through `lock`; the unsafe opt-out is the documented
// pattern for lock-guarded mutable statics under strict concurrency.
nonisolated(unsafe) private static var _stub = Stub()
nonisolated(unsafe) private static var _capturedRequests: [URLRequest] = []
nonisolated(unsafe) private static var _capturedBodies: [Data] = []
static func setStub(_ stub: Stub) {
lock.lock(); defer { lock.unlock() }
_stub = stub
_capturedRequests = []
_capturedBodies = []
}
static func reset() { setStub(Stub()) }
static var capturedRequests: [URLRequest] {
lock.lock(); defer { lock.unlock() }
return _capturedRequests
}
/// Body of the last intercepted request. `URLProtocol` strips `httpBody` for
/// stream bodies, so this reads `httpBodyStream` when needed.
static var lastCapturedBody: Data? {
lock.lock(); defer { lock.unlock() }
return _capturedBodies.last
}
static var requestCount: Int {
lock.lock(); defer { lock.unlock() }
return _capturedRequests.count
}
/// Bodies of every intercepted request, in order for tests asserting batch
/// boundaries across multiple requests (`lastCapturedBody` only sees the last one).
static var capturedBodies: [Data] {
lock.lock(); defer { lock.unlock() }
return _capturedBodies
}
private static func currentStub() -> Stub {
lock.lock(); defer { lock.unlock() }
return _stub
}
private static func record(_ request: URLRequest, body: Data) {
lock.lock(); defer { lock.unlock() }
_capturedRequests.append(request)
_capturedBodies.append(body)
}
// MARK: - URLProtocol
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
let stub = Self.currentStub()
Self.record(request, body: Self.bodyData(from: request))
guard let client = client else { return }
if let error = stub.error {
client.urlProtocol(self, didFailWithError: error)
return
}
guard let url = request.url ?? URL(string: "https://stub.invalid"),
let response = HTTPURLResponse(url: url,
statusCode: stub.statusCode,
httpVersion: "HTTP/1.1",
headerFields: stub.headers) else {
client.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client.urlProtocol(self, didLoad: stub.body)
client.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
// MARK: - Body extraction
private static func bodyData(from request: URLRequest) -> Data {
if let body = request.httpBody { return body }
guard let stream = request.httpBodyStream else { return Data() }
stream.open()
defer { stream.close() }
var data = Data()
let bufferSize = 64 * 1024
var buffer = [UInt8](repeating: 0, count: bufferSize)
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: bufferSize)
if read <= 0 { break }
data.append(buffer, count: read)
}
return data
}
}