Architecture refactor: encapsulation + testability across Core / TapToPay#16
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements a cross-module architecture refactor to improve encapsulation and testability across PayabliSDKCore and PayabliSDKTapToPay, introducing a shared PayabliSession backbone and a transport seam (PayabliTransport + AuthenticatedTransport) while consolidating reusable test fixtures into a shipped PayabliSDKTestUtils product.
Changes:
- Introduces
PayabliSession(Core) and a transport decorator to centralize bearer-header injection and 401 refresh-and-retry behavior. - Moves/generalizes shared primitives into Core (e.g., generic
EventMulticaster, transport-levelRetryPolicy) and updates TapToPay clients to use the transport seam. - Adds
PayabliSDKTestUtilsas a real SPM product and migrates common fixtures from test bundles into it; updates tests/import hygiene accordingly.
Reviewed changes
Copilot reviewed 63 out of 63 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/PayabliSDKTestUtilsTests/PayabliSDKTestUtilsTests.swift | Adds smoke tests for the new PayabliSDKTestUtils product. |
| Tests/PayabliSDKTapToPayTests/TapToPayProviderFactoryTests.swift | Removes tests for the deleted provider factory. |
| Tests/PayabliSDKTapToPayTests/SecureStorageTests.swift | Updates tests to use fixtures from PayabliSDKTestUtils. |
| Tests/PayabliSDKTapToPayTests/PayabliTTPTests.swift | Updates tests to use fixtures from PayabliSDKTestUtils. |
| Tests/PayabliSDKTapToPayTests/PayabliTTPSessionInitTests.swift | Adds coverage for shared-session facade initialization. |
| Tests/PayabliSDKTapToPayTests/PayabliTTPEventCodeMappingTests.swift | Drops @testable usage for public mapping test. |
| Tests/PayabliSDKTapToPayTests/MockTapToPayProvider.swift | Deletes in-test mock moved to PayabliSDKTestUtils. |
| Tests/PayabliSDKTapToPayTests/MockAppAttestor.swift | Deletes in-test mock moved to PayabliSDKTestUtils. |
| Tests/PayabliSDKTapToPayTests/EventMulticasterTests.swift | Deletes TapToPay-local multicaster tests (now in Core). |
| Tests/PayabliSDKTapToPayTests/AppAttestServiceTests.swift | Updates tests to use fixtures from PayabliSDKTestUtils. |
| Tests/PayabliSDKCoreTests/TelemetryClientTests.swift | Updates version assertion and uses in-memory telemetry transport fixture. |
| Tests/PayabliSDKCoreTests/StubURLProtocol.swift | Deletes in-test StubURLProtocol moved to PayabliSDKTestUtils. |
| Tests/PayabliSDKCoreTests/RetryPolicyTests.swift | Updates tests for Core retry primitives and stronger error assertions. |
| Tests/PayabliSDKCoreTests/PayabliTransportTests.swift | Adds compile-time conformance test for the transport seam. |
| Tests/PayabliSDKCoreTests/PayabliSessionTests.swift | Adds tests for PayabliSession construction and URLSession injection. |
| Tests/PayabliSDKCoreTests/PayabliServiceTests.swift | Updates tests to use fixtures from PayabliSDKTestUtils. |
| Tests/PayabliSDKCoreTests/PayabliSDKCoreTests.swift | Drops @testable import where not needed. |
| Tests/PayabliSDKCoreTests/PayabliErrorCodeMappingTests.swift | Adds validation error code mapping test coverage. |
| Tests/PayabliSDKCoreTests/PayabliEnvironmentTests.swift | Gates .local assertions behind #if DEBUG. |
| Tests/PayabliSDKCoreTests/PayabliAuthTests.swift | Adds coverage for tokenChanges() stream emission. |
| Tests/PayabliSDKCoreTests/EventMulticasterTests.swift | Adds tests for the new generic Core multicaster. |
| Tests/PayabliSDKCoreTests/AuthenticatedTransportTests.swift | Adds tests for auth injection + 401 refresh-and-retry decorator behavior. |
| Sources/PayabliSDKTestUtils/StubURLProtocol.swift | Promotes StubURLProtocol to public test utility with body-stream draining. |
| Sources/PayabliSDKTestUtils/PayabliSDKTestUtils.swift | Adds a TestUtils namespace + bundle-derived version accessor. |
| Sources/PayabliSDKTestUtils/MockTapToPayProvider.swift | Adds a public mock TapToPay provider fixture. |
| Sources/PayabliSDKTestUtils/MockDeviceAttestationService.swift | Adds a public mock device attestation fixture. |
| Sources/PayabliSDKTestUtils/MockAppAttestor.swift | Adds a public mock App Attest fixture. |
| Sources/PayabliSDKTestUtils/InMemoryTelemetryTransport.swift | Adds an in-memory TelemetryTransport fixture. |
| Sources/PayabliSDKTestUtils/InMemorySecureStorage.swift | Adds an in-memory SecureStorage fixture. |
| Sources/PayabliSDKTelemetry/PayabliSDKTelemetry.swift | Switches telemetry module version to bundle-derived value. |
| Sources/PayabliSDKTapToPay/TTPTransactionClient.swift | Refactors to depend on PayabliTransport instead of raw service/auth. |
| Sources/PayabliSDKTapToPay/TTPConfigClient.swift | Refactors to depend on PayabliTransport and shared HTTP error mapping. |
| Sources/PayabliSDKTapToPay/TapToPayProviderFactory.swift | Removes unused provider factory implementation. |
| Sources/PayabliSDKTapToPay/SessionManager.swift | Tightens visibility of SessionManager to internal. |
| Sources/PayabliSDKTapToPay/SecureStorage.swift | Removes in-module in-memory storage; points tests to TestUtils fixture. |
| Sources/PayabliSDKTapToPay/PayabliTTP+Charge.swift | Removes open-coded 401 retry; uses session transport for updates. |
| Sources/PayabliSDKTapToPay/PayabliTTP.swift | Introduces session: designated init + uses shared session/transport; extracts ObjC helpers. |
| Sources/PayabliSDKTapToPay/PayabliSDKTapToPay.swift | Switches TapToPay module version to bundle-derived value. |
| Sources/PayabliSDKTapToPay/KeychainStorage.swift | Trims extra KeychainStorage methods from public surface. |
| Sources/PayabliSDKTapToPay/EventMulticasterAlias.swift | Adds TapToPay alias to Core EventMulticaster<PayabliTTPEvent>. |
| Sources/PayabliSDKTapToPay/EventMulticaster.swift | Removes TapToPay-local multicaster implementation (now in Core). |
| Sources/PayabliSDKTapToPay/DeviceAttestationService.swift | Adds explicit initializer to AssertionHeaders. |
| Sources/PayabliSDKTapToPay/AppAttestService+Defaults.swift | Demotes default hardware-id providers to internal visibility. |
| Sources/PayabliSDKTapToPay/AppAttestService.swift | Splits init: public convenience + internal designated for provider overrides. |
| Sources/PayabliSDKTapToPay/Adapters/FiservCardReader.swift | Tightens visibility of Credentials and setCredentials. |
| Sources/PayabliSDKTapToPay/_ObjCBridging.swift | Extracts ObjC bridging helpers from the facade file. |
| Sources/PayabliSDKCore/Telemetry/TelemetryClient.swift | Removes Core in-memory transport; uses Core version for sdkVersion default. |
| Sources/PayabliSDKCore/Public/PayabliSession.swift | Adds shared session backbone exposing auth/service and computed transport. |
| Sources/PayabliSDKCore/Public/PayabliEnvironment.swift | Gates .local environment behind #if DEBUG. |
| Sources/PayabliSDKCore/Public/PayabliConfig.swift | Converts PayabliConfig to a value type (struct). |
| Sources/PayabliSDKCore/PayabliSDKCore.swift | Switches Core version to bundle-derived value. |
| Sources/PayabliSDKCore/Networking/RetryPolicy.swift | Generalizes retry docs and fallback error type in Core. |
| Sources/PayabliSDKCore/Networking/PayabliTransport.swift | Introduces the transport protocol seam. |
| Sources/PayabliSDKCore/Networking/PayabliService.swift | Conforms to PayabliTransport and factors shared HTTP error mapping. |
| Sources/PayabliSDKCore/Networking/AuthenticatedTransport.swift | Adds auth-injecting decorator with 401 refresh-and-retry behavior. |
| Sources/PayabliSDKCore/Models/PayabliError.swift | Adds .validation error code and maps validation errors correctly. |
| Sources/PayabliSDKCore/Concurrency/EventMulticaster.swift | Adds generic Core EventMulticaster<Event>. |
| Sources/PayabliSDKCore/Auth/SessionTierValidator.swift | Demotes SessionTierValidator visibility to internal. |
| Sources/PayabliSDKCore/Auth/PayabliAuth.swift | Adds tokenChanges() AsyncStream and emits on refresh. |
| Package.swift | Adds PayabliSDKTestUtils product/target and wires tests to it. |
| Documentation/Plans/2026-05-06-architecture-refactor.md | Adds detailed implementation plan document. |
| Documentation/ArchitectureAssessment-2026-05-06.md | Adds architecture assessment + resolution status. |
| CLAUDE.md | Codifies an “umbrella inclusion rule” for module packaging. |
Comments suppressed due to low confidence (3)
Sources/PayabliSDKTapToPay/KeychainStorage.swift:42
- Changing
data(forKey:)frompublictointernalon apublictype is a source-breaking change for any consumer calling it directly. If the goal is to tighten the public surface without breaking downstream code, consider keeping itpublicbut deprecating it (and/or update the PR’s “No public-API breaks” statement / versioning).
internal func data(forKey key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
Sources/PayabliSDKTapToPay/KeychainStorage.swift:65
- The
set(_ data:forKey:)overload being demoted frompublictointernalis a source-breaking API change for any consumer that stored rawDatainKeychainStorage. If this is intended, it should be called out in release notes / semver; otherwise consider deprecating rather than removing public access.
internal func set(_ data: Data, forKey key: String) throws {
let baseQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
Sources/PayabliSDKTapToPay/KeychainStorage.swift:100
removeAll()was previouslypublicand is nowinternal, which is a source-breaking change for any external cleanup code using it. If you need to hide it, consider a deprecation period or updating the PR/release messaging to reflect this public API removal.
internal func removeAll() {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service
]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Returns all recorded batches and clears the internal buffer. | ||
| public func drainBatches() -> [[TelemetryEvent]] { | ||
| let snapshot = batches | ||
| batches.removeAll() | ||
| return snapshot |
| func testMockTapToPayProviderInitializes() { | ||
| _ = MockTapToPayProvider() | ||
| XCTAssertTrue(true) | ||
| } |
| "PayabliSDKTapToPay", | ||
| "PayabliSDKTelemetry" |
| public final class MockTapToPayProvider: TapToPayProvider, @unchecked Sendable { | ||
| public static var providerId: String { "mock" } | ||
|
|
||
| public var eligibility: Result<Void, PayabliTTPError> = .success(()) | ||
| public var prepareReaderResult: Result<Void, Error> = .success(()) |
| public final class MockDeviceAttestationService: DeviceAttestationService, @unchecked Sendable { | ||
| public var isAlreadyAttested: Bool = false | ||
| public var cachedDeviceId: String? | ||
| public var attestResult: Result<AttestationResult, Error> = .success( | ||
| AttestationResult(keyId: "mock_key", deviceId: "mock_device") |
| public final class MockAppAttestor: AppAttestor, @unchecked Sendable { | ||
| public var isSupported: Bool = true | ||
| public var generatedKeyId: AppAttestKeyId = AppAttestKeyId("mock_keyId") | ||
| public var attestationPayload: AttestationObject = AttestationObject(Data("attest".utf8)) | ||
| public var assertionPayload: AppAttestAssertion = AppAttestAssertion(Data("assert".utf8)) |
|
Follow-up batch landed (7 commits on top of
Both originally-listed follow-ups (
|
Outputs from the architecture audit pass: the assessment document (findings + decision-ready proposals) and the implementation plan that turns those proposals into bite-sized, TDD-shaped tasks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RetryPolicy, Retry, and RetryableError are general-purpose networking utilities with no TapToPay-specific behaviour. Moving them to PayabliSDKCore/Networking/ makes them reusable by all modules and removes the PayabliTTPError fallback throw, replaced with PayabliGenericError(.networkError). Test coverage moved to PayabliSDKCoreTests accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EventMulticaster is now generic over `Event: Sendable`, making it a reusable fan-out primitive in PayabliSDKCore/Concurrency/. TapToPay keeps a local typealias `TTPEventMulticaster = EventMulticaster<PayabliTTPEvent>` and updates the one construction site in PayabliTTP. Core tests exercise the generic via `EventMulticaster<Int>` and `EventMulticaster<String>`, with no dependency on TapToPay types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review on Phase 1 flagged that RetryPolicy's doc comment still referenced a TapToPay-specific endpoint, that EventMulticaster's public methods lost their doc comments during the generalization move, and that the retry-exhaustion test wasn't asserting the error type that propagates. All three are restored / tightened without changing behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SessionManager is an implementation detail of the PayabliTTP facade and has no business appearing on the public API surface. Strip public from the class, its @published properties, and its initializer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d internal init Split the single 8-arg public init into a 4-arg public convenience init (uses platform defaults) and an 8-arg internal designated init (used by tests to inject deterministic hardware-id providers). Host apps and the production convenience init in PayabliTTP both use the 4-arg surface; test code reaches the internal init via @testable import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Credentials is a pure implementation detail — it's constructed from the /config dict inside configure() and passed immediately into the Fiserv SDK. Host apps never create or inspect Credentials directly, so making it (and setCredentials) internal removes leakage of a low-level wire type from the public surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on is real The validator is a stub (always returns tier1Transactional). Exposing it as public API would lock the detection contract before the JWT-claim path is implemented in phase 2+ (§16.7). Internal access keeps it reachable by TapToPay via @testable import without broadening the public surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.local points at a developer ngrok tunnel and must never ship in a release binary. Wrapping the case and its baseURL branch in #if DEBUG ensures release (non-debug) builds cannot reference it while tests (which are always DEBUG) continue to exercise it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PayabliValidationError wraps HTTP 400 server-side field errors — that is semantically distinct from a JSON decoding failure. Add a .validation case to PayabliErrorCode (raw value "VALIDATION_ERROR") and update PayabliValidationError.code to return it. A new test asserts the mapping. The existing .decodingError case is retained for genuine decode failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dging.swift Both types exist solely to bridge ObjC completion blocks into Swift CheckedContinuations in the @objc convenience init. Moving them out of the 300-line PayabliTTP facade into a dedicated file gives each file a single clear responsibility. No behaviour change; both types remain internal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…local-env test
Code review on Phase 2 flagged two follow-ups:
- AppAttestService+Defaults.{hardwareId,deviceName,model,osVersion}
were left public after the init split — they only need internal
access since the convenience init forwards them within the module.
- PayabliEnvironmentTests referenced .local unconditionally; .local
is now DEBUG-only, so the test file must wrap that assertion in
#if DEBUG to keep release-config test builds compiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Class identity adds nothing — there's no inheritance, no shared mutable state, no need for reference semantics. The @unchecked Sendable opt-out hid the fact that PayabliTokenRefresh is already @sendable, so a struct is naturally Sendable. Audit found zero call sites comparing configs by reference; this is a pure semantic change with no behavioural impact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Components now share a single PayabliAuth/PayabliService when constructed with the same PayabliSession. The existing config: convenience init is preserved (delegates to a fresh session) so no public API breaks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets future modules and telemetry observe token rotations. Each subscriber gets an independent stream; continuations are cleaned up automatically on iterator release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `PayabliTransport` protocol with `perform(_:)` and `performV2(_:decoding:)` requirements as a seam for decorators. `PayabliService` gains the conformance. Proven by a compile-time test in `PayabliTransportTests`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-retry Implements a `PayabliTransport` decorator that injects `Authorization: Bearer` on every request and performs a single refresh-and-retry on 401. A second consecutive 401 after refresh throws `.tokenExpired`. Tested with a `MockTransport` actor for deterministic scripted responses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `PayabliSession.transport: any PayabliTransport` computed property that wraps the session's `PayabliService` in an `AuthenticatedTransport`. Endpoint clients consume this property instead of holding separate `service` + `auth` pairs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the `service: PayabliService, auth: PayabliAuth` pair with a single `transport: any PayabliTransport`. Removes the manual `Authorization` header from `initiate(_:)` — bearer injection is now the transport's responsibility. `PayabliTTP` passes `session.transport` at construction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces `service: PayabliService, auth: PayabliAuth` with a single `transport: any PayabliTransport`. Removes the manual bearer merge — AuthenticatedTransport injects the header. Attestation headers remain inline (component-specific). HTTP 401 at the response layer is now handled by the transport's refresh-and-retry; a body-level 401 (attestation revoked) still propagates as `.tokenExpired`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `let session: PayabliSession` ivar to `PayabliTTP` and uses `session.transport` in `tryUpdate`. The manual token-fetch / 401-check / invalidateAndRefresh / retry sequence is removed; `AuthenticatedTransport` now owns that responsibility. The Retry loop still handles network-retryable errors (5xx, timeouts) as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Error Extracts the switch-on-status-code logic from `PayabliService.mapHTTPError` into a public free function `mapPayabliHTTPError(response:override:)`. The `override` parameter lets component clients supply status-code → domain-error translations before falling through to the standard PRD §8 mapping. `PayabliService.mapHTTPError` now delegates to the free function. `TTPConfigClient` replaces its manual 403/non-2xx guard with a single `mapPayabliHTTPError` call that overrides 403 → `PayabliTTPError.devicePendingActivation`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…performV2 Spec review on Phase 5 caught that AuthenticatedTransport.performV2 was decoding non-2xx responses straight into PayabliV2Envelope and throwing .decodingError when that failed. PayabliService.performV2 calls mapPayabliHTTPError before decoding so 4xx/5xx surface as the correct typed errors (.validation, .decline, .server, ...). This restores the same behaviour through the decorator path used by TTPTransactionClient.initiate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the transport-seam migration, PayabliTTP no longer routes through `service`/`auth` — every consumer reaches them via `session.transport` instead. The stored properties had become dead weight that misleadingly suggested a second access path. The session-sharing test now asserts shared `session` identity, which is structurally equivalent (one PayabliSession owns one auth) and keeps the proof point of the test intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The factory had no production caller — PayabliTTP wired FiservCardReader() directly. Removing dead public surface is cheaper than evolving it; reintroduce a registry once a real second-provider use case materializes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves Tests/PayabliSDKTapToPayTests/MockAppAttestor.swift into Sources/PayabliSDKTestUtils/MockAppAttestor.swift and promotes all declarations to public. AppAttestServiceTests already imports PayabliSDKTestUtils from the StubURLProtocol step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts MockTapToPayProvider from Tests/PayabliSDKTapToPayTests/MockTapToPayProvider.swift into Sources/PayabliSDKTestUtils/MockTapToPayProvider.swift with all declarations promoted to public. The test file is updated to retain only MockDeviceAttestationService (moved in the next commit). Adds import PayabliSDKTestUtils to PayabliTTPSessionInitTests.swift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stUtils Extracts MockDeviceAttestationService from Tests/PayabliSDKTapToPayTests/MockTapToPayProvider.swift (which now no longer needs to exist) into Sources/PayabliSDKTestUtils/MockDeviceAttestationService.swift with all declarations public. Also adds a public memberwise init to AssertionHeaders in DeviceAttestationService.swift, which lacked one (the auto-synthesized init was internal, blocking compilation from the TestUtils module boundary). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Utils Moves InMemoryTelemetryTransport out of Sources/PayabliSDKCore/Telemetry/TelemetryClient.swift (where it was a test fixture embedded in production code) into Sources/PayabliSDKTestUtils/InMemoryTelemetryTransport.swift. Adds import PayabliSDKTestUtils to TelemetryClientTests.swift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a PayabliSDKTestUtilsTests target with three smoke tests that verify StubURLProtocol.makeSession() injects the stub protocol class, InMemorySecureStorage round-trips set/get/remove, and MockTapToPayProvider initializes without crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…age protocol Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d of hard-coding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Append a Resolution status appendix to the assessment doc. 49 commits on this branch implement all 5 proposals (A-E) and 17 of 19 findings; F8 and F17 were deferred in the original assessment, F18 was skipped because PaymentCardReader.isSupported is load-bearing for the import the audit hypothesized was redundant. Three follow-ups surfaced during code review (AppAttestService+Requests still bypasses the transport seam; AuthenticatedTransport public visibility; package access for PayabliSession.auth/service) are listed for a future pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The attestation HTTP path was the last consumer that constructed its own Authorization header and called PayabliService.perform directly, bypassing the AuthenticatedTransport decorator that every other module now uses. Migrating it lands the full Phase 5 intent: every authenticated SDK request flows through one place that handles bearer-header injection, 401 refresh-and-retry, and typed HTTP error mapping. AppAttestService now takes a `transport: any PayabliTransport` in place of (service, auth). The host-facing convenience init on PayabliTTP was already constructing AppAttestService internally, so no public-API consumer is affected. Closes follow-up #1 in the resolution table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demote AuthenticatedTransport and its init(base:auth:) to internal. The type is reachable from the public surface only via PayabliSession.transport (which returns `any PayabliTransport`), so hiding the concrete type reduces the public API footprint. Add @testable import PayabliSDKCore to AppAttestServiceTests so the fixture helper can still construct the type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the three public stored properties (config, auth, service) with inline doc comments so Xcode Quick Help and generated documentation convey each property's role and the recommended usage pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iSession Replace the trivially-passing XCTAssertNotNil on a non-optional property with a real integration test: configure StubURLProtocol, pass a custom URLSession to PayabliSession, perform a request via session.service, and assert the stubbed status code and body are returned — proving the injected session is actually used for outbound calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move case validation after userCancelled within the client-side block, matching alphabetical order within the group (u < v). Previously it appeared between decodingError and userCancelled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a precondition guard at the top of RetryPolicy.init so that passing maxAttempts: 0 (or negative) traps immediately with a clear message rather than crashing inside Retry.run with a fatal 1...0 range error. Add a positive test confirming maxAttempts == 1 constructs without trapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed by the demotion in commit d971ca5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7bd33c4 to
b616a67
Compare
The original resolution table was committed before this branch was rebased against main's "README adjustments" commit. The rebase rewrote every SHA on this branch; the table is now updated to reference the current commits on the rebased history. 39 SHA references remapped by commit message; baseline reference to `180dff6` (on main) preserved. No content changes beyond the SHAs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add PayabliSDKTestUtils to the Modules table — Phase 8 of the refactor shipped it as a real library product (mirroring NIOTestUtils and InMemoryLogging). - Add a one-paragraph note in Installation showing how to link the test-utils product from a host app's testTarget, plus the list of fixtures it ships. - Fix broken anchor link: the `entryPoint` parameter row referenced #payabli-entry-point but the section heading is "Payabli entrypoint" (one word), so the GitHub-generated anchor is #payabli-entrypoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor-caused updates: - Module count is now five (added PayabliSDKTestUtils library product in Phase 8); add a dedicated section for it. - PayabliSDKCore section now lists the new types: PayabliSession, PayabliTransport / AuthenticatedTransport, RetryPolicy / Retry, EventMulticaster<Event>, mapPayabliHTTPError, and PayabliAuth's tokenChanges() AsyncStream. - PayabliSDKTapToPay description no longer claims to host retry policy or EventMulticaster — both moved to Core in Phase 1. - Auth flow now mediated by AuthenticatedTransport; updated wording so contributors look there first instead of in each endpoint client. - Sources/ folder rules row mentions PayabliSDKTestUtils. - Testing section reflects the new test-utils library product and the @testable cleanup from Phase 7. Pre-existing inaccuracies cleaned up while in the file: - Build commands now say up-front that swift build/test fail on macOS for this package (ProximityReader is iOS-only); show the xcodebuild invocations CI uses. - TTP state-machine list had six wrong state names (attesting, reading, charged, declined, cancelled, reinitialized, ...); replaced with the actual nine cases per SessionManager. - Platform-gating note no longer claims macOS builds compile without TTP — they don't. - Reference section says Example/PayabliDemo needs Secrets.swift (the file that actually exists), not Config.xcconfig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…servable contract Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… NSLock Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e behind NSLock Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed @copilot-pull-request-reviewer's findings (5 commits on top of
Tests green after each commit. |
Summary
packageaccess, WWDC19 #410, swift-nio / swift-log / swift-argument-parser conventions). Findings + decision-ready proposals live inDocumentation/ArchitectureAssessment-2026-05-06.md; the bite-sized implementation plan lives inDocumentation/Plans/2026-05-06-architecture-refactor.md.PaymentCardReader.isSupportedis load-bearing) and all five proposals, withxcodebuild testgreen after every commit.PayabliSession), a transport seam (PayabliTransport+AuthenticatedTransportdecorator) so bearer-header injection and 401-refresh-and-retry happen in one place, and a realPayabliSDKTestUtilslibrary product (modeled onNIOTestUtils/swift-log'sInMemoryLogging) so host apps and future modules can depend on the SDK's stub fixtures instead of re-implementing them.Highlights of the structural change
RetryPolicy, genericEventMulticaster<Event>,PayabliTransportprotocol,AuthenticatedTransportdecorator (internal),PayabliSession,mapPayabliHTTPErrorfree function, andPayabliAuth.tokenChanges()AsyncStream.PayabliConfigis now a value type.PayabliSessionis the identity-bearing object that wires onePayabliAuth+ onePayabliServicefor sharing across components — when PayIn re-lands fromdevelopit constructs from the same session.tryUpdatecallssession.transport.perform(...)and the decorator handles the refresh-and-retry once; double-401 throws.tokenExpired.AppAttestServicefollows the same path (no manual bearer injection).SessionManager,SessionTierValidator,FiservCardReader.Credentials,AppAttestServicehardware-id providers,PayabliService.makeDefaultSession,AppAttestService+Defaultsproviders,AuthenticatedTransport,KeychainStoragenon-protocol methods, andPayabliEnvironment.local(gated behind#if DEBUG) all demoted frompublictointernal(or DEBUG-only).PayabliSDKTestUtilsshipped as a real library product. Six fixtures (StubURLProtocol,InMemorySecureStorage,MockAppAttestor,MockTapToPayProvider,MockDeviceAttestationService,InMemoryTelemetryTransport) consolidated intoSources/PayabliSDKTestUtils/, with a smoke-test bundle. All mock state is nowNSLock-synchronized so the@unchecked Sendableconformances are honest.@testable importhygiene: 12 of 23 test files no longer need@testableafter public-surface tightening; remaining 11 keep it for documented internal-access reasons.TapToPayProviderFactory(no production caller), the duplicatedservice/authivars onPayabliTTP, theLocked/UncheckedSendableBoxtypes extracted from the facade into a dedicated file.Bundle(for:).infoDictionary["CFBundleShortVersionString"]with"0.0.0"fallback — no more"1.0.0"literal drift.Resolution table
Full mapping of every finding/proposal to its commit SHA(s) is at the bottom of
Documentation/ArchitectureAssessment-2026-05-06.mdunder "Resolution status". Of the three follow-ups originally noted there, two are closed in this PR (AppAttestService+Requeststransport-seam migration;AuthenticatedTransportdemoted tointernal). The remaining follow-up —PayabliSession.auth/.service→packageaccess — stays as a separate PR since it requires verifyingpackageworks across test bundles.API surface changes
The two consumer-facing inits on
PayabliTTP(theaccessToken:-based convenience init and theconfig:-based init) keep their signatures verbatim. The@objcconvenience init chain is intact. Theconfig:init is now aconvenience initthat builds aPayabliSessionand delegates; the newsession:init is the designated init.Three accidentally-public methods on
KeychainStorage(data(forKey:),set(_ data:forKey:),removeAll()) are demoted tointernal. They were never documented in the README, never part of theSecureStorageprotocol, and existed only because the original struct didn't tighten its access modifiers. The audit (Finding 19e) recommended trimming them. Anyone reaching directly intoKeychainStoragefor rawDatastorage rather than through theSecureStorageprotocol would need to switch to the protocol. No documented consumer is affected.Test plan
xcodebuild test -scheme PayabliSDK-Package -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4.1'— passes locally on every commit; verify clean run on the GitHub Actions runner.import PayabliSDKTapToPayplus the existingPayabliTTP(accessToken:tokenProvider:entryPoint:appId:environment:)init compile unchanged.Documentation/ArchitectureAssessment-2026-05-06.mdresolution table and confirm each row's commit SHA exists on this branch (git show <sha>).Sources/PayabliSDKCore/Public/PayabliSession.swiftandSources/PayabliSDKCore/Networking/AuthenticatedTransport.swiftend-to-end — these are the new architectural seams.PayabliSession.{auth, service}visibility (only remaining open follow-up).🤖 Generated with Claude Code