Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1f370f7db | |||
|
|
c6e9803c75 | ||
|
|
2ba487a6e8 | ||
|
|
d067791930 | ||
| 1d5067212a | |||
|
|
72a5b59c44 | ||
|
|
d678965154 | ||
|
|
0cceda1ad9 | ||
|
|
2cd52d3d12 | ||
|
|
1743bc2d24 | ||
|
|
872d70dc21 | ||
|
|
6eb57f5c44 | ||
|
|
ac9d010b8e | ||
|
|
255e5cfe1a | ||
|
|
f57dc50975 | ||
|
|
971278fa3c | ||
|
|
ec3442ed1c | ||
|
|
4f6a83556d | ||
|
|
d16a42fddc | ||
|
|
c459c9eaf6 | ||
|
|
1d2c595c78 | ||
|
|
9f1db7c121 | ||
|
|
705639b2f5 |
@@ -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
2031
Documentation/Extensions.md
Normal file
File diff suppressed because it is too large
Load Diff
175
Documentation/FeatureControl.md
Normal file
175
Documentation/FeatureControl.md
Normal 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
117
Documentation/SwiftUI.md
Normal 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
815
Documentation/UIKit.md
Normal 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.0…1.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>
|
||||
44
Frameworks/LCECryptoKit.xcframework/Info.plist
Normal file
44
Frameworks/LCECryptoKit.xcframework/Info.plist
Normal 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>
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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": []
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
Binary file not shown.
@@ -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?
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
framework module LCECryptoKit {
|
||||
header "LCECryptoKit-Swift.h"
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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": []
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
Binary file not shown.
@@ -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?
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
Binary file not shown.
@@ -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?
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
framework module LCECryptoKit {
|
||||
header "LCECryptoKit-Swift.h"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]),
|
||||
]
|
||||
)
|
||||
|
||||
85
README.md
85
README.md
@@ -1,85 +1,44 @@
|
||||

|
||||
|
||||

|
||||
Loverde Co. Essentials Swift Scripts
|
||||
----
|
||||
# Loverde Co. Essentials
|
||||
|
||||
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!
|
||||
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 |
|
||||
|
||||
[](https://github.com/loverde-co/resume/)
|
||||
[](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/)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2025 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -18,43 +18,41 @@
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// 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 {
|
||||
|
||||
|
||||
private let hashKey: String
|
||||
|
||||
|
||||
public init() {
|
||||
self.hashKey = ""
|
||||
}
|
||||
|
||||
public init(privateKey: String){
|
||||
|
||||
public init(privateKey: String) {
|
||||
self.hashKey = privateKey
|
||||
}
|
||||
|
||||
|
||||
public static func generateKey() -> String {
|
||||
LCECryptoKit.generateRandomAESKeyString()
|
||||
}
|
||||
|
||||
|
||||
public func encodeTP(email: String, password: String) -> String? {
|
||||
return LCECryptoKit.encodeSeed(email: email, password: password)
|
||||
}
|
||||
|
||||
|
||||
public func decodeOTP(_ otpHash: String) -> String? {
|
||||
return LCECryptoKit.decodeSeed(otpKey: otpHash)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Need hashKey to decode
|
||||
|
||||
|
||||
public func encodeOTPWithKey(email: String, password: String) -> String? {
|
||||
return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey)
|
||||
}
|
||||
|
||||
|
||||
public func decodeOTPWithKey(_ otpHash: String) -> Bool {
|
||||
LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey)
|
||||
}
|
||||
@@ -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
|
||||
@@ -42,13 +42,46 @@ class LCENavigationState: ObservableObject {
|
||||
|
||||
/// A boolean value that controls the visibility of the navigation bar.
|
||||
@Published var hideNavigationBar: Bool = false
|
||||
|
||||
|
||||
/// The title view of the navigation bar.
|
||||
@Published var title: (any View) = Text("")
|
||||
/// The subtitle view of the navigation bar.
|
||||
@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.
|
||||
@@ -60,7 +93,10 @@ 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,20 +128,31 @@ 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
|
||||
Spacer()
|
||||
NavRightButton
|
||||
.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.
|
||||
private var TitleView: some View {
|
||||
VStack {
|
||||
@@ -115,8 +162,8 @@ 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,14 +217,8 @@ 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
|
||||
|
||||
if let string = state.rightButtonText.string, string.isEmpty {
|
||||
state.rightButtonText = text.foregroundColor(.clear)
|
||||
}
|
||||
if state.rightButtonImage == nil {
|
||||
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
|
||||
}
|
||||
|
||||
state.hasLeftButton = true
|
||||
|
||||
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 {
|
||||
|
||||
39
Sources/LCFeatureControl/FeatureControlAuthorizing.swift
Normal file
39
Sources/LCFeatureControl/FeatureControlAuthorizing.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Sources/LCFeatureControl/FeatureControlCache.swift
Normal file
46
Sources/LCFeatureControl/FeatureControlCache.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
33
Sources/LCFeatureControl/FeatureControlConfiguration.swift
Normal file
33
Sources/LCFeatureControl/FeatureControlConfiguration.swift
Normal 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 30–60s client-side.
|
||||
public var cacheTTL: TimeInterval
|
||||
/// FC-060 §5 recommends 1.5–3s 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
|
||||
}
|
||||
}
|
||||
30
Sources/LCFeatureControl/FeatureControlContext.swift
Normal file
30
Sources/LCFeatureControl/FeatureControlContext.swift
Normal 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
|
||||
}
|
||||
}
|
||||
18
Sources/LCFeatureControl/FeatureControlDateParsing.swift
Normal file
18
Sources/LCFeatureControl/FeatureControlDateParsing.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
51
Sources/LCFeatureControl/FeatureControlError.swift
Normal file
51
Sources/LCFeatureControl/FeatureControlError.swift
Normal 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
|
||||
}
|
||||
}
|
||||
28
Sources/LCFeatureControl/FeatureControlExposureEvent.swift
Normal file
28
Sources/LCFeatureControl/FeatureControlExposureEvent.swift
Normal 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?
|
||||
}
|
||||
61
Sources/LCFeatureControl/FeatureControlFlag.swift
Normal file
61
Sources/LCFeatureControl/FeatureControlFlag.swift
Normal 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
|
||||
}
|
||||
33
Sources/LCFeatureControl/FeatureControlJSON.swift
Normal file
33
Sources/LCFeatureControl/FeatureControlJSON.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
142
Sources/LCFeatureControl/FeatureControlManager.swift
Normal file
142
Sources/LCFeatureControl/FeatureControlManager.swift
Normal 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)]) }
|
||||
}
|
||||
}
|
||||
65
Sources/LCFeatureControl/FeatureControlNotification.swift
Normal file
65
Sources/LCFeatureControl/FeatureControlNotification.swift
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Tests/LCFeatureControlTests/FeatureControlAuthTests.swift
Normal file
38
Tests/LCFeatureControlTests/FeatureControlAuthTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
70
Tests/LCFeatureControlTests/FeatureControlCacheTests.swift
Normal file
70
Tests/LCFeatureControlTests/FeatureControlCacheTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
76
Tests/LCFeatureControlTests/FeatureControlErrorTests.swift
Normal file
76
Tests/LCFeatureControlTests/FeatureControlErrorTests.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
40
Tests/LCFeatureControlTests/FeatureControlJSONTests.swift
Normal file
40
Tests/LCFeatureControlTests/FeatureControlJSONTests.swift
Normal 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)]))
|
||||
}
|
||||
}
|
||||
187
Tests/LCFeatureControlTests/FeatureControlManagerTests.swift
Normal file
187
Tests/LCFeatureControlTests/FeatureControlManagerTests.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
164
Tests/LCFeatureControlTests/FeatureControlModelsTests.swift
Normal file
164
Tests/LCFeatureControlTests/FeatureControlModelsTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
137
Tests/LCFeatureControlTests/Support/StubURLProtocol.swift
Normal file
137
Tests/LCFeatureControlTests/Support/StubURLProtocol.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user