diff --git a/apps/backend/src/routes/event-capture.ts b/apps/backend/src/routes/event-capture.ts index d3fc3c312..88e533703 100644 --- a/apps/backend/src/routes/event-capture.ts +++ b/apps/backend/src/routes/event-capture.ts @@ -19,9 +19,15 @@ import { CaptureDependencyUnavailableError, CaptureInternalServerError, CaptureRateLimitedError, + CaptureRejectedRecord, EventCaptureApi, + MeasurementDeletionAcceptedResponse, + ProtectedEvidenceAcceptedResponse, } from "@voidhash/api-contracts/event-capture"; import { EventCaptureService } from "@voidhash/core/services/analyticsIngest/EventCaptureService"; +import { MeasurementConfigurationService } from "@voidhash/core/services/measurement/MeasurementConfigurationService"; +import { MeasurementDeletionService } from "@voidhash/core/services/measurement/MeasurementDeletionService"; +import { ProtectedEvidenceService } from "@voidhash/core/services/measurement/ProtectedEvidenceService"; import { Effect } from "effect"; import * as HttpEffect from "effect/unstable/http/HttpEffect"; import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; @@ -60,6 +66,9 @@ export const EventCaptureGroupLive = HttpApiBuilder.group( (handlers) => Effect.gen(function* () { const captureService = yield* EventCaptureService; + const configurationService = yield* MeasurementConfigurationService; + const deletionService = yield* MeasurementDeletionService; + const protectedEvidenceService = yield* ProtectedEvidenceService; return handlers .handle("capture", ({ request, payload }) => Effect.gen(function* () { @@ -113,7 +122,9 @@ export const EventCaptureGroupLive = HttpApiBuilder.group( return new CaptureAcceptedResponse({ accepted: result.accepted, - rejected: result.rejected, + rejected: result.rejected.map((record) => + new CaptureRejectedRecord(record), + ), }); }), ) @@ -169,9 +180,103 @@ export const EventCaptureGroupLive = HttpApiBuilder.group( return new CaptureAcceptedResponse({ accepted: result.accepted, - rejected: result.rejected, + rejected: result.rejected.map((record) => + new CaptureRejectedRecord(record), + ), }); }), + ) + .handle("protected", ({ payload }) => + protectedEvidenceService.put(payload).pipe( + Effect.map( + (result) => + new ProtectedEvidenceAcceptedResponse({ + accepted: true, + blobId: result.blobId, + }), + ), + Effect.catchTag("EffectDrizzleQueryError", () => + Effect.fail( + new CaptureDependencyUnavailableError({ + code: "dependency_unavailable", + error: "protected evidence dependency is unavailable", + }), + ), + ), + Effect.catchDefect(() => + Effect.fail( + new CaptureInternalServerError({ + code: "internal_error", + error: "internal server error", + }), + ), + ), + ), + ) + .handle("deleteMeasurementData", ({ payload }) => + deletionService.request(payload).pipe( + Effect.map( + (result) => + new MeasurementDeletionAcceptedResponse({ + accepted: true, + deletedProtectedEvidence: result.deletedProtectedEvidence, + requestId: result.requestId, + status: "completed", + }), + ), + Effect.catchTag("EffectDrizzleQueryError", () => + Effect.fail( + new CaptureDependencyUnavailableError({ + code: "dependency_unavailable", + error: "measurement deletion dependency is unavailable", + }), + ), + ), + Effect.catchTag("SqlError", () => + Effect.fail( + new CaptureDependencyUnavailableError({ + code: "dependency_unavailable", + error: "measurement deletion dependency is unavailable", + }), + ), + ), + Effect.catchDefect(() => + Effect.fail( + new CaptureInternalServerError({ + code: "internal_error", + error: "internal server error", + }), + ), + ), + ), + ) + .handle("getMeasurementConfiguration", ({ headers }) => + configurationService.get(headers["x-publishable-key"]).pipe( + Effect.catchTag("MeasurementConfigSigningError", () => + Effect.fail( + new CaptureDependencyUnavailableError({ + code: "dependency_unavailable", + error: "measurement configuration signing is unavailable", + }), + ), + ), + Effect.catchTag("EffectDrizzleQueryError", () => + Effect.fail( + new CaptureDependencyUnavailableError({ + code: "dependency_unavailable", + error: "measurement configuration dependency is unavailable", + }), + ), + ), + Effect.catchDefect(() => + Effect.fail( + new CaptureInternalServerError({ + code: "internal_error", + error: "internal server error", + }), + ), + ), + ), ); }), ); diff --git a/apps/backend/src/routes/links.ts b/apps/backend/src/routes/links.ts new file mode 100644 index 000000000..522a00ce0 --- /dev/null +++ b/apps/backend/src/routes/links.ts @@ -0,0 +1,49 @@ +import { + CreateLinkResponse, + LinksApi, + LinkInvalidRequestError, + LinkRateLimitedError, + LinkServiceUnavailableError, + LinkUnauthorizedError, +} from "@voidhash/api-contracts/links"; +import { LinkRedirectService } from "@voidhash/core/services/measurement/LinkRedirectService"; +import { Effect } from "effect"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; + +const publicOrigin = (headers: Readonly>): string => { + const host = headers["x-forwarded-host"]?.split(",")[0]?.trim() ?? headers.host?.trim(); + if (!host) return "https://links.voidhash.com"; + const protocol = headers["x-forwarded-proto"]?.split(",")[0]?.trim() ?? "https"; + return `${protocol}://${host}`; +}; + +const mapLinkError = (error: unknown): + | LinkInvalidRequestError + | LinkUnauthorizedError + | LinkRateLimitedError + | LinkServiceUnavailableError => { + if ( + error instanceof LinkInvalidRequestError || + error instanceof LinkUnauthorizedError || + error instanceof LinkRateLimitedError || + error instanceof LinkServiceUnavailableError + ) return error; + return new LinkServiceUnavailableError({ code: "service_unavailable", error: "link dependency is unavailable" }); +}; + +/** HTTP handlers for signed-link creation and deterministic deferred resolution. */ +export const LinksGroupLive = HttpApiBuilder.group(LinksApi, "links", (handlers) => + Effect.gen(function* () { + const service = yield* LinkRedirectService; + return handlers + .handle("createLink", ({ payload, request }) => + service.create(payload, publicOrigin(request.headers)).pipe( + Effect.map((result) => new CreateLinkResponse(result)), + Effect.mapError(mapLinkError), + ), + ) + .handle("resolveDeferredLink", ({ payload }) => + service.resolveDeferred(payload).pipe(Effect.mapError(mapLinkError)), + ); + }), +); diff --git a/docs/react-native-measurement/api-reference.md b/docs/react-native-measurement/api-reference.md new file mode 100644 index 000000000..941056513 --- /dev/null +++ b/docs/react-native-measurement/api-reference.md @@ -0,0 +1,43 @@ +# React Native unified SDK API + +Create the client with `createVoidhashClient(publishableKey, options)`. The common client methods are `init`, `capture`, `identify`, `reset`, `purchase`, `restorePurchases`, `flush`, and `end`. Captures use the identity, consent revision, session, and configuration that exist at capture time. + +## `client.measurement` + +- `configure(patch)` validates and applies collection, session, purchase, context, currency, locale, and protected-identity settings. Purchase observation supports iOS and Android purchase-kind enrichment callbacks; the returned object is validated and snapshotted at observation time. +- `start(options?)` starts or returns the current measurement session. +- `stop(options?)` independently stops collection, upload, or partner sharing. +- `handle(input)` records an explicit location or ATT observation. +- `on(event, listener)` subscribes to `error`, `attribution`, `attributionError`, `conversion`, `delivery`, `purchaseValidation`, or `session`. +- `getState()` returns a redacted local inspector. +- `createSupportBundle()` returns an opt-in, classifier-checked diagnostic document with hashed installation/session IDs. +- `getInstallationId()` returns the opaque local installation identifier. +- `createInviteLink(input)`, `trackInviteShare(input)`, and `trackCrossPromotion(input)` implement signed owned-media links. +- `trackAdRevenue(input)` records a decimal, currency-qualified ad impression. +- `validatePurchase(input)` returns a correlated `valid`, `invalid`, or `indeterminate` result. Inline receipts and legacy Android keys/signatures are rejected. +- `deleteData()` durably records deletion before protected local purge. +- `setTestDevice(enabled)` persists project test-device diagnostics across cold start. + +## `client.links` + +- `handle({ url, source, receivedAt? })` normalizes a manual or native link through allowlists, wrapped-domain limits, dedupe, and route projection. +- `on("deepLink", listener)` receives the single direct/deferred result stream. Results are `found`, `notFound`, or `error`; raw URLs are never returned. + +## `client.consent` + +- `set(snapshot)` requires a monotonically increasing revision and records the transition. +- `get()` returns the source snapshot and effective analytics, attribution, upload, and partner-sharing decisions. + +## `client.notifications` + +- `getPermissionStatus()` observes permission without prompting. +- `requestPermission(options?)` is the only permission-prompting path. +- `register()`, `unregister()`, and `getRegistration()` manage an opaque `pushDeviceTokenId`; raw platform tokens are protected and deleted after registration. +- `setBadgeCount(count)` sets or clears the native badge. +- `on(event, listener)` subscribes to `received`, `opened`, `tokenChanged`, and `registrationError`. + +## Errors and capability results + +All unified failures derive from `MeasurementError`. `MeasurementConfigurationError` identifies invalid configuration, `MeasurementInputError` invalid inputs, `MeasurementPolicyBlocked` an observable policy denial, and `MeasurementCapabilityUnavailable` an unavailable build/runtime capability. Capability reasons are `notConfigured`, `notImplemented`, `notInstalled`, `unsupported`, or `disabled`; calls do not silently succeed. + +Release measurement logs are disabled unless an internal Ed25519-signed diagnostic session is valid for the current project and time. Debug and authorized release logs use the same recursive protected-field redaction. diff --git a/docs/react-native-measurement/bare-react-native.md b/docs/react-native-measurement/bare-react-native.md new file mode 100644 index 000000000..23911a4fc --- /dev/null +++ b/docs/react-native-measurement/bare-react-native.md @@ -0,0 +1,9 @@ +# Bare React Native integration + +Install `@voidhash/react-native`, pods, and the Android Gradle dependencies, then make the same native changes produced by the Expo plugin. + +On iOS, add associated domains and URL schemes, forward application/scene/SwiftUI URLs to the Voidhash link collector, add `aps-environment` and remote-notification background mode when push is enabled, forward APNs registration/receipt/open callbacks, and set the SKAdNetwork and AdAttributionKit HTTPS postback origins. Select StoreKit 1, StoreKit 2, or disabled purchase observation explicitly. A strict-no-IDFA build must not link an IDFA collector. + +On Android, add verified App Link/custom-scheme intent filters and forward `onNewIntent`, register the lifecycle collector before React starts, configure Firebase and notification permission/channel policy, explicitly include or remove AD_ID, retain the measurement database under `noBackupFilesDir`, and include Google Play referrer plus only the requested optional-store providers. Select Billing 8 or disabled observation explicitly. + +Configure cloud or self-host origins through `endpoints`. Origins must not contain credentials, path, query, or fragment; production uses HTTPS. Self-host deployments configure rotating signed-measurement keys and a positive configuration version. Run `npx voidhash-doctor`; resolve every error before building the release archive/APK. diff --git a/docs/react-native-measurement/data-dictionary.md b/docs/react-native-measurement/data-dictionary.md new file mode 100644 index 000000000..51b44ab0c --- /dev/null +++ b/docs/react-native-measurement/data-dictionary.md @@ -0,0 +1,35 @@ +# Measurement data dictionary + +Every `MeasurementEnvelopeV1` contains `schemaVersion`, `recordId`, `type`, `occurredAt`, `queuedAt`, `installationId`, `installationSequence`, capture-time `identity` and `consent`, `app`, `device`, `source`, `publicPayload`, and optionally an opaque `protectedPayloadRef`. Session state and monotonic time are included when available. Product analytics carries standardized metadata under `context`, not duplicated inside event properties. + +| Record type | Public purpose | +| --- | --- | +| `installation.created.v1` | First open, app release, and collector capability baseline. | +| `installation.updated.v1` | App release transition. | +| `session.started.v1` | Session sequence, reason, and readiness. | +| `session.ended.v1` | End reason and monotonic duration. | +| `identity.changed.v1` | Immutable previous/current identity revisions. | +| `consent.changed.v1` | Immutable previous/current consent and effective policy. | +| `link.received.v1` | Source, app state, time, and protected raw-link reference. | +| `link.resolved.v1` | Direct/deferred status and allowlisted route/campaign projection. | +| `link.routed.v1` | Application routing outcome. | +| `android.install_referrer.v1` | Store outcome, timestamps/version/verification, protected referrer. | +| `android.preinstall.v1` | Typed OEM/preinstall attribution. | +| `ios.adservices.v1` | Availability/timing and protected Apple Ads result. | +| `ios.att.changed.v1` | ATT status transition and source. | +| `identifier.observed.v1` | Identifier kind, policy basis, outcome, and protected reference. | +| `push.token.v1` | Provider, environment, rotation reason, and opaque device-token ID. | +| `push.received.v1` | Allowlisted push metadata and protected payload reference. | +| `push.opened.v1` | Notification/open/link correlation. | +| `revenue.ad_impression.v1` | Impression ID, network, mediation, decimal revenue, currency, and safe dimensions. | +| `purchase.observed.v1` | Store transaction projection and protected receipt/token reference. | +| `purchase.validation_requested.v1` | Correlated validation request, environment, and idempotency key. | +| `purchase.validation_result.v1` | Valid/invalid/indeterminate outcome, store state, and failure class. | +| `diagnostic.capability.v1` | Collector/build capability and redacted error state. | +| `partner.context_changed.v1` | Partner IDs, configuration revision, and protected partner-context reference. | + +Protected vault purposes are `advertising-identifier`, `diagnostic-authorization`, `email`, `install-referrer`, `link-capture`, `partner-context`, `phone`, `purchase-receipt`, and `push-token`. A vault row carries its opaque blob ID, consent revision, retention class, encryption-key version, deletion state, and upload state. Ciphertext and raw values are excluded from public records, reports, state, and support bundles. + +`purchase.validation_result.v1` may carry normalized cancellation, pause/resume, offer, replacement, prepaid/top-up, price-change, line-item, and test-environment state. It never carries the raw store response; that response uses `purchase-receipt` protected storage. + +Standard event aliases are: `add payment info`, `add to cart`, `add to wishlist`, `complete registration`, `initiated checkout`, `invite shared`, `level achieved`, `location`, `login`, `purchase`, `rate`, `search`, `share`, `spent credits`, `subscribe`, `tutorial completion`, `unlock achievement`, `viewed content`, and the SDK-only automatic `opened from push notification` event. Revenue is represented only by explicit purchase/ad-revenue fields; aliases do not infer revenue. diff --git a/docs/react-native-measurement/operations-runbook.md b/docs/react-native-measurement/operations-runbook.md new file mode 100644 index 000000000..cfc803b09 --- /dev/null +++ b/docs/react-native-measurement/operations-runbook.md @@ -0,0 +1,13 @@ +# Measurement operations and support runbook + +Use `measurement.getState()` first. Record SDK/native/config versions, readiness, signed-config version, capability states, outbox counts, and last delivery outcome. Use `measurement.createSupportBundle()` only with operator/user consent; never request raw URLs, store receipts, push tokens, advertising identifiers, email, phone, protected ciphertext, or configuration key material. + +For delivery backlog, separate policy-blocked, retry-scheduled, and quarantined records. A 429 must preserve the server `Retry-After`; 5xx/network failures use bounded backoff; 413 recursively splits and quarantines only a failing single record. Verify protected evidence is acknowledged before investigating its referencing public record. Do not manually acknowledge or delete evidence to clear an alert. + +For self-hosted deployments, verify the API, ingest, and links origins independently. Rotate configuration signing keys by publishing the new public key ID alongside the old ID, deploying the new signer, confirming a higher signed version is accepted, and only then removing the old trust entry. Never lower a configuration version or reuse a signing key ID with different key material. + +Deletion incidents are tracked by request ID and installation/person scope. Confirm the durable client request, protected-vault purge, raw/derived-data deletion, and partner-send suppression. Retention exceptions require a documented legal basis and must remain unavailable to ordinary analytics reads. + +Partner incidents are investigated from append-only send/suppression audit rows: trigger ID, partner, current consent revision, filtered fields, result, and reason. Do not replay a postback until its idempotency key and current send-time policy have been checked. + +Release operators attach physical-device results for the fourteen scenarios, the Android/iOS matrix cells, the offline soak, self-host run, store/campaign runs, privacy/store disclosures, retention review, and security/legal/support approvals to the release decision record. diff --git a/docs/react-native-measurement/parity-test-map.json b/docs/react-native-measurement/parity-test-map.json new file mode 100644 index 000000000..33912138a --- /dev/null +++ b/docs/react-native-measurement/parity-test-map.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": 1, + "wontDo": ["PRIV-03", "PRIV-09", "PRIV-10", "CONS-03", "PUR-02", "PUR-03"], + "groups": [ + { + "rows": ["LIFE-01", "LIFE-02", "LIFE-03", "LIFE-04", "LIFE-05", "LIFE-06", "EVT-01", "EVT-02", "EVT-03", "EVT-04", "EVT-05", "EVT-06", "ID-01", "ID-02", "ID-03", "ATTR-02", "ATTR-03", "ATTR-04", "ATTR-05", "ATTR-06", "LINK-02", "LINK-03", "LINK-04", "LINK-05", "LINK-06", "LINK-07", "LINK-08", "OWN-01", "OWN-02", "OWN-03", "OWN-04", "OWN-05", "PUSH-01", "PUSH-02", "REV-01", "REV-02", "PUR-01", "PUR-04", "PC-01", "PC-02", "PC-03", "PC-05", "PC-06", "PC-07", "PC-08", "PC-09", "PC-10", "PC-11", "CFG-06", "CFG-07", "DIAG-01", "DIAG-02", "DIAG-03", "DIAG-05"], + "testFile": "libraries/react-native/tests/core/measurement-runtime.test.ts", + "testCase": "describe(\"UnifiedMeasurementRuntime\"" + }, + { + "rows": ["ID-04", "ID-05"], + "testFile": "libraries/react-native/tests/core/protected-identity.test.ts", + "testCase": "describe(\"protected identity\"" + }, + { + "rows": ["ATTR-01"], + "testFile": "packages/core/test/domain/measurement/Attribution.test.ts", + "testCase": "describe(\"attribution engine\"" + }, + { + "rows": ["ATTR-07", "ATTR-08"], + "testFile": "libraries/react-native/android/src/test/java/com/margelo/nitro/voidhash/measurement/MeasurementStoreTest.kt", + "testCase": "optionalAndConfiguredReferrerProvidersExposeExplicitCapabilities" + }, + { + "rows": ["ATTR-09", "PRIV-05", "PRIV-06"], + "testFile": "libraries/react-native/ios-tests/AppleIdentifierPolicyTests.swift", + "testCase": "AppleIdentifierPolicyTests" + }, + { + "rows": ["ATTR-10", "ATTR-11"], + "testFile": "packages/core/test/domain/measurement/ApplePostback.test.ts", + "testCase": "describe(\"Apple postback ingest domain\"" + }, + { + "rows": ["LINK-01", "LINK-09"], + "testFile": "libraries/react-native/ios-tests/LinkCollectorTests.swift", + "testCase": "testLinksAreEncryptedOrderedAndDuplicateCallbacksAreSuppressed" + }, + { + "rows": ["PUSH-03"], + "testFile": "packages/core/test/domain/measurement/UninstallInference.test.ts", + "testCase": "describe(\"uninstall inference\"" + }, + { + "rows": ["PRIV-01", "PRIV-02"], + "testFile": "packages/core/test/domain/measurement/PartnerPostback.test.ts", + "testCase": "evaluates policy at send time and anonymizes identity" + }, + { + "rows": ["PRIV-04", "PRIV-07", "PRIV-08", "PRIV-11", "CONS-01", "CONS-02"], + "testFile": "libraries/react-native/tests/core/measurement-policy.test.ts", + "testCase": "describe(\"measurement policy\"" + }, + { + "rows": ["PC-04", "CFG-01", "CFG-02", "CFG-03", "CFG-04", "CFG-05"], + "testFile": "libraries/react-native/tests/core/expo-plugin-validation.test.ts", + "testCase": "describe(\"Voidhash Expo plugin validation\"" + }, + { + "rows": ["DIAG-04"], + "testFile": "libraries/react-native/tests/core/measurement-hardening.test.ts", + "testCase": "describe(\"measurement hardening matrix\"" + } + ] +} diff --git a/docs/react-native-measurement/privacy-guide.md b/docs/react-native-measurement/privacy-guide.md new file mode 100644 index 000000000..ccb73cdca --- /dev/null +++ b/docs/react-native-measurement/privacy-guide.md @@ -0,0 +1,11 @@ +# React Native measurement privacy guide + +Collection and upload are separate controls. `collectionOptOut` prevents new collection; upload pause retains already durable evidence. Partner sharing is re-evaluated at send time. A deletion request is written durably before protected local values are purged and the server deletion endpoint is called. + +Consent revisions must increase. Effective precedence is: collection opt-out, explicit collection policy, category consent (`adStorage` for advertising identifiers and `dataUsage` for vendor identifiers/protected identity), then partner exclusions. TCF/DMA fields are evidence inputs and do not override a stricter application or system decision. ATT is observed only; the SDK never prompts outside the application-controlled permission path. IDFA is read only with authorized ATT and an allowed advertising policy. The strict-no-IDFA build configuration makes the collector unavailable. + +Raw URLs/referrers, push tokens/payloads, receipts/JWS/purchase tokens, advertising and vendor identifiers, email, phone, and diagnostic authorizations are protected fields. They are encrypted in the native vault and ordinary records contain only opaque references. The public-property classifier rejects protected key names and URL/email-shaped values. Support bundles hash installation/session IDs and omit endpoint origins, event properties, and protected values. + +Location is denied by default and is manual-only when enabled. Email/phone and advertising identifiers require product/legal approval before enabling. Configure iOS privacy-manifest declarations and Play Data safety answers from the enabled capability manifest; include collection purpose, retention, linking, sharing, deletion, ATT/AD_ID behavior, and every optional store provider actually shipped. + +Production builds must use production purchase validation. Sandbox validation in a release build is rejected. Self-hosted endpoints require HTTPS except an explicit debug-only localhost policy, and signed remote configuration requires a project binding and trusted rotating public keys. diff --git a/docs/react-native-measurement/release-gate.md b/docs/react-native-measurement/release-gate.md new file mode 100644 index 000000000..74d0d3896 --- /dev/null +++ b/docs/react-native-measurement/release-gate.md @@ -0,0 +1,26 @@ +# Measurement public-release gate + +Automated evidence: + +- `parity-test-map.json` accounts for every planned ledger row and the exact six-row `wont-do` set. +- `release-scenarios.json` enumerates all fourteen release scenarios. +- React Native unit/type tests, Nitro generation, plugin compilation, API/client/database/backend typechecks, core tests, Swift tests, and Android tests must pass from a clean checkout. +- Store disclosure inputs are generated from the same capability options as the native manifest. +- Documentation coverage compares canonical record and standard-event sources with the data dictionary. + +Required attached evidence before a public release: + +- iOS physical-device matrix and archive inspection: +- Android physical-device matrix and merged-manifest inspection: +- 72-hour offline soak per platform: +- Dedicated campaign/deferred-link run per platform: +- Sandbox and production purchase/store-notification reconciliation per platform: +- Real self-host deployment run per platform: +- App Store privacy and Google Play Data safety approval: +- Security/privacy review and accepted risks: +- Retention/deletion-SLA review: +- Legal approval or verified default-off status for location, email/phone, and advertising identifiers: +- Operations/support approval: +- Release owner and decision date: + +No blank item is an approval. Automated simulator/unit evidence cannot replace physical-device, store, campaign, legal, privacy, or organizational sign-off. diff --git a/docs/react-native-measurement/release-scenarios.json b/docs/react-native-measurement/release-scenarios.json new file mode 100644 index 000000000..8988c4188 --- /dev/null +++ b/docs/react-native-measurement/release-scenarios.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "scenarios": [ + { "id": 1, "name": "fresh install, conversion, and deep-link not found", "automatedCase": "creates install evidence first and starts only one automatic session" }, + { "id": 2, "name": "background deep link", "automatedCase": "normalizes allowlisted links, projects routes, emits once, and dedupes" }, + { "id": 3, "name": "foreground deep link", "automatedCase": "normalizes allowlisted links, projects routes, emits once, and dedupes" }, + { "id": 4, "name": "rich event serialization", "automatedCase": "captures identity and configuration per item instead of reading globals at flush" }, + { "id": 5, "name": "identity, currency, and context round trip", "automatedCase": "captures identity and configuration per item instead of reading globals at flush" }, + { "id": 6, "name": "stop and resume suppression", "automatedCase": "distinguishes upload pause from deletion and keeps the installation identity" }, + { "id": 7, "name": "offline first install and delayed referrer", "automatedCase": "retries an offline wrapped link and emits only the eventual result" }, + { "id": 8, "name": "consent-gated route before session", "automatedCase": "preserves consent tri-state values and snapshots revisions at capture time" }, + { "id": 9, "name": "click, install, deferred route, first event, and attribution", "automatedCase": "signs, records before redirect, stamps Android referrer, and resolves once" }, + { "id": 10, "name": "push open and re-engagement attribution", "automatedCase": "attributes push and deep-link re-engagement without creating an install decision" }, + { "id": 11, "name": "purchase and ad-revenue validation and dedupe", "automatedCase": "records purchase observation once with receipt material only in protected evidence" }, + { "id": 12, "name": "reinstall, update, backup restore, and device transfer", "automatedCase": "testDedupeAndInboxAreDurableAndIdempotent" }, + { "id": 13, "name": "store renewal and refund while app is closed", "automatedCase": "parks an early notification and converges on replay without duplicates" }, + { "id": 14, "name": "partner postback allow, deny, and deletion", "automatedCase": "evaluates policy at send time and anonymizes identity" } + ] +} diff --git a/docs/react-native-measurement/troubleshooting.md b/docs/react-native-measurement/troubleshooting.md new file mode 100644 index 000000000..21e24c09d --- /dev/null +++ b/docs/react-native-measurement/troubleshooting.md @@ -0,0 +1,18 @@ +# Measurement integration troubleshooting + +Run `npx voidhash-doctor` from the app root with a secret-free `voidhash.config.json` containing the plugin options. The command prints a redacted capability report and exits nonzero for required integration failures. + +- `VH_CFG_CONTRADICTION`: fix the configuration reported by Expo prebuild; doctor and plugin share the same validator. +- `VH_IOS_APS_ENTITLEMENT_MISSING`: add the push environment entitlement. +- `VH_ANDROID_GOOGLE_SERVICES_MISSING` / `VH_ANDROID_FCM_HOOK_MISSING`: add the Firebase file/plugin and native messaging subscriber. +- `VH_ANDROID_NO_BACKUP_UNVERIFIED`: keep measurement SQLite/install state under no-backup storage without replacing unrelated app backup rules. +- `VH_IOS_ASSOCIATED_DOMAINS_MISSING` / `VH_ANDROID_APP_LINK_MISSING`: configure matching native link declarations and verify the website association files. +- `VH_IOS_SKAN_ENDPOINT_MISSING`: add the HTTPS postback origin or explicitly disable Apple attribution. +- `VH_IOS_SKAN_PLIST_MISSING` / `VH_IOS_ADATTRIBUTIONKIT_PLIST_MISSING`: regenerate the built Info.plist with both configured copy endpoints. +- `VH_ANDROID_APP_LINK_UNVERIFIED`: enable `android:autoVerify` and publish a matching Digital Asset Links file. + +Use `measurement.getState()` for local readiness, collector states, signed-config version, and outbox counts. Generate `measurement.createSupportBundle()` only with user/operator consent. A persistent outbox usually indicates offline state, a retryable 429/5xx, or protected-evidence upload pending. Quarantine indicates a permanent item failure such as an oversized record. Raw links, identifiers, tokens, and receipts never belong in logs or support tickets. + +Before release, verify direct/background/foreground/deferred links, offline install referrer, consent-gated start, push delivery/open, purchase/ad-revenue dedupe, reinstall/backup behavior, server renewal/refund correlation, partner allow/deny, and deletion on physical iOS and Android devices. Simulator smoke tests do not certify ATT/IDFA, referrer, store validation, push invalid-token uninstall inference, or real campaign correlation. + +The executable parity references and fourteen-scenario catalog are checked in as `parity-test-map.json` and `release-scenarios.json` beside this guide. Physical-device results and organizational approvals must be attached to the release record; local unit tests never substitute for those gates. diff --git a/libraries/node/src/generated/grouped-client.ts b/libraries/node/src/generated/grouped-client.ts index 01a4784a3..a5cd1b846 100644 --- a/libraries/node/src/generated/grouped-client.ts +++ b/libraries/node/src/generated/grouped-client.ts @@ -2,32 +2,32 @@ import type { VoidhashCoreClient } from "@voidhash/generated-clients"; export const groupCoreClient = (client: VoidhashCoreClient) => ({ apiKeys: { - createSecretKey: (request: { - payload: Parameters[0]; - }) => client.apiKeysCreateSecretKey(request.payload), - deleteApiKey: (request: { params: { readonly apiKeyId: string } }) => - client.apiKeysDeleteApiKey(request.params["apiKeyId"]), - getApiKeyById: (request: { params: { readonly apiKeyId: string } }) => - client.apiKeysGetApiKeyById(request.params["apiKeyId"]), + createSecretKey: (request: { payload: Parameters[0] }) => client.apiKeysCreateSecretKey(request.payload), + deleteApiKey: (request: { params: { readonly "apiKeyId": string } }) => client.apiKeysDeleteApiKey(request.params["apiKeyId"]), + getApiKeyById: (request: { params: { readonly "apiKeyId": string } }) => client.apiKeysGetApiKeyById(request.params["apiKeyId"]), listApiKeys: () => client.apiKeysListApiKeys(), - rotateSecretKey: (request: { params: { readonly apiKeyId: string } }) => - client.apiKeysRotateSecretKey(request.params["apiKeyId"]), + rotateSecretKey: (request: { params: { readonly "apiKeyId": string } }) => client.apiKeysRotateSecretKey(request.params["apiKeyId"]), }, auth: { session: () => client.authSession(), }, + notifications: { + sendNotification: (request: { payload: Parameters[0] }) => client.notificationsSendNotification(request.payload), + }, organizations: { - createOrganization: (request: { - payload: Parameters[0]; - }) => client.organizationsCreateOrganization(request.payload), + createOrganization: (request: { payload: Parameters[0] }) => client.organizationsCreateOrganization(request.payload), }, paymentProviderConfigurations: { - listPaymentProviderConfigurations: () => - client.paymentProviderConfigurationsListPaymentProviderConfigurations(), + listPaymentProviderConfigurations: () => client.paymentProviderConfigurationsListPaymentProviderConfigurations(), }, paymentProviderProducts: { listPaymentProviderProducts: () => client.paymentProviderProductsListPaymentProviderProducts(), }, + paywallDeploys: { + createDeploy: (request: { payload: Parameters[0] }) => client.paywallDeploysCreateDeploy(request.payload), + finalizeDeploy: (request: { params: { readonly "deployId": string } }) => client.paywallDeploysFinalizeDeploy(request.params["deployId"]), + uploadBlob: (request: { params: Parameters[0]; payload: Parameters[1] }) => client.paywallDeploysUploadBlob(request.params, request.payload), + }, paywallLocations: { listPaywallLocations: () => client.paywallLocationsListPaywallLocations(), }, @@ -35,28 +35,20 @@ export const groupCoreClient = (client: VoidhashCoreClient) => ({ listPerks: () => client.perksListPerks(), }, persons: { - createPerson: (request: { - payload: Parameters[0]; - }) => client.personsCreatePerson(request.payload), - getPersonByDistinctId: (request: { params: { readonly distinctId: string } }) => - client.personsGetPersonByDistinctId(request.params["distinctId"]), - getPersonById: (request: { params: { readonly personId: string } }) => - client.personsGetPersonById(request.params["personId"]), + createPerson: (request: { payload: Parameters[0] }) => client.personsCreatePerson(request.payload), + getPersonByDistinctId: (request: { params: { readonly "distinctId": string } }) => client.personsGetPersonByDistinctId(request.params["distinctId"]), + getPersonById: (request: { params: { readonly "personId": string } }) => client.personsGetPersonById(request.params["personId"]), listPersons: () => client.personsListPersons(), }, productPerks: { - listProductPerksByProductId: (request: { params: { readonly productId: string } }) => - client.productPerksListProductPerksByProductId(request.params["productId"]), + listProductPerksByProductId: (request: { params: { readonly "productId": string } }) => client.productPerksListProductPerksByProductId(request.params["productId"]), }, products: { listProducts: () => client.productsListProducts(), }, projects: { - createProject: (request: { - payload: Parameters[0]; - }) => client.projectsCreateProject(request.payload), - listProjects: (request: { params: { readonly organizationId: string } }) => - client.projectsListProjects(request.params["organizationId"]), + createProject: (request: { payload: Parameters[0] }) => client.projectsCreateProject(request.payload), + listProjects: (request: { params: { readonly "organizationId": string } }) => client.projectsListProjects(request.params["organizationId"]), }, schema: { getSchema: () => client.schemaGetSchema(), @@ -66,27 +58,16 @@ export const groupCoreClient = (client: VoidhashCoreClient) => ({ getUser: () => client.usersGetUser(), }, webhooks: { - createWebhookEndpoint: (request: { - payload: Parameters[0]; - }) => client.webhooksCreateWebhookEndpoint(request.payload), - deleteWebhookEndpoint: (request: { params: { readonly endpointId: string } }) => - client.webhooksDeleteWebhookEndpoint(request.params["endpointId"]), - getWebhookDelivery: (request: { params: { readonly deliveryId: string } }) => - client.webhooksGetWebhookDelivery(request.params["deliveryId"]), - getWebhookEndpoint: (request: { params: { readonly endpointId: string } }) => - client.webhooksGetWebhookEndpoint(request.params["endpointId"]), + createWebhookEndpoint: (request: { payload: Parameters[0] }) => client.webhooksCreateWebhookEndpoint(request.payload), + deleteWebhookEndpoint: (request: { params: { readonly "endpointId": string } }) => client.webhooksDeleteWebhookEndpoint(request.params["endpointId"]), + getWebhookDelivery: (request: { params: { readonly "deliveryId": string } }) => client.webhooksGetWebhookDelivery(request.params["deliveryId"]), + getWebhookEndpoint: (request: { params: { readonly "endpointId": string } }) => client.webhooksGetWebhookEndpoint(request.params["endpointId"]), listWebhookDeliveries: () => client.webhooksListWebhookDeliveries(), listWebhookEndpoints: () => client.webhooksListWebhookEndpoints(), - retryWebhookDelivery: (request: { params: { readonly deliveryId: string } }) => - client.webhooksRetryWebhookDelivery(request.params["deliveryId"]), - rotateWebhookSecret: (request: { params: { readonly endpointId: string } }) => - client.webhooksRotateWebhookSecret(request.params["endpointId"]), - testWebhookEndpoint: (request: { params: { readonly endpointId: string } }) => - client.webhooksTestWebhookEndpoint(request.params["endpointId"]), - updateWebhookEndpoint: (request: { - params: { readonly endpointId: string }; - payload: Parameters[1]; - }) => client.webhooksUpdateWebhookEndpoint(request.params["endpointId"], request.payload), + retryWebhookDelivery: (request: { params: { readonly "deliveryId": string } }) => client.webhooksRetryWebhookDelivery(request.params["deliveryId"]), + rotateWebhookSecret: (request: { params: { readonly "endpointId": string } }) => client.webhooksRotateWebhookSecret(request.params["endpointId"]), + testWebhookEndpoint: (request: { params: { readonly "endpointId": string } }) => client.webhooksTestWebhookEndpoint(request.params["endpointId"]), + updateWebhookEndpoint: (request: { params: { readonly "endpointId": string }; payload: Parameters[1] }) => client.webhooksUpdateWebhookEndpoint(request.params["endpointId"], request.payload), }, }); diff --git a/libraries/react-native/NitroVoidhash.podspec b/libraries/react-native/NitroVoidhash.podspec index a9ee359a0..83dce08e6 100644 --- a/libraries/react-native/NitroVoidhash.podspec +++ b/libraries/react-native/NitroVoidhash.podspec @@ -32,5 +32,6 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.libraries = 'sqlite3' install_modules_dependencies(s) end diff --git a/libraries/react-native/Package.swift b/libraries/react-native/Package.swift index 7ec625fec..068abe8d9 100644 --- a/libraries/react-native/Package.swift +++ b/libraries/react-native/Package.swift @@ -14,6 +14,8 @@ let package = Package( exclude: [ "HybridPaywallPresenter.swift", "HybridPaywallWebView.swift", + "HybridMeasurement.swift", + "HybridNotifications.swift", "HybridPurchasedItem.swift", "HybridStorekit.swift", "HybridStorekitProduct.swift", @@ -24,7 +26,21 @@ let package = Package( "HybridVoidhash.swift", "ProductStore.swift", ], - sources: ["TransactionRetentionStore.swift"] + sources: [ + "TransactionRetentionStore.swift", + "measurement/ConversionValueEngine.swift", + "measurement/AppleIdentifierPolicy.swift", + "measurement/AppleSystemIdentifiers.swift", + "measurement/MeasurementStore.swift", + "measurement/MeasurementDelivery.swift", + "measurement/LinkCollector.swift", + "measurement/PushCollector.swift", + ], + linkerSettings: [ + .linkedLibrary("sqlite3"), + .linkedFramework("Security"), + .linkedFramework("CryptoKit"), + ] ), .testTarget( name: "VoidhashPurchaseCoordinatorTests", diff --git a/libraries/react-native/README.md b/libraries/react-native/README.md index 810794856..17e1d729d 100644 --- a/libraries/react-native/README.md +++ b/libraries/react-native/README.md @@ -7,7 +7,7 @@ React Native SDK for in-app purchases. You can run the SDK in observer mode to coexist with other billing SDKs. ```ts -createVoidhashClient("pk_test", schema, { +createVoidhashClient("pk_test", { readOnly: true, scheme: "myapp", }); @@ -33,7 +33,7 @@ Observer reconciliation: For early-alpha integrations, you can enable unstable side-effect error swallowing: ```ts -createVoidhashClient("pk_test", schema, { +createVoidhashClient("pk_test", { readOnly: true, scheme: "myapp", unstable_swallowErrors: true, @@ -66,7 +66,7 @@ This flag is intentionally unstable and best used for background/observer-style Enable verbose HTTP logging when debugging request/response flow: ```ts -createVoidhashClient("pk_test", schema, { +createVoidhashClient("pk_test", { debug: true, scheme: "myapp", }); @@ -78,6 +78,9 @@ When enabled, the SDK logs: - Incoming response status, headers, and request duration. - HTTP/client errors with reason and status when available. +Release builds keep measurement diagnostic logging off unless a project-bound, signed, unexpired +support session is active. Diagnostic fields remain redacted even during an authorized session. + ## Product analytics capture Use `client.capture(...)` to send product analytics events: @@ -105,16 +108,131 @@ await voidhash.client.flush(); ### Ingest URL configuration -By default, ingest URL is derived from `baseUrl` by prefixing host with `i.` and posting to `/v1/events`. +Cloud endpoints are used by default. Self-hosted deployments configure explicit origins; paths, +credentials, query strings, and fragments are rejected. HTTP is accepted only when both `debug` +and `allowInsecureDebugTransport` are enabled. For local development with ingest on a different host/port, pass `ingestUrl`: ```ts -createVoidhashClient("pk_test", schema, { - baseUrl: "http://localhost:5001", - ingestUrl: "http://localhost:8083", +createVoidhashClient("pk_test", { + debug: true, + endpoints: { + api: "http://localhost:5001", + ingest: "http://localhost:8083", + links: "http://localhost:8090", + allowInsecureDebugTransport: true, + }, + scheme: "myapp", +}); +``` + +Only trusted configuration key IDs are exposed through diagnostics; public key material is never +included in `measurement.getState()`. + +## Unified measurement API + +The SDK has four namespaces with one shared identity, consent revision, session, and durable record +sequence: + +```ts +const voidhash = createVoidhashClient("pk_test", { scheme: "myapp", + consent: { + revision: 1, + decidedAt: new Date().toISOString(), + source: "application", + dataUsage: true, + }, + measurement: { + defaultCurrency: "USD", + context: { releaseChannel: "production" }, + purchases: { + enabled: true, + enrichment: { + ios: (transaction) => ({ source: "storekit", product: transaction.productId }), + android: { + subscription: (transaction) => ({ source: "billing", product: transaction.productId }), + }, + }, + }, + }, + links: { + allowedDomains: ["links.example.com"], + allowedSchemes: ["https", "myapp"], + }, + notifications: { registration: "manual" }, }); + +await voidhash.client.measurement.start(); +await voidhash.client.consent.set({ + revision: 2, + decidedAt: new Date().toISOString(), + source: "application", + partnerSharingOptOut: true, +}); + +const unsubscribe = voidhash.client.links.on("deepLink", (result) => { + if (result.status === "found") routeTo(result.route.value); +}); + +await voidhash.client.notifications.requestPermission(); +await voidhash.client.notifications.register(); +const state = await voidhash.client.measurement.getState(); +unsubscribe(); +``` + +Configuration and input failures are typed `MeasurementError` subclasses. Missing native +capabilities reject with `MeasurementCapabilityUnavailable`; methods never pretend success. + +### Privacy and protected fields + +Every record snapshots identity, consent, session, app, and device state when it is captured. +Changing identity or consent later does not rewrite queued records. Raw URLs, push tokens, receipts, +advertising identifiers, email, and phone are rejected from public event properties and context. +Link and notification payloads are referenced as protected evidence and do not appear in delivery +diagnostics or `measurement.getState()`. + +Location is denied by default and is accepted only as an explicit manual measurement input after +enabling the location collection policy. Advertising and vendor identifiers remain unavailable when +their collectors are not present or policy disables them. + +### Expo native configuration + +The package's Expo plugin configures associated domains, URL schemes, App Links, push entitlement +and permission policy, background notification mode, the default Android notification channel, and +an embedded redacted capability manifest. Invalid domains, schemes, paths, or incomplete Android +push configuration fail prebuild. + +```json +{ + "plugins": [ + [ + "@voidhash/react-native", + { + "measurement": { + "ios": { + "associatedDomains": ["links.example.com"], + "urlSchemes": ["myapp"] + }, + "android": { + "appLinks": [{ "host": "links.example.com", "autoVerify": true }], + "urlSchemes": ["myapp"] + } + }, + "notifications": { + "enabled": true, + "ios": { "apsEnvironment": "production", "backgroundRemoteNotifications": true }, + "android": { + "googleServicesFile": "./google-services.json", + "postNotifications": "include", + "defaultChannel": { "id": "updates", "name": "Updates", "importance": "high" } + } + } + } + ] + ] +} ``` ## Native paywall preloading + presentation diff --git a/libraries/react-native/android/build.gradle b/libraries/react-native/android/build.gradle index cfe43fd7e..ebe8e01ae 100644 --- a/libraries/react-native/android/build.gradle +++ b/libraries/react-native/android/build.gradle @@ -105,6 +105,10 @@ android { disable "GradleCompatible" } + testOptions { + unitTests.includeAndroidResources = true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -139,4 +143,11 @@ dependencies { implementation "androidx.webkit:webkit:1.12.1" implementation 'com.android.billingclient:billing-ktx:8.0.0' implementation 'com.google.android.gms:play-services-base:18.7.2' + implementation 'com.android.installreferrer:installreferrer:2.2' + implementation platform('com.google.firebase:firebase-bom:34.15.0') + implementation 'com.google.firebase:firebase-messaging' + implementation 'androidx.core:core-ktx:1.16.0' + testImplementation 'androidx.test:core:1.6.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.16.1' } diff --git a/libraries/react-native/android/src/main/AndroidManifest.xml b/libraries/react-native/android/src/main/AndroidManifest.xml index af1adc22a..b8f8aeac3 100644 --- a/libraries/react-native/android/src/main/AndroidManifest.xml +++ b/libraries/react-native/android/src/main/AndroidManifest.xml @@ -13,6 +13,13 @@ + + + + + Unit>() + private var readiness = "uninitialized" + private var consentRevision = 0.0 + private var configurationRevision = 0.0 + private var publishableKey: String? = null + private var ingestOrigin: String? = null + private val installReferrerStarted = AtomicBoolean(false) + private var appliedConfigurationVersion = 0L + + private val store: MeasurementStore by lazy { + val context = NitroModules.applicationContext + ?: throw IllegalStateException("NITRO_CONTEXT_UNAVAILABLE") + MeasurementStore(context) + } + + private fun snapshot(): MeasurementStateBridge { + val state = store.snapshot() + return MeasurementStateBridge( + installationId = state.installationId, + firstOpenedAt = state.firstOpenedAt, + installationSequence = state.sequence.toDouble(), + readiness = readiness, + currentSessionId = null, + currentSessionSequence = null, + consentRevision = consentRevision, + configurationRevision = configurationRevision, + outboxCritical = (state.counts["critical"] ?: 0).toDouble(), + outboxHigh = (state.counts["high"] ?: 0).toDouble(), + outboxNormal = (state.counts["normal"] ?: 0).toDouble(), + outboxLow = (state.counts["low"] ?: 0).toDouble(), + oldestRecordAgeMs = state.oldestQueuedAtMs?.let { (System.currentTimeMillis() - it).coerceAtLeast(0).toDouble() }, + ) + } + + override fun initialize(publishableKey: String, configuration: MeasurementInitializeConfiguration): Promise = Promise.async { + require(publishableKey.isNotBlank()) { "INVALID_PUBLISHABLE_KEY" } + store.snapshot() + this.publishableKey = publishableKey + ingestOrigin = configuration.ingestUrl + configurationRevision += 1 + readiness = "sdkReady" + if (installReferrerStarted.compareAndSet(false, true)) { + val context = NitroModules.applicationContext + ?: throw IllegalStateException("NITRO_CONTEXT_UNAVAILABLE") + suspend { + InstallReferrerCoordinator( + store, + GooglePlayInstallReferrerProvider(context), + ).collectOnce() + }.startCoroutine(object : Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) = Unit + }) + } + snapshot() + } + + override fun enqueue(command: MeasurementCommand): Promise = Promise.async { + val buffer = command.publicPayload.getBuffer(true) + buffer.rewind() + val publicPayload = ByteArray(buffer.remaining()) + buffer.get(publicPayload) + val sequence = store.enqueue( + recordId = command.commandId, + recordType = command.recordType, + occurredAt = command.occurredAt, + priority = command.priority.name, + source = command.source.name, + publicPayload = publicPayload.toString(Charsets.UTF_8), + protectedPayloadRef = command.protectedEvidenceRef, + ) + command.consent?.let { consentRevision = it.revision } + MeasurementCommandResult(true, command.commandId, sequence.toDouble(), null) + } + + override fun flush(): Promise = Promise.async { + val key = publishableKey + val origin = ingestOrigin + if (key == null || origin.isNullOrBlank()) { + val scheduled = store.peekEligible(Int.MAX_VALUE).size + MeasurementFlushBridgeResult(0.0, scheduled.toDouble(), 0.0, 0.0) + } else { + val result = MeasurementDelivery(store, key, origin).flush() + MeasurementFlushBridgeResult( + result.accepted.toDouble(), + result.scheduled.toDouble(), + result.quarantined.toDouble(), + result.policyBlocked.toDouble(), + ) + } + } + + override fun getInstallationId(): Promise = Promise.async { + store.snapshot().installationId + } + + override fun getState(): Promise = Promise.async { snapshot() } + + override fun subscribe(subscriptionId: String, listener: (MeasurementBridgeEvent) -> Unit) { + listeners[subscriptionId] = listener + } + + override fun unsubscribe(subscriptionId: String) { + listeners.remove(subscriptionId) + } + + override fun peekInbox(limit: Double): Promise> = + Promise.async { + store.peekInbox(limit.toInt()).map { + MeasurementInboxEntry(it.id, it.kind, it.source, it.appState, it.receivedAt, it.protectedPayloadRef) + }.toTypedArray() + } + + override fun acknowledgeInbox(entryId: String): Promise = Promise.async { + store.acknowledgeInbox(entryId) + } + + override fun readProtectedEvidence(blobId: String): Promise = Promise.async { + val evidence = store.getProtectedEvidence(blobId) + ?: throw IllegalStateException("PROTECTED_EVIDENCE_NOT_FOUND") + ArrayBuffer.copy(ByteBuffer.wrap(evidence.value)) + } + + override fun putProtectedEvidence(input: MeasurementProtectedEvidenceInput): Promise = Promise.async { + val buffer = input.value.getBuffer(true) + buffer.rewind() + val value = ByteArray(buffer.remaining()) + buffer.get(value) + store.putProtectedEvidence( + blobId = input.blobId, + purpose = input.purpose.name.lowercase().replace('_', '-'), + consentRevision = input.consentRevision.toLong(), + retentionClass = input.retentionClass.name.lowercase().replace('_', '-'), + value = value, + ) + } + + override fun deleteProtectedEvidence(blobId: String): Promise = Promise.async { + store.deleteProtectedEvidence(blobId) + } + + override fun deleteProtectedData(requestId: String): Promise = Promise.async { + store.deleteProtectedData(requestId) + } + + override fun getMeasurementConfigurationState(): Promise = Promise.async { + val state = store.measurementConfigurationState() + MeasurementConfigurationStateBridge( + state.version.toDouble(), + state.payload?.let { ArrayBuffer.copy(ByteBuffer.wrap(it)) }, + ) + } + + override fun persistMeasurementConfigurationState(version: Double, payload: ArrayBuffer): Promise = Promise.async { + val buffer = payload.getBuffer(true) + buffer.rewind() + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + store.persistMeasurementConfiguration(version.toLong(), bytes) + } + + override fun applyMeasurementConfiguration(version: Double, payload: ArrayBuffer): Promise = Promise.async { + require(version.toLong() > appliedConfigurationVersion) { "MEASUREMENT_CONFIGURATION_VERSION_REPLAY" } + val buffer = payload.getBuffer(true) + buffer.rewind() + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + val decoded = JSONObject(bytes.toString(Charsets.UTF_8)) + require(decoded.optInt("schemaVersion") == 1) { "MEASUREMENT_CONFIGURATION_INVALID" } + appliedConfigurationVersion = version.toLong() + } + + override fun applyMeasurementStorageLimits( + maxOutboxRecords: Double, + maxOutboxBytes: Double, + maxProtectedBytes: Double, + ): Promise = Promise.async { + store.applyStorageLimits( + maxOutboxRecords.toInt(), + maxOutboxBytes.toLong(), + maxProtectedBytes.toLong(), + ) + } + + override fun getPushRegistrationState(): Promise = Promise.async { + val state = store.pushRegistrationState() + MeasurementConfigurationStateBridge( + state.version.toDouble(), + state.payload?.let { ArrayBuffer.copy(ByteBuffer.wrap(it)) }, + ) + } + + override fun persistPushRegistrationState(payload: ArrayBuffer): Promise = Promise.async { + val buffer = payload.getBuffer(true) + buffer.rewind() + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + store.persistPushRegistration(bytes) + } + + override fun clearPushRegistrationState(): Promise = Promise.async { + store.clearPushRegistration() + } + + override fun getTestDeviceState(): Promise = Promise.async { store.testDeviceState() } + + override fun persistTestDeviceState(enabled: Boolean): Promise = Promise.async { + store.persistTestDeviceState(enabled) + } + + override fun checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Double): Promise = Promise.async { + store.checkAndSetDedupe(namespace, key, expiresAtMs.toLong()) + } + + override fun hasDedupe(namespace: String, key: String): Promise = Promise.async { + store.hasDedupe(namespace, key) + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/HybridNotifications.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/HybridNotifications.kt new file mode 100644 index 000000000..ad9bc9090 --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/HybridNotifications.kt @@ -0,0 +1,91 @@ +package com.margelo.nitro.voidhash + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise +import java.util.concurrent.ConcurrentHashMap +import com.margelo.nitro.voidhash.measurement.VoidhashPushCollector + +class HybridNotifications : HybridNotificationsSpec() { + private val listeners = ConcurrentHashMap Unit>() + private val collectorSubscriptionId = "hybrid-${System.identityHashCode(this)}" + + init { + VoidhashPushCollector.subscribe(collectorSubscriptionId) { event -> + val kind = when (event.kind) { + "received" -> NativeNotificationEventKind.RECEIVED + "opened" -> NativeNotificationEventKind.OPENED + "tokenChanged" -> NativeNotificationEventKind.TOKENCHANGED + else -> NativeNotificationEventKind.REGISTRATIONERROR + } + val bridged = NativeNotificationEvent( + event.id, + kind, + event.occurredAt, + event.protectedPayloadRef, + event.pushNotificationSendId, + event.link, + event.errorCode, + ) + listeners.values.forEach { it(bridged) } + } + } + + override fun getPermissionStatus(): Promise = Promise.async { + val context = NitroModules.applicationContext + ?: throw IllegalStateException("NITRO_CONTEXT_UNAVAILABLE") + if (Build.VERSION.SDK_INT < 33) "notRequired" + else if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) "authorized" + else if (context.getSharedPreferences("voidhash-notifications", 0).getBoolean("permission-requested", false)) "denied" + else "notDetermined" + } + + override fun requestPermission(provisional: Boolean): Promise = Promise.async { + if (Build.VERSION.SDK_INT < 33) return@async "notRequired" + val context = NitroModules.applicationContext + ?: throw IllegalStateException("NITRO_CONTEXT_UNAVAILABLE") + if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + return@async "authorized" + } + val activity = context.currentActivity + ?: throw IllegalStateException("PERMISSION_REQUEST_REQUIRES_ACTIVITY") + context.getSharedPreferences("voidhash-notifications", 0) + .edit().putBoolean("permission-requested", true).apply() + activity.runOnUiThread { + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 0x5648, + ) + } + repeat(300) { + if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + return@async "authorized" + } + Thread.sleep(100) + } + "denied" + } + + override fun getToken(): Promise = Promise.async { + val observed = VoidhashPushCollector.currentToken() + ?: throw IllegalStateException("PUSH_TOKEN_NOT_OBSERVED") + NativePushToken(observed.token, NativePushProvider.FCM, NativePushEnvironment.PRODUCTION) + } + + override fun setBadgeCount(count: Double): Promise = Promise.async { + require(count >= 0 && count % 1.0 == 0.0) { "INVALID_BADGE_COUNT" } + throw IllegalStateException("BADGE_COUNT_UNSUPPORTED_BY_ANDROID") + } + + override fun subscribe(subscriptionId: String, listener: (NativeNotificationEvent) -> Unit) { + listeners[subscriptionId] = listener + } + + override fun unsubscribe(subscriptionId: String) { + listeners.remove(subscriptionId) + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/NitroVoidhashPackage.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/NitroVoidhashPackage.kt index 9cdf971af..53c99ab35 100644 --- a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/NitroVoidhashPackage.kt +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/NitroVoidhashPackage.kt @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.margelo.nitro.voidhash.views.HybridPaywallWebViewManager +import com.margelo.nitro.voidhash.measurement.VoidhashLinkCollector class NitroVoidhashPackage : BaseReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { @@ -17,6 +18,7 @@ class NitroVoidhashPackage : BaseReactPackage() { } override fun createViewManagers(reactContext: ReactApplicationContext): List> { + VoidhashLinkCollector.install(reactContext) val viewManagers = ArrayList>() viewManagers.add(HybridPaywallWebViewManager()) return viewManagers diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/IdentifierCollector.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/IdentifierCollector.kt new file mode 100644 index 000000000..3a546120a --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/IdentifierCollector.kt @@ -0,0 +1,92 @@ +package com.margelo.nitro.voidhash.measurement + +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.UUID +import org.json.JSONObject + +internal enum class IdentifierKind(val wireValue: String) { + ADVERTISING_ID("gaid"), + APP_SET_ID("appSetId"), + OAID("oaid"), + AMAZON_AAID("aaid"), + META_ATTRIBUTION_ID("metaAttributionId"), +} + +internal data class IdentifierCollectionPolicy( + val advertisingIdentifiers: Boolean, + val vendorIdentifiers: Boolean, + val collectionOptOut: Boolean, +) + +internal data class IdentifierProviderResult( + val value: String?, + val limited: Boolean = false, + val capability: InstallReferrerOutcome = InstallReferrerOutcome.COLLECTED, +) + +internal fun interface IdentifierProvider { + fun read(): IdentifierProviderResult +} + +internal class AndroidIdentifierCollector( + private val store: MeasurementStore, + private val providers: Map, +) { + fun collect(kind: IdentifierKind, policy: IdentifierCollectionPolicy): InstallReferrerOutcome { + val allowed = !policy.collectionOptOut && when (kind) { + IdentifierKind.APP_SET_ID -> policy.vendorIdentifiers + else -> policy.advertisingIdentifiers + } + if (!allowed) { + record(kind, "permissionDenied", null, false) + return InstallReferrerOutcome.PERMISSION_DENIED + } + val provider = providers[kind] + if (provider == null) { + record(kind, "notInstalled", null, false) + return InstallReferrerOutcome.NOT_INSTALLED + } + val result = try { + provider.read() + } catch (_: SecurityException) { + IdentifierProviderResult(null, capability = InstallReferrerOutcome.PERMISSION_DENIED) + } catch (_: Throwable) { + IdentifierProviderResult(null, capability = InstallReferrerOutcome.UNSUPPORTED) + } + val reference = result.value?.takeIf { it.isNotBlank() && !result.limited }?.let { value -> + store.putProtectedEvidence( + purpose = "advertising-identifier", + consentRevision = 0, + retentionClass = "installation", + value = value.toByteArray(Charsets.UTF_8), + ) + } + record(kind, result.capability.wireValue, reference, result.limited) + return result.capability + } + + private fun record(kind: IdentifierKind, outcome: String, reference: String?, limited: Boolean) { + val installation = store.snapshot() + val occurredAt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(Date()) + val payload = JSONObject().apply { + put("kind", kind.wireValue) + put("outcome", outcome) + put("limited", limited) + put("policyBasis", if (outcome == "permissionDenied") "denied" else "configured") + }.toString() + store.enqueue( + recordId = "identifier_${installation.installationId}_${kind.wireValue}_${UUID.randomUUID()}", + recordType = "identifier.observed.v1", + occurredAt = occurredAt, + priority = "high", + source = "native", + publicPayload = payload, + protectedPayloadRef = reference, + ) + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/InstallReferrerProvider.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/InstallReferrerProvider.kt new file mode 100644 index 000000000..9e9f38908 --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/InstallReferrerProvider.kt @@ -0,0 +1,241 @@ +package com.margelo.nitro.voidhash.measurement + +import android.content.Context +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import java.util.Timer +import java.util.TimerTask +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.text.SimpleDateFormat +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import org.json.JSONObject + +internal enum class StoreKind { + GOOGLE_PLAY, + SAMSUNG_GALAXY_STORE, + HUAWEI_APP_GALLERY, + XIAOMI_GET_APPS, + META, + PREINSTALL, + OUT_OF_STORE, +} + +internal enum class InstallReferrerOutcome(val wireValue: String) { + AVAILABLE("available"), + COLLECTED("collected"), + NOT_INSTALLED("notInstalled"), + UNSUPPORTED("unsupported"), + TIMEOUT("timeout"), + PERMISSION_DENIED("permissionDenied"), + INVALID_SIGNATURE("invalidSignature"), +} + +internal data class InstallReferrerEvidence( + val store: StoreKind, + val outcome: InstallReferrerOutcome, + val rawReferrer: String? = null, + val clickTimestampSeconds: Long? = null, + val installTimestampSeconds: Long? = null, + val clickServerTimestampSeconds: Long? = null, + val installServerTimestampSeconds: Long? = null, + val instantExperience: Boolean? = null, + val installVersion: String? = null, + val responseCode: Int? = null, + val libraryVersion: String = "2.2", + val verificationState: String = "unverified", +) + +internal interface InstallReferrerProvider { + val store: StoreKind + suspend fun collect(): InstallReferrerEvidence +} + +internal class ConfiguredInstallReferrerProvider( + override val store: StoreKind, + private val referrer: String?, + private val verificationState: String = "configured", +) : InstallReferrerProvider { + override suspend fun collect(): InstallReferrerEvidence = + if (referrer.isNullOrBlank()) { + InstallReferrerEvidence(store, InstallReferrerOutcome.NOT_INSTALLED) + } else { + InstallReferrerEvidence( + store = store, + outcome = InstallReferrerOutcome.COLLECTED, + rawReferrer = referrer, + verificationState = verificationState, + ) + } +} + +internal class OptionalDependencyInstallReferrerProvider( + override val store: StoreKind, + private val dependencyClassName: String, + private val collector: suspend () -> InstallReferrerEvidence, +) : InstallReferrerProvider { + override suspend fun collect(): InstallReferrerEvidence { + try { + Class.forName(dependencyClassName, false, javaClass.classLoader) + } catch (_: SecurityException) { + return InstallReferrerEvidence(store, InstallReferrerOutcome.PERMISSION_DENIED) + } catch (_: ClassNotFoundException) { + return InstallReferrerEvidence(store, InstallReferrerOutcome.NOT_INSTALLED) + } catch (_: LinkageError) { + return InstallReferrerEvidence(store, InstallReferrerOutcome.NOT_INSTALLED) + } catch (_: Throwable) { + return InstallReferrerEvidence(store, InstallReferrerOutcome.UNSUPPORTED) + } + return try { + collector() + } catch (_: SecurityException) { + InstallReferrerEvidence(store, InstallReferrerOutcome.PERMISSION_DENIED) + } catch (_: Throwable) { + InstallReferrerEvidence(store, InstallReferrerOutcome.UNSUPPORTED) + } + } +} + +internal fun classifyPlayInstallReferrerResponse(responseCode: Int): InstallReferrerOutcome = + when (responseCode) { + InstallReferrerClient.InstallReferrerResponse.OK -> InstallReferrerOutcome.COLLECTED + InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> InstallReferrerOutcome.UNSUPPORTED + InstallReferrerClient.InstallReferrerResponse.PERMISSION_ERROR -> InstallReferrerOutcome.PERMISSION_DENIED + InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> InstallReferrerOutcome.NOT_INSTALLED + InstallReferrerClient.InstallReferrerResponse.SERVICE_DISCONNECTED -> InstallReferrerOutcome.AVAILABLE + else -> InstallReferrerOutcome.UNSUPPORTED + } + +internal class GooglePlayInstallReferrerProvider( + context: Context, + private val timeoutMs: Long = 5_000, + private val maximumTransientRetries: Int = 2, +) : InstallReferrerProvider { + override val store = StoreKind.GOOGLE_PLAY + private val appContext = context.applicationContext + + override suspend fun collect(): InstallReferrerEvidence = suspendCoroutine { continuation -> + val completed = AtomicBoolean(false) + val timer = Timer("voidhash-install-referrer-timeout", true) + var retries = 0 + var activeClient: InstallReferrerClient? = null + + fun finish(evidence: InstallReferrerEvidence) { + if (!completed.compareAndSet(false, true)) return + timer.cancel() + activeClient?.endConnection() + continuation.resume(evidence) + } + + fun connect() { + val client = InstallReferrerClient.newBuilder(appContext).build() + activeClient = client + try { + client.startConnection(object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { + try { + val details = client.installReferrer + finish( + InstallReferrerEvidence( + store = store, + outcome = InstallReferrerOutcome.COLLECTED, + rawReferrer = details.installReferrer, + clickTimestampSeconds = details.referrerClickTimestampSeconds, + installTimestampSeconds = details.installBeginTimestampSeconds, + clickServerTimestampSeconds = details.referrerClickTimestampServerSeconds, + installServerTimestampSeconds = details.installBeginTimestampServerSeconds, + instantExperience = details.googlePlayInstantParam, + installVersion = details.installVersion, + responseCode = responseCode, + ), + ) + } catch (_: SecurityException) { + finish(InstallReferrerEvidence(store, InstallReferrerOutcome.PERMISSION_DENIED, responseCode = responseCode)) + } catch (_: Throwable) { + finish(InstallReferrerEvidence(store, InstallReferrerOutcome.UNSUPPORTED, responseCode = responseCode)) + } + return + } + val outcome = classifyPlayInstallReferrerResponse(responseCode) + if (outcome == InstallReferrerOutcome.AVAILABLE && retries < maximumTransientRetries) { + retries += 1 + client.endConnection() + connect() + } else { + finish(InstallReferrerEvidence(store, outcome, responseCode = responseCode)) + } + } + + override fun onInstallReferrerServiceDisconnected() { + if (retries < maximumTransientRetries) { + retries += 1 + connect() + } else { + finish(InstallReferrerEvidence(store, InstallReferrerOutcome.AVAILABLE)) + } + } + }) + } catch (_: SecurityException) { + finish(InstallReferrerEvidence(store, InstallReferrerOutcome.PERMISSION_DENIED)) + } catch (_: Throwable) { + finish(InstallReferrerEvidence(store, InstallReferrerOutcome.NOT_INSTALLED)) + } + } + + timer.schedule(object : TimerTask() { + override fun run() = finish(InstallReferrerEvidence(store, InstallReferrerOutcome.TIMEOUT)) + }, timeoutMs) + connect() + } +} + +internal class InstallReferrerCoordinator( + private val store: MeasurementStore, + private val provider: InstallReferrerProvider, +) { + suspend fun collectOnce() { + val installation = store.snapshot() + val key = "${provider.store.name.lowercase()}:${installation.installationId}" + if (store.hasDedupe("install-referrer", key)) return + val evidence = provider.collect() + val blobId = evidence.rawReferrer?.let { + store.putProtectedEvidence( + blobId = "referrer_${installation.installationId}_${provider.store.name.lowercase()}", + purpose = "install-referrer", + consentRevision = 0, + retentionClass = "installation", + value = it.toByteArray(Charsets.UTF_8), + ) + } + val payload = JSONObject().apply { + put("schemaVersion", 1) + put("store", provider.store.name.lowercase()) + put("outcome", evidence.outcome.wireValue) + put("clickTimestampSeconds", evidence.clickTimestampSeconds) + put("installTimestampSeconds", evidence.installTimestampSeconds) + put("clickServerTimestampSeconds", evidence.clickServerTimestampSeconds) + put("installServerTimestampSeconds", evidence.installServerTimestampSeconds) + put("instantExperience", evidence.instantExperience) + put("installVersion", evidence.installVersion) + put("responseCode", evidence.responseCode) + put("libraryVersion", evidence.libraryVersion) + put("verificationState", evidence.verificationState) + }.toString() + store.enqueue( + recordId = "install_referrer_${installation.installationId}_${provider.store.name.lowercase()}", + recordType = "android.install_referrer.v1", + occurredAt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(Date()), + priority = "critical", + source = "native", + publicPayload = payload, + protectedPayloadRef = blobId, + ) + store.checkAndSetDedupe("install-referrer", key, Long.MAX_VALUE) + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/LinkCollector.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/LinkCollector.kt new file mode 100644 index 000000000..ac0715e8e --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/LinkCollector.kt @@ -0,0 +1,92 @@ +package com.margelo.nitro.voidhash.measurement + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import android.os.Bundle +import java.security.MessageDigest +import java.time.Instant +import java.util.Collections +import java.util.UUID +import java.util.WeakHashMap +import java.util.concurrent.atomic.AtomicBoolean + +/** Captures Android deep-link intents into the encrypted, pre-JavaScript inbox. */ +object VoidhashLinkCollector { + private const val DEDUPE_WINDOW_MS = 30_000L + private val installed = AtomicBoolean(false) + private val observedIntents = Collections.newSetFromMap(WeakHashMap()) + + /** Installs lifecycle capture once for the process. */ + @JvmStatic + fun install(context: Context) { + val application = context.applicationContext as? Application ?: return + if (!installed.compareAndSet(false, true)) return + application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, state: Bundle?) { + captureIntent(activity, activity.intent, if (state == null) "cold" else "warm") + } + + override fun onActivityResumed(activity: Activity) { + captureIntent(activity, activity.intent, "foreground") + } + + override fun onActivityStarted(activity: Activity) = Unit + override fun onActivityPaused(activity: Activity) = Unit + override fun onActivityStopped(activity: Activity) = Unit + override fun onActivitySaveInstanceState(activity: Activity, state: Bundle) = Unit + override fun onActivityDestroyed(activity: Activity) = Unit + }) + } + + /** Captures an initial or `onNewIntent` link without requiring a resumed activity. */ + @JvmStatic + fun captureIntent(context: Context, intent: Intent?, appState: String = "warm"): Boolean { + intent ?: return false + synchronized(observedIntents) { + if (!observedIntents.add(intent)) return false + } + val raw = intent.dataString ?: return false + val source = if (intent.data?.scheme.equals("http", true) || intent.data?.scheme.equals("https", true)) { + "appLink" + } else { + "customScheme" + } + val store = MeasurementStore(context) + return try { + capture(store, raw, source, appState) + } finally { + store.close() + } + } + + internal fun capture( + store: MeasurementStore, + raw: String, + source: String, + appState: String, + nowMs: Long = System.currentTimeMillis(), + ): Boolean { + val digest = MessageDigest.getInstance("SHA-256").digest(raw.toByteArray()) + .joinToString("") { "%02x".format(it) } + if (!store.checkAndSetDedupe("native-link-capture", digest, nowMs + DEDUPE_WINDOW_MS)) return false + val blobId = "link-${UUID.randomUUID()}" + store.putProtectedEvidence( + blobId = blobId, + purpose = "link-capture", + consentRevision = 0, + retentionClass = "installation", + value = raw.toByteArray(), + ) + val entryId = "inbox-${UUID.randomUUID()}" + return store.appendInbox( + id = entryId, + kind = "link", + source = source, + appState = appState, + receivedAt = Instant.ofEpochMilli(nowMs).toString(), + protectedPayloadRef = blobId, + ) + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementDelivery.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementDelivery.kt new file mode 100644 index 000000000..8212dbd07 --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementDelivery.kt @@ -0,0 +1,295 @@ +package com.margelo.nitro.voidhash.measurement + +import android.util.Base64 +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import kotlin.math.min +import org.json.JSONArray +import org.json.JSONObject + +internal data class MeasurementDeliveryResult( + val accepted: Int, + val scheduled: Int, + val quarantined: Int, + val policyBlocked: Int, +) + +internal data class MeasurementHttpResult(val status: Int, val body: String, val retryAfterMs: Long?) + +internal fun interface MeasurementHttpTransport { + fun send(path: String, body: ByteArray): MeasurementHttpResult +} + +private class UrlConnectionMeasurementHttpTransport(private val ingestOrigin: String) : MeasurementHttpTransport { + override fun send(path: String, body: ByteArray): MeasurementHttpResult { + val connection = URL("${ingestOrigin.trimEnd('/')}$path").openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.connectTimeout = 10_000 + connection.readTimeout = 15_000 + connection.doOutput = true + connection.setRequestProperty("content-type", "application/json") + connection.setRequestProperty("accept", "application/json") + connection.setFixedLengthStreamingMode(body.size) + connection.outputStream.use { it.write(body) } + val status = connection.responseCode + val stream = if (status >= 400) connection.errorStream else connection.inputStream + val responseBody = stream?.bufferedReader()?.use { it.readText() }.orEmpty() + val retryAfter = connection.getHeaderField("retry-after")?.toLongOrNull()?.times(1_000) + connection.disconnect() + return MeasurementHttpResult(status, responseBody, retryAfter) + } +} + +internal class MeasurementDelivery( + private val store: MeasurementStore, + private val publishableKey: String, + ingestOrigin: String, + private val transport: MeasurementHttpTransport = UrlConnectionMeasurementHttpTransport(ingestOrigin), +) { + fun flush(): MeasurementDeliveryResult { + val records = store.peekEligible(100) + if (records.isEmpty()) return MeasurementDeliveryResult(0, 0, 0, 0) + val deletionRecords = records.filter { it.recordType == "measurement.deletion_requested.v1" } + val deletionResult = deletionRecords.fold(MeasurementDeliveryResult(0, 0, 0, 0)) { result, record -> + combine(result, deliverDeletion(record)) + } + val protected = prepareProtectedEvidence(records.filterNot { it.recordType == "measurement.deletion_requested.v1" }) + return combine(deletionResult, combine(protected.result, deliver(protected.ready))) + } + + private fun deliverDeletion(record: StoredOutboxRecord): MeasurementDeliveryResult { + val response = try { + sendDeletion(record) + } catch (_: Exception) { + schedule(listOf(record), null) + return MeasurementDeliveryResult(0, 1, 0, 0) + } + if (response.status == 429 || response.status >= 500) { + schedule(listOf(record), response.retryAfterMs) + return MeasurementDeliveryResult(0, 1, 0, 0) + } + if (response.status in 200..299) { + store.acknowledge(record.recordId) + return MeasurementDeliveryResult(1, 0, 0, 0) + } + store.reject(record.recordId, "deletion_http_${response.status}") + return MeasurementDeliveryResult(0, 0, 1, 0) + } + + private fun prepareProtectedEvidence(records: List): ProtectedPreparation { + val ready = mutableListOf() + var scheduled = 0 + var quarantined = 0 + val outcomes = mutableMapOf() + for (record in records) { + val reference = record.protectedPayloadRef + if (reference == null) { + ready += record + continue + } + val outcome = outcomes.getOrPut(reference) { uploadProtectedEvidence(reference) } + when (outcome) { + ProtectedOutcome.ACCEPTED -> ready += record + ProtectedOutcome.RETRY -> { + store.scheduleRetry(record.recordId, System.currentTimeMillis() + 1_000) + scheduled += 1 + } + ProtectedOutcome.REJECTED -> { + store.reject(record.recordId, "protected_evidence_rejected") + quarantined += 1 + } + } + } + return ProtectedPreparation( + ready, + MeasurementDeliveryResult(0, scheduled, quarantined, 0), + ) + } + + private fun uploadProtectedEvidence(blobId: String): ProtectedOutcome { + val evidence = store.getProtectedUpload(blobId) ?: return ProtectedOutcome.REJECTED + if (evidence.uploadState == "acknowledged") return ProtectedOutcome.ACCEPTED + if (evidence.uploadState != "pending" || evidence.deletionState != "active" || evidence.ciphertext == null) { + return ProtectedOutcome.REJECTED + } + val now = System.currentTimeMillis() + if (evidence.eligibleAtMs > now) return ProtectedOutcome.RETRY + val response = try { + sendProtected(evidence) + } catch (_: Exception) { + scheduleProtected(evidence, null) + return ProtectedOutcome.RETRY + } + if (response.status in 200..299) { + store.acknowledgeProtectedUpload(blobId) + return ProtectedOutcome.ACCEPTED + } + if (response.status == 429 || response.status >= 500) { + scheduleProtected(evidence, response.retryAfterMs) + return ProtectedOutcome.RETRY + } + store.rejectProtectedUpload(blobId) + return ProtectedOutcome.REJECTED + } + + private fun deliver(records: List): MeasurementDeliveryResult { + if (records.isEmpty()) return MeasurementDeliveryResult(0, 0, 0, 0) + val response = try { + send(records) + } catch (_: Exception) { + schedule(records, null) + return MeasurementDeliveryResult(0, records.size, 0, 0) + } + if (response.status == 413) { + if (records.size == 1) { + store.quarantine(records.first().recordId, "payload_too_large") + return MeasurementDeliveryResult(0, 0, 1, 0) + } + val middle = records.size / 2 + return combine(deliver(records.subList(0, middle)), deliver(records.subList(middle, records.size))) + } + if (response.status == 429 || response.status >= 500) { + schedule(records, response.retryAfterMs) + return MeasurementDeliveryResult(0, records.size, 0, 0) + } + if (response.status !in 200..299) { + records.forEach { store.reject(it.recordId, "http_${response.status}") } + return MeasurementDeliveryResult(0, 0, records.size, 0) + } + + val payload = try { + JSONObject(response.body) + } catch (_: Exception) { + schedule(records, null) + return MeasurementDeliveryResult(0, records.size, 0, 0) + } + val accepted = payload.optJSONArray("accepted") ?: JSONArray() + var acceptedCount = 0 + for (index in 0 until accepted.length()) { + if (store.acknowledge(accepted.getString(index))) acceptedCount += 1 + } + val rejected = payload.optJSONArray("rejected") ?: JSONArray() + var quarantined = 0 + for (index in 0 until rejected.length()) { + val item = rejected.getJSONObject(index) + if (store.reject(item.getString("recordId"), item.getString("reason"))) quarantined += 1 + } + val acknowledged = buildSet { + for (index in 0 until accepted.length()) add(accepted.getString(index)) + for (index in 0 until rejected.length()) add(rejected.getJSONObject(index).getString("recordId")) + } + val missing = records.filterNot { it.recordId in acknowledged } + schedule(missing, null) + return MeasurementDeliveryResult(acceptedCount, missing.size, quarantined, 0) + } + + private fun send(records: List): MeasurementHttpResult { + val body = JSONObject().apply { + put("token", publishableKey) + put("sent_at", isoNow()) + put("events", JSONArray(records.map(::captureEvent))) + }.toString().toByteArray(Charsets.UTF_8) + return transport.send("/i/v1/batch", body) + } + + private fun sendProtected(evidence: StoredProtectedUpload): MeasurementHttpResult { + val body = JSONObject().apply { + put("blobId", evidence.blobId) + put("ciphertext", Base64.encodeToString(evidence.ciphertext, Base64.NO_WRAP)) + put("consentRevision", evidence.consentRevision) + put("deletionState", evidence.deletionState) + put("encryptionKeyVersion", evidence.encryptionKeyVersion) + put("installationId", store.snapshot().installationId) + put("purpose", evidence.purpose) + put("retentionClass", evidence.retentionClass) + put("token", publishableKey) + }.toString().toByteArray(Charsets.UTF_8) + return transport.send("/i/v1/measurement/protected", body) + } + + private fun sendDeletion(record: StoredOutboxRecord): MeasurementHttpResult { + val envelope = JSONObject(record.publicPayload) + val payload = envelope.optJSONObject("publicPayload") ?: JSONObject() + val identity = envelope.optJSONObject("identity") ?: JSONObject() + val body = JSONObject().apply { + put("installationId", envelope.getString("installationId")) + identity.optString("personId").takeIf { it.isNotBlank() }?.let { put("personId", it) } + put("requestId", payload.optString("requestId", record.recordId)) + put("requestedAt", envelope.optString("occurredAt", isoNow())) + put("token", publishableKey) + }.toString().toByteArray(Charsets.UTF_8) + return transport.send("/i/v1/measurement/delete", body) + } + + private fun captureEvent(record: StoredOutboxRecord): JSONObject { + val envelope = JSONObject(record.publicPayload) + val identity = envelope.optJSONObject("identity") ?: JSONObject() + val consent = envelope.optJSONObject("consent") ?: JSONObject() + val session = envelope.optJSONObject("session") + return JSONObject().apply { + put("uuid", record.recordId) + put("event", record.recordType) + put("timestamp", envelope.optString("occurredAt", isoNow())) + put("distinct_id", identity.optString("distinctId", envelope.optString("installationId"))) + session?.optString("id")?.takeIf { it.isNotBlank() }?.let { put("session_id", it) } + put("properties", envelope.optJSONObject("publicPayload") ?: JSONObject()) + put("context", JSONObject().apply { + put("schemaVersion", 1) + put("installation", JSONObject().apply { + put("id", envelope.optString("installationId")) + put("sequence", envelope.optLong("installationSequence", record.sequence)) + }) + put("identity", identity) + put("consentRevision", consent.optLong("revision", 0)) + put("app", envelope.optJSONObject("app") ?: JSONObject()) + put("device", envelope.optJSONObject("device") ?: JSONObject()) + put("measurement", JSONObject().apply { + put("recordType", record.recordType) + put("source", envelope.optString("source")) + }) + }) + } + } + + private fun schedule(records: List, retryAfterMs: Long?) { + val now = System.currentTimeMillis() + for (record in records) { + val exponential = min(3_600_000L, 1_000L shl min(record.attemptCount, 12)) + val stableJitter = 800L + (record.recordId.hashCode().toLong().and(0x7fffffff) % 401L) + val computed = exponential * stableJitter / 1_000L + store.scheduleRetry(record.recordId, now + maxOf(computed, retryAfterMs ?: 0L)) + } + } + + private fun scheduleProtected(evidence: StoredProtectedUpload, retryAfterMs: Long?) { + val exponential = min(3_600_000L, 1_000L shl min(evidence.attemptCount, 12)) + val stableJitter = 800L + (evidence.blobId.hashCode().toLong().and(0x7fffffff) % 401L) + val computed = exponential * stableJitter / 1_000L + store.scheduleProtectedUpload( + evidence.blobId, + System.currentTimeMillis() + maxOf(computed, retryAfterMs ?: 0L), + ) + } + + private fun combine(left: MeasurementDeliveryResult, right: MeasurementDeliveryResult) = + MeasurementDeliveryResult( + left.accepted + right.accepted, + left.scheduled + right.scheduled, + left.quarantined + right.quarantined, + left.policyBlocked + right.policyBlocked, + ) + + private fun isoNow(): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(Date()) + + private data class ProtectedPreparation( + val ready: List, + val result: MeasurementDeliveryResult, + ) + private enum class ProtectedOutcome { ACCEPTED, RETRY, REJECTED } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementStore.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementStore.kt new file mode 100644 index 000000000..359142479 --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/MeasurementStore.kt @@ -0,0 +1,871 @@ +package com.margelo.nitro.voidhash.measurement + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.io.File +import java.security.KeyStore +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.UUID +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import org.json.JSONObject + +internal data class MeasurementStoreLimits( + val maxOutboxRecords: Int = 10_000, + val maxOutboxBytes: Long = 20L * 1024L * 1024L, + val maxProtectedBytes: Long = 20L * 1024L * 1024L, + val maxDedupeRecords: Int = 25_000, + val maxInboxRecords: Int = 1_000, +) + +internal data class StoredOutboxRecord( + val recordId: String, + val recordType: String, + val sequence: Long, + val priority: String, + val publicPayload: String, + val protectedPayloadRef: String?, + val attemptCount: Int, +) + +internal data class StoredProtectedEvidence( + val blobId: String, + val purpose: String, + val consentRevision: Long, + val retentionClass: String, + val encryptionKeyVersion: Int, + val deletionState: String, + val value: ByteArray, +) + +internal data class StoredProtectedUpload( + val blobId: String, + val purpose: String, + val consentRevision: Long, + val retentionClass: String, + val encryptionKeyVersion: Int, + val deletionState: String, + val ciphertext: ByteArray?, + val uploadState: String, + val attemptCount: Int, + val eligibleAtMs: Long, +) + +internal data class StoredInboxEntry( + val id: String, + val kind: String, + val source: String, + val appState: String, + val receivedAt: String, + val protectedPayloadRef: String, +) + +internal data class MeasurementStoreSnapshot( + val installationId: String, + val firstOpenedAt: String, + val sequence: Long, + val counts: Map, + val oldestQueuedAtMs: Long?, +) + +internal data class StoredMeasurementConfigurationState( + val version: Long, + val payload: ByteArray?, +) + +internal class MeasurementStore( + context: Context, + limits: MeasurementStoreLimits = MeasurementStoreLimits(), + cryptoFactory: () -> MeasurementCrypto = { MeasurementVaultCrypto() }, +) { + private val lock = Any() + private val databaseFile = File(context.noBackupFilesDir, "voidhash-measurement.sqlite") + private val crypto by lazy(cryptoFactory) + private val database: SQLiteDatabase + private var limits = limits + + init { + databaseFile.parentFile?.mkdirs() + database = SQLiteDatabase.openDatabase( + databaseFile.absolutePath, + null, + SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING, + ) + migrate() + ensureInstallation() + } + + fun databasePath(): String = databaseFile.absolutePath + + fun close() = synchronized(lock) { database.close() } + + fun snapshot(): MeasurementStoreSnapshot = synchronized(lock) { + ensureInstallation() + val installation = database.rawQuery( + "SELECT installation_id, first_opened_at, sequence FROM installation WHERE singleton = 1", + null, + ).use { cursor -> + check(cursor.moveToFirst()) + Triple(cursor.getString(0), cursor.getString(1), cursor.getLong(2)) + } + val counts = mutableMapOf() + database.rawQuery( + "SELECT priority, COUNT(*) FROM outbox WHERE acknowledgement_state = 'pending' GROUP BY priority", + null, + ).use { cursor -> + while (cursor.moveToNext()) counts[cursor.getString(0)] = cursor.getInt(1) + } + val oldest = database.rawQuery( + "SELECT MIN(queued_at_ms) FROM outbox WHERE acknowledgement_state = 'pending'", + null, + ).use { cursor -> + if (cursor.moveToFirst() && !cursor.isNull(0)) cursor.getLong(0) else null + } + MeasurementStoreSnapshot(installation.first, installation.second, installation.third, counts, oldest) + } + + fun enqueue( + recordId: String, + recordType: String, + occurredAt: String, + priority: String, + source: String, + publicPayload: String, + protectedPayloadRef: String?, + queuedAtMs: Long = System.currentTimeMillis(), + ): Long = synchronized(lock) { + database.beginTransaction() + try { + ensureInstallation() + existingSequence(recordId)?.let { + database.setTransactionSuccessful() + return@synchronized it + } + database.execSQL("UPDATE installation SET sequence = sequence + 1 WHERE singleton = 1") + val installation = database.rawQuery( + "SELECT installation_id, sequence FROM installation WHERE singleton = 1", + null, + ).use { cursor -> + cursor.moveToFirst() + cursor.getString(0) to cursor.getLong(1) + } + val sequence = installation.second + val storedPayload = canonicalEnvelope( + recordId = recordId, + recordType = recordType, + occurredAt = occurredAt, + queuedAtMs = queuedAtMs, + installationId = installation.first, + sequence = sequence, + source = source, + publicPayload = publicPayload, + protectedPayloadRef = protectedPayloadRef, + ) + evictFor(storedPayload.toByteArray(Charsets.UTF_8).size.toLong()) + database.insertOrThrow( + "outbox", + null, + ContentValues().apply { + put("record_id", recordId) + put("record_type", recordType) + put("installation_sequence", sequence) + put("occurred_at", occurredAt) + put("queued_at_ms", queuedAtMs) + put("priority", priority.lowercase(Locale.US)) + put("source", source.lowercase(Locale.US)) + put("public_payload", storedPayload) + put("public_payload_bytes", storedPayload.toByteArray(Charsets.UTF_8).size) + put("protected_payload_ref", protectedPayloadRef) + put("attempt_count", 0) + put("eligible_at_ms", queuedAtMs) + put("acknowledgement_state", "pending") + }, + ) + database.setTransactionSuccessful() + sequence + } finally { + database.endTransaction() + } + } + + fun peekEligible(limit: Int, nowMs: Long = System.currentTimeMillis()): List = + synchronized(lock) { + database.rawQuery( + """ + SELECT record_id, record_type, installation_sequence, priority, public_payload, + protected_payload_ref, attempt_count + FROM outbox + WHERE acknowledgement_state = 'pending' AND eligible_at_ms <= ? + ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END, + installation_sequence ASC + LIMIT ? + """.trimIndent(), + arrayOf(nowMs.toString(), limit.coerceAtLeast(0).toString()), + ).use { cursor -> cursor.mapRows(::outboxRecord) } + } + + fun acknowledge(recordId: String): Boolean = synchronized(lock) { + database.update( + "outbox", + ContentValues().apply { put("acknowledgement_state", "acknowledged") }, + "record_id = ? AND acknowledgement_state != 'acknowledged'", + arrayOf(recordId), + ) > 0 + } + + fun quarantine(recordId: String, reason: String): Boolean = synchronized(lock) { + database.beginTransaction() + try { + writeDiagnostic(recordId, "quarantined", reason) + val changed = database.update( + "outbox", + ContentValues().apply { put("acknowledgement_state", "quarantined") }, + "record_id = ? AND acknowledgement_state = 'pending'", + arrayOf(recordId), + ) > 0 + database.setTransactionSuccessful() + changed + } finally { + database.endTransaction() + } + } + + fun reject(recordId: String, reason: String): Boolean = synchronized(lock) { + database.beginTransaction() + try { + writeDiagnostic(recordId, "rejected", reason) + val changed = database.update( + "outbox", + ContentValues().apply { put("acknowledgement_state", "rejected") }, + "record_id = ? AND acknowledgement_state = 'pending'", + arrayOf(recordId), + ) > 0 + database.setTransactionSuccessful() + changed + } finally { + database.endTransaction() + } + } + + fun scheduleRetry(recordId: String, eligibleAtMs: Long): Boolean = synchronized(lock) { + database.execSQL( + "UPDATE outbox SET attempt_count = attempt_count + 1, eligible_at_ms = ? WHERE record_id = ? AND acknowledgement_state = 'pending'", + arrayOf(eligibleAtMs, recordId), + ) + database.rawQuery("SELECT changes()", null).use { cursor -> cursor.moveToFirst(); cursor.getInt(0) > 0 } + } + + fun putProtectedEvidence( + blobId: String = "blob_${UUID.randomUUID()}", + purpose: String, + consentRevision: Long, + retentionClass: String, + value: ByteArray, + keyVersion: Int = crypto.currentVersion, + ): String = synchronized(lock) { + val encrypted = crypto.encrypt(value, keyVersion) + evictProtectedFor(encrypted.size.toLong()) + database.insertWithOnConflict( + "protected_evidence", + null, + ContentValues().apply { + put("blob_id", blobId) + put("purpose", purpose) + put("consent_revision", consentRevision) + put("retention_class", retentionClass) + put("encryption_key_version", keyVersion) + put("deletion_state", "active") + put("ciphertext", encrypted) + put("created_at_ms", System.currentTimeMillis()) + }, + SQLiteDatabase.CONFLICT_IGNORE, + ) + blobId + } + + fun getProtectedEvidence(blobId: String): StoredProtectedEvidence? = synchronized(lock) { + database.rawQuery( + "SELECT purpose, consent_revision, retention_class, encryption_key_version, deletion_state, ciphertext FROM protected_evidence WHERE blob_id = ?", + arrayOf(blobId), + ).use { cursor -> + if (!cursor.moveToFirst()) return@synchronized null + val keyVersion = cursor.getInt(3) + StoredProtectedEvidence( + blobId, + cursor.getString(0), + cursor.getLong(1), + cursor.getString(2), + keyVersion, + cursor.getString(4), + crypto.decrypt(cursor.getBlob(5), keyVersion), + ) + } + } + + fun deleteProtectedEvidence(blobId: String): Boolean = synchronized(lock) { + database.update( + "protected_evidence", + ContentValues().apply { + put("deletion_state", "deleted") + putNull("ciphertext") + }, + "blob_id = ? AND deletion_state != 'deleted'", + arrayOf(blobId), + ) > 0 + } + + fun deleteProtectedData(requestId: String): Boolean = synchronized(lock) { + database.beginTransaction() + try { + database.update( + "protected_evidence", + ContentValues().apply { + put("deletion_state", "deleted") + putNull("ciphertext") + put("upload_state", "rejected") + }, + "deletion_state != 'deleted'", + null, + ) + database.insertWithOnConflict( + "state_revision", + null, + ContentValues().apply { + put("kind", "deletion") + put("revision", deletionRevision() + 1) + put("payload", requestId) + }, + SQLiteDatabase.CONFLICT_REPLACE, + ) + database.setTransactionSuccessful() + true + } finally { + database.endTransaction() + } + } + + fun measurementConfigurationState(): StoredMeasurementConfigurationState = synchronized(lock) { + database.rawQuery( + "SELECT revision, payload FROM state_revision WHERE kind = 'measurement_configuration'", + null, + ).use { cursor -> + if (!cursor.moveToFirst()) return@synchronized StoredMeasurementConfigurationState(0, null) + StoredMeasurementConfigurationState( + cursor.getLong(0), + if (cursor.isNull(1)) null else cursor.getString(1).toByteArray(Charsets.UTF_8), + ) + } + } + + fun persistMeasurementConfiguration(version: Long, payload: ByteArray): Boolean = synchronized(lock) { + database.beginTransaction() + try { + val current = database.rawQuery( + "SELECT revision FROM state_revision WHERE kind = 'measurement_configuration'", + null, + ).use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else 0 } + if (version <= current) { + database.setTransactionSuccessful() + return@synchronized false + } + database.insertWithOnConflict( + "state_revision", + null, + ContentValues().apply { + put("kind", "measurement_configuration") + put("revision", version) + put("payload", payload.toString(Charsets.UTF_8)) + }, + SQLiteDatabase.CONFLICT_REPLACE, + ) + database.setTransactionSuccessful() + true + } finally { + database.endTransaction() + } + } + + fun pushRegistrationState(): StoredMeasurementConfigurationState = synchronized(lock) { + database.rawQuery( + "SELECT revision, payload FROM state_revision WHERE kind = 'push_registration'", + null, + ).use { cursor -> + if (!cursor.moveToFirst()) return@synchronized StoredMeasurementConfigurationState(0, null) + StoredMeasurementConfigurationState( + cursor.getLong(0), + if (cursor.isNull(1)) null else cursor.getString(1).toByteArray(Charsets.UTF_8), + ) + } + } + + fun persistPushRegistration(payload: ByteArray): Boolean = synchronized(lock) { + val revision = pushRegistrationState().version + 1 + database.insertWithOnConflict( + "state_revision", + null, + ContentValues().apply { + put("kind", "push_registration") + put("revision", revision) + put("payload", payload.toString(Charsets.UTF_8)) + }, + SQLiteDatabase.CONFLICT_REPLACE, + ) >= 0 + } + + fun clearPushRegistration(): Boolean = synchronized(lock) { + database.delete("state_revision", "kind = 'push_registration'", null) > 0 + } + + fun testDeviceState(): Boolean = synchronized(lock) { + database.rawQuery("SELECT payload FROM state_revision WHERE kind = 'test_device'", null).use { cursor -> + cursor.moveToFirst() && !cursor.isNull(0) && cursor.getString(0) == "true" + } + } + + fun persistTestDeviceState(enabled: Boolean): Boolean = synchronized(lock) { + database.insertWithOnConflict( + "state_revision", + null, + ContentValues().apply { + put("kind", "test_device") + put("revision", 1) + put("payload", enabled.toString()) + }, + SQLiteDatabase.CONFLICT_REPLACE, + ) >= 0 + } + + fun applyStorageLimits( + maxOutboxRecords: Int, + maxOutboxBytes: Long, + maxProtectedBytes: Long, + ) = synchronized(lock) { + require(maxOutboxRecords > 0 && maxOutboxBytes > 0 && maxProtectedBytes > 0) { + "MEASUREMENT_INVALID_STORAGE_LIMITS" + } + limits = limits.copy( + maxOutboxRecords = maxOutboxRecords, + maxOutboxBytes = maxOutboxBytes, + maxProtectedBytes = maxProtectedBytes, + ) + evictFor(0) + } + + private fun deletionRevision(): Int = database.rawQuery( + "SELECT revision FROM state_revision WHERE kind = 'deletion'", + null, + ).use { cursor -> if (cursor.moveToFirst()) cursor.getInt(0) else 0 } + + fun getProtectedUpload(blobId: String): StoredProtectedUpload? = synchronized(lock) { + database.rawQuery( + "SELECT purpose, consent_revision, retention_class, encryption_key_version, deletion_state, ciphertext, upload_state, upload_attempt_count, upload_eligible_at_ms FROM protected_evidence WHERE blob_id = ?", + arrayOf(blobId), + ).use { cursor -> + if (!cursor.moveToFirst()) return@synchronized null + StoredProtectedUpload( + blobId, + cursor.getString(0), + cursor.getLong(1), + cursor.getString(2), + cursor.getInt(3), + cursor.getString(4), + if (cursor.isNull(5)) null else cursor.getBlob(5), + cursor.getString(6), + cursor.getInt(7), + cursor.getLong(8), + ) + } + } + + fun acknowledgeProtectedUpload(blobId: String): Boolean = synchronized(lock) { + database.update( + "protected_evidence", + ContentValues().apply { put("upload_state", "acknowledged") }, + "blob_id = ? AND upload_state != 'acknowledged'", + arrayOf(blobId), + ) > 0 + } + + fun scheduleProtectedUpload(blobId: String, eligibleAtMs: Long): Boolean = synchronized(lock) { + database.execSQL( + "UPDATE protected_evidence SET upload_attempt_count = upload_attempt_count + 1, upload_eligible_at_ms = ? WHERE blob_id = ? AND upload_state = 'pending'", + arrayOf(eligibleAtMs, blobId), + ) + database.rawQuery("SELECT changes()", null).use { cursor -> cursor.moveToFirst(); cursor.getInt(0) > 0 } + } + + fun rejectProtectedUpload(blobId: String): Boolean = synchronized(lock) { + database.update( + "protected_evidence", + ContentValues().apply { put("upload_state", "rejected") }, + "blob_id = ? AND upload_state = 'pending'", + arrayOf(blobId), + ) > 0 + } + + fun rotateProtectedEvidenceKey(newVersion: Int): Int = synchronized(lock) { + require(newVersion > 0) + val active = mutableListOf>() + database.rawQuery( + "SELECT blob_id, encryption_key_version, ciphertext FROM protected_evidence WHERE deletion_state = 'active'", + null, + ).use { cursor -> + while (cursor.moveToNext()) { + active += cursor.getString(0) to crypto.decrypt(cursor.getBlob(2), cursor.getInt(1)) + } + } + database.beginTransaction() + try { + for ((blobId, plaintext) in active) { + database.update( + "protected_evidence", + ContentValues().apply { + put("encryption_key_version", newVersion) + put("ciphertext", crypto.encrypt(plaintext, newVersion)) + }, + "blob_id = ?", + arrayOf(blobId), + ) + } + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + crypto.currentVersion = newVersion + active.size + } + + fun checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Long): Boolean = synchronized(lock) { + database.delete("dedupe", "expires_at_ms <= ?", arrayOf(System.currentTimeMillis().toString())) + val inserted = database.insertWithOnConflict( + "dedupe", + null, + ContentValues().apply { + put("namespace", namespace) + put("dedupe_key", key) + put("created_at_ms", System.currentTimeMillis()) + put("expires_at_ms", expiresAtMs) + }, + SQLiteDatabase.CONFLICT_IGNORE, + ) != -1L + trimTable("dedupe", limits.maxDedupeRecords, "created_at_ms") + inserted + } + + fun hasDedupe(namespace: String, key: String): Boolean = synchronized(lock) { + database.delete("dedupe", "expires_at_ms <= ?", arrayOf(System.currentTimeMillis().toString())) + database.rawQuery( + "SELECT 1 FROM dedupe WHERE namespace = ? AND dedupe_key = ? LIMIT 1", + arrayOf(namespace, key), + ).use { it.moveToFirst() } + } + + fun appendInbox( + id: String, + kind: String, + source: String, + appState: String, + receivedAt: String, + protectedPayloadRef: String, + ): Boolean = synchronized(lock) { + val inserted = database.insertWithOnConflict( + "inbox", + null, + ContentValues().apply { + put("entry_id", id) + put("kind", kind) + put("source", source) + put("app_state", appState) + put("received_at", receivedAt) + put("protected_payload_ref", protectedPayloadRef) + put("acknowledged", 0) + }, + SQLiteDatabase.CONFLICT_IGNORE, + ) != -1L + trimTable("inbox", limits.maxInboxRecords, "rowid", "acknowledged = 1") + inserted + } + + fun peekInbox(limit: Int): List = synchronized(lock) { + database.rawQuery( + "SELECT entry_id, kind, source, app_state, received_at, protected_payload_ref FROM inbox WHERE acknowledged = 0 ORDER BY rowid ASC LIMIT ?", + arrayOf(limit.coerceAtLeast(0).toString()), + ).use { cursor -> + cursor.mapRows { + StoredInboxEntry( + it.getString(0), + it.getString(1), + it.getString(2), + it.getString(3), + it.getString(4), + it.getString(5), + ) + } + } + } + + fun acknowledgeInbox(id: String): Boolean = synchronized(lock) { + database.update( + "inbox", + ContentValues().apply { put("acknowledged", 1) }, + "entry_id = ? AND acknowledged = 0", + arrayOf(id), + ) > 0 + } + + private fun migrate() = synchronized(lock) { + val version = database.version + if (version < 1) { + database.beginTransaction() + try { + database.execSQL("CREATE TABLE IF NOT EXISTS installation (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), installation_id TEXT NOT NULL, first_opened_at TEXT NOT NULL, sequence INTEGER NOT NULL DEFAULT 0, first_release TEXT, last_release TEXT)") + database.execSQL("CREATE TABLE IF NOT EXISTS session (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), session_id TEXT, sequence INTEGER NOT NULL DEFAULT 0, started_at TEXT, last_foreground_monotonic_ms INTEGER, last_background_monotonic_ms INTEGER)") + database.execSQL("CREATE TABLE IF NOT EXISTS state_revision (kind TEXT PRIMARY KEY, revision INTEGER NOT NULL, payload TEXT)") + database.execSQL("CREATE TABLE IF NOT EXISTS outbox (record_id TEXT PRIMARY KEY, record_type TEXT NOT NULL, installation_sequence INTEGER NOT NULL, occurred_at TEXT NOT NULL, queued_at_ms INTEGER NOT NULL, priority TEXT NOT NULL, source TEXT NOT NULL, public_payload TEXT NOT NULL, public_payload_bytes INTEGER NOT NULL, protected_payload_ref TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, eligible_at_ms INTEGER NOT NULL, acknowledgement_state TEXT NOT NULL DEFAULT 'pending')") + database.execSQL("CREATE INDEX IF NOT EXISTS outbox_eligible_idx ON outbox (acknowledgement_state, priority, eligible_at_ms, installation_sequence)") + database.execSQL("CREATE TABLE IF NOT EXISTS protected_evidence (blob_id TEXT PRIMARY KEY, purpose TEXT NOT NULL, consent_revision INTEGER NOT NULL, retention_class TEXT NOT NULL, encryption_key_version INTEGER NOT NULL, deletion_state TEXT NOT NULL, ciphertext BLOB, created_at_ms INTEGER NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS dedupe (namespace TEXT NOT NULL, dedupe_key TEXT NOT NULL, created_at_ms INTEGER NOT NULL, expires_at_ms INTEGER NOT NULL, PRIMARY KEY (namespace, dedupe_key))") + database.version = 1 + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } + if (version < 2) { + database.beginTransaction() + try { + database.execSQL("CREATE TABLE IF NOT EXISTS inbox (entry_id TEXT PRIMARY KEY, kind TEXT NOT NULL, source TEXT NOT NULL, app_state TEXT NOT NULL, received_at TEXT NOT NULL, protected_payload_ref TEXT NOT NULL, acknowledged INTEGER NOT NULL DEFAULT 0)") + database.execSQL("CREATE TABLE IF NOT EXISTS delivery_diagnostic (diagnostic_id TEXT PRIMARY KEY, record_id TEXT NOT NULL, outcome TEXT NOT NULL, reason TEXT NOT NULL, occurred_at_ms INTEGER NOT NULL)") + database.version = 2 + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } + if (version < 3) { + database.beginTransaction() + try { + database.execSQL("ALTER TABLE protected_evidence ADD COLUMN upload_state TEXT NOT NULL DEFAULT 'pending'") + database.execSQL("ALTER TABLE protected_evidence ADD COLUMN upload_attempt_count INTEGER NOT NULL DEFAULT 0") + database.execSQL("ALTER TABLE protected_evidence ADD COLUMN upload_eligible_at_ms INTEGER NOT NULL DEFAULT 0") + database.execSQL("CREATE INDEX IF NOT EXISTS protected_evidence_upload_idx ON protected_evidence (upload_state, upload_eligible_at_ms)") + database.version = 3 + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } + } + + private fun ensureInstallation() { + database.insertWithOnConflict( + "installation", + null, + ContentValues().apply { + put("singleton", 1) + put("installation_id", "install_${UUID.randomUUID()}") + put("first_opened_at", nowIso()) + put("sequence", 0) + }, + SQLiteDatabase.CONFLICT_IGNORE, + ) + } + + private fun existingSequence(recordId: String): Long? = database.rawQuery( + "SELECT installation_sequence FROM outbox WHERE record_id = ?", + arrayOf(recordId), + ).use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else null } + + private fun canonicalEnvelope( + recordId: String, + recordType: String, + occurredAt: String, + queuedAtMs: Long, + installationId: String, + sequence: Long, + source: String, + publicPayload: String, + protectedPayloadRef: String?, + ): String { + val decoded = try { + JSONObject(publicPayload) + } catch (_: Exception) { + JSONObject().put("value", publicPayload) + } + if ( + decoded.optInt("schemaVersion") == 1 && + decoded.optString("recordId").isNotBlank() && + decoded.optString("installationId").isNotBlank() && + decoded.has("publicPayload") + ) return publicPayload + + return JSONObject().apply { + put("schemaVersion", 1) + put("recordId", recordId) + put("type", recordType) + put("occurredAt", occurredAt) + put("queuedAt", isoTimestamp(queuedAtMs)) + put("installationId", installationId) + put("installationSequence", sequence) + put("identity", JSONObject().apply { + put("distinctId", installationId) + put("anonymousId", installationId) + put("revision", 0) + }) + put("consent", JSONObject().apply { + put("revision", 0) + put("decidedAt", occurredAt) + put("source", "unknown") + }) + put("app", JSONObject()) + put("device", JSONObject().put("platform", "android")) + put("source", source.lowercase(Locale.US)) + put("publicPayload", decoded) + protectedPayloadRef?.let { put("protectedPayloadRef", it) } + }.toString() + } + + private fun isoTimestamp(timestampMs: Long): String = SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + Locale.US, + ).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date(timestampMs)) + + private fun evictFor(additionalBytes: Long) { + while (pendingCount() >= limits.maxOutboxRecords || pendingBytes() + additionalBytes > limits.maxOutboxBytes) { + val candidate = database.rawQuery( + """ + SELECT record_id FROM outbox + WHERE acknowledgement_state = 'pending' + AND priority IN ('low', 'normal') + AND record_type NOT LIKE 'installation.%' + AND record_type NOT LIKE 'consent.%' + AND record_type NOT LIKE 'link.%' + AND record_type NOT LIKE 'referrer.%' + AND record_type NOT LIKE 'purchase.%' + ORDER BY CASE priority WHEN 'low' THEN 0 ELSE 1 END, installation_sequence ASC LIMIT 1 + """.trimIndent(), + null, + ).use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null } + ?: throw SQLiteException("MEASUREMENT_OUTBOX_PROTECTED_BOUND") + writeDiagnostic(candidate, "evicted", "storage_bound") + database.delete("outbox", "record_id = ?", arrayOf(candidate)) + } + } + + private fun evictProtectedFor(additionalBytes: Long) { + val current = database.rawQuery( + "SELECT COALESCE(SUM(LENGTH(ciphertext)), 0) FROM protected_evidence WHERE deletion_state = 'active'", + null, + ).use { cursor -> cursor.moveToFirst(); cursor.getLong(0) } + if (current + additionalBytes > limits.maxProtectedBytes) { + throw SQLiteException("MEASUREMENT_PROTECTED_VAULT_BOUND") + } + } + + private fun pendingCount(): Long = android.database.DatabaseUtils.queryNumEntries( + database, + "outbox", + "acknowledgement_state = 'pending'", + ) + + private fun pendingBytes(): Long = database.rawQuery( + "SELECT COALESCE(SUM(public_payload_bytes), 0) FROM outbox WHERE acknowledgement_state = 'pending'", + null, + ).use { cursor -> cursor.moveToFirst(); cursor.getLong(0) } + + private fun writeDiagnostic(recordId: String, outcome: String, reason: String) { + database.insert( + "delivery_diagnostic", + null, + ContentValues().apply { + put("diagnostic_id", "diag_${UUID.randomUUID()}") + put("record_id", recordId) + put("outcome", outcome) + put("reason", reason.take(128)) + put("occurred_at_ms", System.currentTimeMillis()) + }, + ) + } + + private fun trimTable(table: String, maximum: Int, orderBy: String, where: String? = null) { + val count = android.database.DatabaseUtils.queryNumEntries(database, table, where) + val excess = count - maximum + if (excess <= 0) return + database.execSQL( + "DELETE FROM $table WHERE rowid IN (SELECT rowid FROM $table${where?.let { " WHERE $it" } ?: ""} ORDER BY $orderBy ASC LIMIT ?)", + arrayOf(excess), + ) + } + + private fun outboxRecord(cursor: Cursor) = StoredOutboxRecord( + cursor.getString(0), + cursor.getString(1), + cursor.getLong(2), + cursor.getString(3), + cursor.getString(4), + if (cursor.isNull(5)) null else cursor.getString(5), + cursor.getInt(6), + ) + + private fun Cursor.mapRows(transform: (Cursor) -> T): List { + val values = mutableListOf() + while (moveToNext()) values += transform(this) + return values + } + + private fun nowIso(): String { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + formatter.timeZone = TimeZone.getTimeZone("UTC") + return formatter.format(Date()) + } +} + +internal interface MeasurementCrypto { + var currentVersion: Int + fun encrypt(value: ByteArray, version: Int): ByteArray + fun decrypt(value: ByteArray, version: Int): ByteArray +} + +private class MeasurementVaultCrypto : MeasurementCrypto { + override var currentVersion: Int = 1 + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + override fun encrypt(value: ByteArray, version: Int): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, key(version)) + return cipher.iv + cipher.doFinal(value) + } + + override fun decrypt(value: ByteArray, version: Int): ByteArray { + require(value.size > 12) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, key(version), GCMParameterSpec(128, value.copyOfRange(0, 12))) + return cipher.doFinal(value.copyOfRange(12, value.size)) + } + + private fun key(version: Int): SecretKey { + val alias = "voidhash-measurement-vault-v$version" + (keyStore.getKey(alias, null) as? SecretKey)?.let { return it } + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + generator.init( + KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, + ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setRandomizedEncryptionRequired(true) + .build(), + ) + return generator.generateKey() + } +} diff --git a/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/PushCollector.kt b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/PushCollector.kt new file mode 100644 index 000000000..8226d04c6 --- /dev/null +++ b/libraries/react-native/android/src/main/java/com/margelo/nitro/voidhash/measurement/PushCollector.kt @@ -0,0 +1,103 @@ +package com.margelo.nitro.voidhash.measurement + +import android.content.Context +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import org.json.JSONObject + +internal data class ObservedFcmToken(val token: String) + +internal data class PushCollectorEvent( + val id: String, + val kind: String, + val occurredAt: String, + val protectedPayloadRef: String? = null, + val pushNotificationSendId: String? = null, + val link: String? = null, + val errorCode: String? = null, +) + +/** Native FCM callback sink that remains available before JavaScript starts. */ +object VoidhashPushCollector { + private val listeners = ConcurrentHashMap Unit>() + @Volatile private var token: ObservedFcmToken? = null + + /** Records a newly issued FCM token in memory and announces a safe rotation event. */ + fun observeToken(value: String) { + if (value.isBlank()) { + observeRegistrationError("FCM_EMPTY_TOKEN") + return + } + token = ObservedFcmToken(value) + emit(PushCollectorEvent(newId(), "tokenChanged", now())) + } + + /** Records a typed registration failure without retaining its message. */ + fun observeRegistrationError(code: String) { + emit(PushCollectorEvent(newId(), "registrationError", now(), errorCode = code.ifBlank { "FCM_REGISTRATION_FAILED" })) + } + + /** Records an opened notification projection supplied by the host Activity hook. */ + fun observeOpened( + id: String, + protectedPayloadRef: String?, + pushNotificationSendId: String?, + link: String?, + ) { + emit(PushCollectorEvent(id, "opened", now(), protectedPayloadRef, pushNotificationSendId, link)) + } + + internal fun currentToken(): ObservedFcmToken? = token + internal fun subscribe(id: String, listener: (PushCollectorEvent) -> Unit) { listeners[id] = listener } + internal fun unsubscribe(id: String) { listeners.remove(id) } + internal fun emit(event: PushCollectorEvent) { listeners.values.forEach { it(event) } } + private fun newId() = "notification_${UUID.randomUUID().toString().lowercase()}" + private fun now() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(Date()) +} + +/** Firebase service that durably vaults payloads and forwards safe receipt metadata. */ +class VoidhashFirebaseMessagingService : FirebaseMessagingService() { + override fun onNewToken(token: String) { + VoidhashPushCollector.observeToken(token) + } + + override fun onMessageReceived(message: RemoteMessage) { + val id = message.messageId ?: "notification_${UUID.randomUUID().toString().lowercase()}" + val raw = JSONObject(message.data as Map<*, *>).toString() + val store = MeasurementStore(applicationContext) + try { + val protectedRef = store.putProtectedEvidence( + blobId = "push_payload_$id", + purpose = "push-token", + consentRevision = 0, + retentionClass = "ephemeral", + value = raw.toByteArray(Charsets.UTF_8), + ) + store.appendInbox(id, "push", "fcm", "background", now(), protectedRef) + VoidhashPushCollector.emit( + PushCollectorEvent( + id = id, + kind = "received", + occurredAt = now(), + protectedPayloadRef = protectedRef, + pushNotificationSendId = message.data["voidhash_send_id"], + link = message.data["voidhash_link"], + ), + ) + } finally { + store.close() + } + } + + private fun now() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(Date()) +} diff --git a/libraries/react-native/android/src/test/java/com/margelo/nitro/voidhash/measurement/MeasurementStoreTest.kt b/libraries/react-native/android/src/test/java/com/margelo/nitro/voidhash/measurement/MeasurementStoreTest.kt new file mode 100644 index 000000000..a517eb5f0 --- /dev/null +++ b/libraries/react-native/android/src/test/java/com/margelo/nitro/voidhash/measurement/MeasurementStoreTest.kt @@ -0,0 +1,386 @@ +package com.margelo.nitro.voidhash.measurement + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import java.io.File +import java.util.Collections +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.RobolectricTestRunner +import com.android.installreferrer.api.InstallReferrerClient +import org.json.JSONArray +import org.json.JSONObject +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class MeasurementStoreTest { + private lateinit var context: Context + private lateinit var store: MeasurementStore + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + File(context.noBackupFilesDir, "voidhash-measurement.sqlite").delete() + store = MeasurementStore(context, cryptoFactory = { TestMeasurementCrypto() }) + } + + @Test + fun identifierPolicyNeverReadsDeniedProviderAndVaultsAllowedValue() { + var reads = 0 + val collector = AndroidIdentifierCollector( + store, + mapOf(IdentifierKind.ADVERTISING_ID to IdentifierProvider { + reads += 1 + IdentifierProviderResult("gaid-secret") + }), + ) + assertEquals( + InstallReferrerOutcome.PERMISSION_DENIED, + collector.collect( + IdentifierKind.ADVERTISING_ID, + IdentifierCollectionPolicy(advertisingIdentifiers = false, vendorIdentifiers = true, collectionOptOut = false), + ), + ) + assertEquals(0, reads) + assertEquals( + InstallReferrerOutcome.COLLECTED, + collector.collect( + IdentifierKind.ADVERTISING_ID, + IdentifierCollectionPolicy(advertisingIdentifiers = true, vendorIdentifiers = true, collectionOptOut = false), + ), + ) + assertEquals(1, reads) + val record = store.peekEligible(20).last { it.recordType == "identifier.observed.v1" } + assertFalse(record.publicPayload.contains("gaid-secret")) + assertTrue(record.protectedPayloadRef != null) + val envelope = JSONObject(record.publicPayload) + assertEquals(record.recordId, envelope.getString("recordId")) + assertEquals("android", envelope.getJSONObject("device").getString("platform")) + assertEquals("gaid", envelope.getJSONObject("publicPayload").getString("kind")) + assertEquals("gaid-secret", String(store.getProtectedEvidence(record.protectedPayloadRef!!)?.value ?: byteArrayOf())) + } + + @Test + fun optionalAndConfiguredReferrerProvidersExposeExplicitCapabilities() { + val missing = runSuspend { + OptionalDependencyInstallReferrerProvider( + StoreKind.SAMSUNG_GALAXY_STORE, + "com.voidhash.missing.SamsungReferrer", + ) { error("must not execute") }.collect() + } + assertEquals(InstallReferrerOutcome.NOT_INSTALLED, missing.outcome) + + runSuspend { + InstallReferrerCoordinator( + store, + ConfiguredInstallReferrerProvider(StoreKind.OUT_OF_STORE, "source=oem&campaign=preload"), + ).collectOnce() + } + val record = store.peekEligible(20).last { it.recordType == "android.install_referrer.v1" } + assertTrue(record.publicPayload.contains("out_of_store")) + assertFalse(record.publicPayload.contains("source=oem")) + val envelope = JSONObject(record.publicPayload) + assertEquals("out_of_store", envelope.getJSONObject("publicPayload").getString("store")) + assertEquals(record.sequence, envelope.getLong("installationSequence")) + assertEquals( + "source=oem&campaign=preload", + String(store.getProtectedEvidence(record.protectedPayloadRef!!)?.value ?: byteArrayOf()), + ) + } + + @Test + fun deliveryRecursivelySplits413AndQuarantinesOnlyTheOversizedRecord() { + listOf("accepted-1", "oversized", "accepted-2").forEach { id -> enqueueDeliveryFixture(id) } + val transport = MeasurementHttpTransport { _, bytes -> + val events = JSONObject(bytes.toString(Charsets.UTF_8)).getJSONArray("events") + val ids = (0 until events.length()).map { events.getJSONObject(it).getString("uuid") } + if (ids.size > 1 || ids == listOf("oversized")) { + MeasurementHttpResult(413, "", null) + } else { + MeasurementHttpResult( + 200, + JSONObject().put("accepted", JSONArray(ids)).put("rejected", JSONArray()).toString(), + null, + ) + } + } + val result = MeasurementDelivery(store, "pk_test", "https://ingest.example", transport).flush() + assertEquals(2, result.accepted) + assertEquals(1, result.quarantined) + assertEquals(0, result.scheduled) + assertTrue(store.peekEligible(10).isEmpty()) + } + + @Test + fun deliveryHonorsRetryAfterAndHandlesPartialAcknowledgements() { + enqueueDeliveryFixture("retry") + val retry = MeasurementDelivery( + store, + "pk_test", + "https://ingest.example", + MeasurementHttpTransport { _, _ -> MeasurementHttpResult(429, "", 120_000) }, + ).flush() + assertEquals(1, retry.scheduled) + assertEquals(0, retry.accepted) + assertTrue(store.peekEligible(10).isEmpty()) + + store.close() + File(context.noBackupFilesDir, "voidhash-measurement.sqlite").delete() + store = MeasurementStore(context, cryptoFactory = { TestMeasurementCrypto() }) + listOf("accepted", "rejected", "missing").forEach { id -> enqueueDeliveryFixture(id) } + val response = JSONObject() + .put("accepted", JSONArray().put("accepted")) + .put( + "rejected", + JSONArray().put(JSONObject().put("recordId", "rejected").put("reason", "invalid_record")), + ) + .toString() + val partial = MeasurementDelivery( + store, + "pk_test", + "https://ingest.example", + MeasurementHttpTransport { _, _ -> MeasurementHttpResult(200, response, null) }, + ).flush() + assertEquals(1, partial.accepted) + assertEquals(1, partial.quarantined) + assertEquals(1, partial.scheduled) + assertTrue(store.peekEligible(10).isEmpty()) + } + + @Test + fun deliverySchedulesMalformedSuccessResponsesInsteadOfThrowing() { + enqueueDeliveryFixture("malformed-response") + val result = MeasurementDelivery( + store, + "pk_test", + "https://ingest.example", + MeasurementHttpTransport { _, _ -> MeasurementHttpResult(202, "not-json", null) }, + ).flush() + + assertEquals(0, result.accepted) + assertEquals(1, result.scheduled) + assertEquals(0, result.quarantined) + assertTrue(store.peekEligible(10).isEmpty()) + } + + private fun enqueueDeliveryFixture(id: String) { + store.enqueue( + id, + "analytics.capture.v1", + "2026-01-01T00:00:00.000Z", + "normal", + "javascript", + "{\"installationId\":\"install-1\",\"identity\":{\"distinctId\":\"person-1\"},\"consent\":{\"revision\":1},\"publicPayload\":{}}", + null, + ) + } + + @After + fun tearDown() { + if (::store.isInitialized) store.close() + File(context.noBackupFilesDir, "voidhash-measurement.sqlite").delete() + } + + @Test + fun `database uses no-backup storage and migrates all tables`() { + assertTrue(store.databasePath().startsWith(context.noBackupFilesDir.absolutePath)) + val tables = android.database.sqlite.SQLiteDatabase.openDatabase( + store.databasePath(), + null, + android.database.sqlite.SQLiteDatabase.OPEN_READONLY, + ).use { database -> + assertEquals(3, database.version) + database.rawQuery("SELECT name FROM sqlite_master WHERE type = 'table'", null).use { cursor -> + buildSet { while (cursor.moveToNext()) add(cursor.getString(0)) } + } + } + assertTrue(setOf("installation", "session", "state_revision", "outbox", "protected_evidence", "dedupe", "inbox").all(tables::contains)) + } + + @Test + fun `sequence allocation is unique under concurrency`() { + val sequences = Collections.synchronizedSet(mutableSetOf()) + val executor = Executors.newFixedThreadPool(8) + repeat(100) { index -> + executor.submit { + sequences += store.enqueue( + recordId = "record-$index", + recordType = "analytics.capture.v1", + occurredAt = "2026-01-01T00:00:00Z", + priority = "normal", + source = "javascript", + publicPayload = "{\"index\":$index}", + protectedPayloadRef = null, + ) + } + } + executor.shutdown() + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)) + assertEquals(100, sequences.size) + assertEquals(100, store.peekEligible(200).size) + } + + @Test + fun `priority ordering ack dedupe and inbox are idempotent`() { + store.enqueue("normal", "analytics.capture.v1", "2026-01-01T00:00:00Z", "normal", "javascript", "{}", null) + store.enqueue("critical", "consent.changed.v1", "2026-01-01T00:00:01Z", "critical", "javascript", "{}", null) + assertEquals(listOf("critical", "normal"), store.peekEligible(10).map { it.recordId }) + assertTrue(store.acknowledge("critical")) + assertFalse(store.acknowledge("critical")) + assertEquals(listOf("normal"), store.peekEligible(10).map { it.recordId }) + + assertTrue(store.checkAndSetDedupe("transaction", "tx-1", Long.MAX_VALUE)) + assertFalse(store.checkAndSetDedupe("transaction", "tx-1", Long.MAX_VALUE)) + assertTrue(store.appendInbox("inbox-1", "link", "appLink", "cold", "2026-01-01T00:00:00Z", "blob-1")) + assertFalse(store.appendInbox("inbox-1", "link", "appLink", "cold", "2026-01-01T00:00:00Z", "blob-1")) + assertEquals(listOf("inbox-1"), store.peekInbox(10).map { it.id }) + assertTrue(store.acknowledgeInbox("inbox-1")) + assertFalse(store.acknowledgeInbox("inbox-1")) + } + + @Test + fun `protected deletion persists its durable marker`() { + assertTrue(store.deleteProtectedData("delete-1")) + val marker = android.database.sqlite.SQLiteDatabase.openDatabase( + store.databasePath(), + null, + android.database.sqlite.SQLiteDatabase.OPEN_READONLY, + ).use { database -> + database.rawQuery( + "SELECT payload FROM state_revision WHERE kind = 'deletion'", + null, + ).use { cursor -> + assertTrue(cursor.moveToFirst()) + cursor.getString(0) + } + } + assertEquals("delete-1", marker) + } + + @Test + fun `signed configuration rejects downgrade and survives restart`() { + val payload = "{\"keyId\":\"rotation-2\"}".toByteArray() + assertTrue(store.persistMeasurementConfiguration(4, payload)) + assertFalse(store.persistMeasurementConfiguration(3, "{}".toByteArray())) + store.close() + store = MeasurementStore(context) + assertEquals(4L, store.measurementConfigurationState().version) + assertTrue(payload.contentEquals(store.measurementConfigurationState().payload)) + } + + @Test + fun `remote storage bounds apply to an existing store`() { + store.applyStorageLimits(2, 1_000_000, 1_000_000) + repeat(3) { index -> + store.enqueue( + "analytics-$index", "analytics.capture.v1", "2026-01-01T00:00:00Z", + "low", "javascript", "{}", null, + ) + } + assertEquals(listOf("analytics-1", "analytics-2"), store.peekEligible(10).map { it.recordId }) + } + + @Test + fun `play referrer response codes map to explicit provider outcomes`() { + val cases = mapOf( + InstallReferrerClient.InstallReferrerResponse.OK to InstallReferrerOutcome.COLLECTED, + InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED to InstallReferrerOutcome.UNSUPPORTED, + InstallReferrerClient.InstallReferrerResponse.PERMISSION_ERROR to InstallReferrerOutcome.PERMISSION_DENIED, + InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE to InstallReferrerOutcome.NOT_INSTALLED, + InstallReferrerClient.InstallReferrerResponse.SERVICE_DISCONNECTED to InstallReferrerOutcome.AVAILABLE, + ) + cases.forEach { (code, expected) -> + assertEquals(expected, classifyPlayInstallReferrerResponse(code)) + } + } + + @Test + fun `opaque push registration persists and clears`() { + val payload = "{\"pushDeviceTokenId\":\"push_tok_1\"}".toByteArray() + assertTrue(store.persistPushRegistration(payload)) + assertTrue(payload.contentEquals(store.pushRegistrationState().payload)) + assertTrue(store.clearPushRegistration()) + assertEquals(null, store.pushRegistrationState().payload) + assertFalse(store.clearPushRegistration()) + } + + @Test + fun `native links are encrypted ordered and duplicate callbacks are suppressed`() { + val now = System.currentTimeMillis() + val first = VoidhashLinkCollector.capture( + store, + "https://links.example/one?secret=value", + "appLink", + "cold", + now, + ) + val duplicate = VoidhashLinkCollector.capture( + store, + "https://links.example/one?secret=value", + "universalLink", + "cold", + now + 1, + ) + val second = VoidhashLinkCollector.capture( + store, + "voidhash://open/two", + "customScheme", + "foreground", + now + 2, + ) + + assertTrue(first) + assertFalse(duplicate) + assertTrue(second) + val entries = store.peekInbox(10) + assertEquals(listOf("appLink", "customScheme"), entries.map { it.source }) + assertEquals("https://links.example/one?secret=value", String(store.getProtectedEvidence(entries[0].protectedPayloadRef)!!.value)) + val bytes = File(store.databasePath()).readBytes().toString(Charsets.ISO_8859_1) + assertFalse(bytes.contains("secret=value")) + } +} + +private fun runSuspend(block: suspend () -> T): T { + var outcome: Result? = null + block.startCoroutine(object : Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) { outcome = result } + }) + return checkNotNull(outcome).getOrThrow() +} + +private class TestMeasurementCrypto : MeasurementCrypto { + override var currentVersion = 1 + private val keys = mutableMapOf() + + override fun encrypt(value: ByteArray, version: Int): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, key(version)) + return cipher.iv + cipher.doFinal(value) + } + + override fun decrypt(value: ByteArray, version: Int): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, key(version), GCMParameterSpec(128, value.copyOfRange(0, 12))) + return cipher.doFinal(value.copyOfRange(12, value.size)) + } + + private fun key(version: Int): SecretKey = keys.getOrPut(version) { + KeyGenerator.getInstance("AES").apply { init(256) }.generateKey() + } +} diff --git a/libraries/react-native/app.plugin.js b/libraries/react-native/app.plugin.js index 11264ee50..0a51fa33c 100644 --- a/libraries/react-native/app.plugin.js +++ b/libraries/react-native/app.plugin.js @@ -3,4 +3,4 @@ // const withMyConfigPlugins = (config) => { // return withPlugins(config, []) // } -module.exports = require("./plugin/build/withVoidhashReactNative"); +module.exports = require("./plugin/build/withVoidhashReactNative").default; diff --git a/libraries/react-native/ios-tests/AppleIdentifierPolicyTests.swift b/libraries/react-native/ios-tests/AppleIdentifierPolicyTests.swift new file mode 100644 index 000000000..4319256b5 --- /dev/null +++ b/libraries/react-native/ios-tests/AppleIdentifierPolicyTests.swift @@ -0,0 +1,79 @@ +import XCTest +@testable import VoidhashPurchaseCoordinators + +private final class LockedValue: @unchecked Sendable { + private let lock = NSLock() + private var value: Value + init(_ value: Value) { self.value = value } + func get() -> Value { lock.withLock { value } } + func update(_ body: (inout Value) -> Void) { lock.withLock { body(&value) } } +} + +final class AppleIdentifierPolicyTests: XCTestCase { + func testAttObserverEmitsEachTransitionExactlyOnce() { + let status = LockedValue("notDetermined") + let observer = AppleAttTransitionObserver { status.get() } + XCTAssertEqual( + observer.observe(source: "system", observedAt: "2026-01-01T00:00:00Z"), + AppleAttTransition( + previous: nil, + current: "notDetermined", + source: "system", + observedAt: "2026-01-01T00:00:00Z" + ) + ) + XCTAssertNil(observer.observe(source: "application", observedAt: "2026-01-01T00:00:01Z")) + status.update { $0 = "authorized" } + XCTAssertEqual( + observer.observe(source: "system", observedAt: "2026-01-01T00:00:02Z")?.previous, + "notDetermined" + ) + XCTAssertNil(observer.observe(source: "system", observedAt: "2026-01-01T00:00:03Z")) + } + + func testIdfaRequiresAuthorizedAttAndStrictBuildNeverReadsProvider() { + let reads = LockedValue(0) + let collector = AppleIdentifierCollector( + providers: [.idfa: { + reads.update { $0 += 1 } + return "idfa-secret" + }], + vault: { _, _ in "protected-idfa" } + ) + let denied = collector.collect(.idfa, policy: AppleIdentifierPolicy( + advertisingIdentifiers: true, + vendorIdentifiers: true, + collectionOptOut: false, + attStatus: "denied", + strictNoIdfa: false + )) + let strict = collector.collect(.idfa, policy: AppleIdentifierPolicy( + advertisingIdentifiers: true, + vendorIdentifiers: true, + collectionOptOut: false, + attStatus: "authorized", + strictNoIdfa: true + )) + XCTAssertEqual(denied.outcome, "permissionDenied") + XCTAssertEqual(strict.outcome, "permissionDenied") + XCTAssertEqual(reads.get(), 0) + } + + func testAllowedIdentifiersReturnOnlyOpaqueProtectedReference() { + let vaultedValue = LockedValue(nil) + let collector = AppleIdentifierCollector( + providers: [.idfv: { "vendor-secret" }], + vault: { _, value in vaultedValue.update { $0 = value }; return "protected-vendor" } + ) + let result = collector.collect(.idfv, policy: AppleIdentifierPolicy( + advertisingIdentifiers: false, + vendorIdentifiers: true, + collectionOptOut: false, + attStatus: "notDetermined", + strictNoIdfa: true + )) + XCTAssertEqual(result.protectedReference, "protected-vendor") + XCTAssertEqual(vaultedValue.get(), "vendor-secret") + XCTAssertFalse(String(describing: result).contains("vendor-secret")) + } +} diff --git a/libraries/react-native/ios-tests/ConversionValueEngineTests.swift b/libraries/react-native/ios-tests/ConversionValueEngineTests.swift new file mode 100644 index 000000000..6de250fc9 --- /dev/null +++ b/libraries/react-native/ios-tests/ConversionValueEngineTests.swift @@ -0,0 +1,143 @@ +import XCTest +@testable import VoidhashPurchaseCoordinators + +private enum FakeConversionError: Error { case failed } + +private final class LockedValues: @unchecked Sendable { + private let lock = NSLock() + private var values: [Value] = [] + + func append(_ value: Value) { lock.withLock { values.append(value) } } + func snapshot() -> [Value] { lock.withLock { values } } +} + +private final class FakeConversionAdapter: ConversionValuePlatformAdapter, @unchecked Sendable { + let supportsSKAdNetwork: Bool + let supportsAdAttributionKit: Bool + var fail = false + private(set) var skanUpdates: [ConversionValueUpdate] = [] + private(set) var attributionKitUpdates: [ConversionValueUpdate] = [] + + init(skan: Bool = true, attributionKit: Bool = true) { + supportsSKAdNetwork = skan + supportsAdAttributionKit = attributionKit + } + + func updateSKAdNetwork(_ update: ConversionValueUpdate) async throws { + if fail { throw FakeConversionError.failed } + skanUpdates.append(update) + } + + func updateAdAttributionKit(_ update: ConversionValueUpdate) async throws { + if fail { throw FakeConversionError.failed } + attributionKitUpdates.append(update) + } +} + +final class ConversionValueEngineTests: XCTestCase { + private let rules = [ + ConversionValueRule( + eventName: "trial_started", minimumCount: 1, fineValue: 12, + coarseValue: .medium, lockWindow: false, window: 1 + ), + ConversionValueRule( + eventName: "purchase", minimumCount: 2, fineValue: 31, + coarseValue: .high, lockWindow: true, window: 1 + ), + ] + + func testWindowBoundariesMatchThreeAppleConversionWindows() { + let day: TimeInterval = 86_400 + XCTAssertEqual(ConversionValueEngine.conversionWindow(elapsedSinceFirstLaunch: 0), 1) + XCTAssertEqual(ConversionValueEngine.conversionWindow(elapsedSinceFirstLaunch: 2 * day), 2) + XCTAssertEqual(ConversionValueEngine.conversionWindow(elapsedSinceFirstLaunch: 7 * day), 3) + XCTAssertNil(ConversionValueEngine.conversionWindow(elapsedSinceFirstLaunch: 35 * day)) + } + + func testEvaluationSelectsHighestMatchingValueAndProducesSafeTrace() throws { + let result = try XCTUnwrap(ConversionValueEngine.evaluate( + rules: rules, + eventCounts: ["trial_started": 1, "purchase": 2], + window: 1 + )) + XCTAssertEqual(result.fineValue, 31) + XCTAssertEqual(result.coarseValue, .high) + XCTAssertTrue(result.lockWindow) + let encoded = String(decoding: try JSONEncoder().encode(result.trace), as: UTF8.self) + XCTAssertFalse(encoded.contains("token")) + XCTAssertFalse(encoded.contains("identifier")) + XCTAssertFalse(encoded.contains("properties")) + } + + func testUpdateCallsBothFrameworksAndWritesExactlyOneEvidence() async throws { + let adapter = FakeConversionAdapter() + let evidence = LockedValues() + let engine = ConversionValueEngine( + adapter: adapter, + persistRules: { _, _ in true }, + evidenceSink: { evidence.append($0) } + ) + try engine.applyRules(version: 7, rules: rules) + let result = await engine.update( + eventCounts: ["purchase": 2], + elapsedSinceFirstLaunch: 100, + attributionAllowed: true + ) + XCTAssertEqual(result?.outcome, .succeeded) + XCTAssertEqual(adapter.skanUpdates.count, 1) + XCTAssertEqual(adapter.attributionKitUpdates.count, 1) + XCTAssertEqual(evidence.snapshot().count, 1) + XCTAssertEqual(evidence.snapshot().first?.ruleVersion, 7) + } + + func testLockedWindowAndPlatformFailureAreRecordedWithoutIllegalCall() async throws { + let adapter = FakeConversionAdapter() + let evidence = LockedValues() + let engine = ConversionValueEngine( + adapter: adapter, + persistRules: { _, _ in true }, + evidenceSink: { evidence.append($0) } + ) + try engine.applyRules(version: 1, rules: rules) + _ = await engine.update(eventCounts: ["purchase": 2], elapsedSinceFirstLaunch: 1, attributionAllowed: true) + let locked = await engine.update(eventCounts: ["purchase": 2], elapsedSinceFirstLaunch: 2, attributionAllowed: true) + XCTAssertEqual(locked?.errorCode, "windowLocked") + XCTAssertEqual(adapter.skanUpdates.count, 1) + XCTAssertEqual(evidence.snapshot().count, 2) + } + + func testNoRulesAndPolicyDenialNeverCallPlatformApis() async throws { + let adapter = FakeConversionAdapter() + let evidence = LockedValues() + let engine = ConversionValueEngine( + adapter: adapter, + persistRules: { _, _ in true }, + evidenceSink: { evidence.append($0) } + ) + XCTAssertEqual(engine.capabilityState, "noRules") + let noRules = await engine.update( + eventCounts: ["purchase": 2], elapsedSinceFirstLaunch: 1, attributionAllowed: true + ) + XCTAssertNil(noRules) + try engine.applyRules(version: 1, rules: rules) + let blocked = await engine.update( + eventCounts: ["purchase": 2], elapsedSinceFirstLaunch: 1, attributionAllowed: false + ) + XCTAssertEqual(blocked?.outcome, .policyBlocked) + XCTAssertTrue(adapter.skanUpdates.isEmpty) + XCTAssertTrue(adapter.attributionKitUpdates.isEmpty) + XCTAssertEqual(evidence.snapshot().count, 1) + } + + func testRuleVersionsAreStrictlyMonotonicAndPersisted() throws { + let persisted = LockedValues() + let engine = ConversionValueEngine( + adapter: FakeConversionAdapter(), + persistRules: { version, _ in persisted.append(version); return true }, + evidenceSink: { _ in } + ) + try engine.applyRules(version: 3, rules: rules) + XCTAssertThrowsError(try engine.applyRules(version: 2, rules: rules)) + XCTAssertEqual(persisted.snapshot(), [3]) + } +} diff --git a/libraries/react-native/ios-tests/LinkCollectorTests.swift b/libraries/react-native/ios-tests/LinkCollectorTests.swift new file mode 100644 index 000000000..79f39654b --- /dev/null +++ b/libraries/react-native/ios-tests/LinkCollectorTests.swift @@ -0,0 +1,45 @@ +import Foundation +import XCTest +@testable import VoidhashPurchaseCoordinators + +final class LinkCollectorTests: XCTestCase { + func testLinksAreEncryptedOrderedAndDuplicateCallbacksAreSuppressed() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let database = directory.appendingPathComponent("measurement.sqlite") + let store = try MeasurementStore(databaseURL: database) + let now = Date() + + XCTAssertTrue(try VoidhashLinkCollector.capture( + store: store, + raw: "https://links.example/one?secret=value", + source: "universalLink", + appState: "cold", + now: now + )) + XCTAssertFalse(try VoidhashLinkCollector.capture( + store: store, + raw: "https://links.example/one?secret=value", + source: "universalLink", + appState: "cold", + now: now.addingTimeInterval(0.001) + )) + XCTAssertTrue(try VoidhashLinkCollector.capture( + store: store, + raw: "voidhash://open/two", + source: "customScheme", + appState: "foreground", + now: now.addingTimeInterval(0.002) + )) + + let entries = try store.peekInbox(limit: 10) + XCTAssertEqual(entries.map(\.source), ["universalLink", "customScheme"]) + XCTAssertEqual( + String(data: try XCTUnwrap(store.getProtectedEvidence(blobId: entries[0].protectedPayloadRef)?.value), encoding: .utf8), + "https://links.example/one?secret=value" + ) + XCTAssertFalse(String(decoding: try Data(contentsOf: database), as: UTF8.self).contains("secret=value")) + } +} diff --git a/libraries/react-native/ios-tests/MeasurementDeliveryTests.swift b/libraries/react-native/ios-tests/MeasurementDeliveryTests.swift new file mode 100644 index 000000000..d7e673efd --- /dev/null +++ b/libraries/react-native/ios-tests/MeasurementDeliveryTests.swift @@ -0,0 +1,146 @@ +import Foundation +import XCTest +@testable import VoidhashPurchaseCoordinators + +private final class LockedURLHandler: @unchecked Sendable { + typealias Handler = @Sendable (URLRequest) throws -> (Int, [String: String], Data) + private let lock = NSLock() + private var handler: Handler? + + func set(_ value: @escaping Handler) { + lock.lock() + handler = value + lock.unlock() + } + + func get() -> Handler? { + lock.lock() + defer { lock.unlock() } + return handler + } +} + +private final class MeasurementURLProtocol: URLProtocol { + static let state = LockedURLHandler() + + override class func canInit(with request: URLRequest) -> Bool { true } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + + override func startLoading() { + do { + guard let handler = Self.state.get(), let url = request.url else { throw URLError(.badURL) } + let (status, headers, data) = try handler(request) + let response = HTTPURLResponse(url: url, statusCode: status, httpVersion: "HTTP/1.1", headerFields: headers)! + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} + +private func requestBody(_ request: URLRequest) -> Data? { + if let body = request.httpBody { return body } + guard let stream = request.httpBodyStream else { return nil } + stream.open() + defer { stream.close() } + var result = Data() + var buffer = [UInt8](repeating: 0, count: 4_096) + while true { + let count = stream.read(&buffer, maxLength: buffer.count) + if count <= 0 { break } + result.append(buffer, count: count) + } + return result +} + +final class MeasurementDeliveryTests: XCTestCase { + private func fixture() throws -> (URL, MeasurementStore, URLSession) { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("voidhash-delivery-tests-\(UUID().uuidString)", isDirectory: true) + let store = try MeasurementStore(databaseURL: directory.appendingPathComponent("measurement.sqlite")) + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MeasurementURLProtocol.self] + return (directory, store, URLSession(configuration: configuration)) + } + + private func enqueue(_ ids: [String], store: MeasurementStore) throws { + for id in ids { + _ = try store.enqueue( + recordId: id, + recordType: "analytics.capture.v1", + occurredAt: "2026-01-01T00:00:00.000Z", + priority: "normal", + source: "javascript", + publicPayload: "{\"installationId\":\"install-1\",\"identity\":{\"distinctId\":\"person-1\"},\"consent\":{\"revision\":1},\"publicPayload\":{}}", + protectedPayloadRef: nil + ) + } + } + + func testPayloadTooLargeRecursivelySplitsAndQuarantinesOnlyTheOversizedRecord() async throws { + let (directory, store, session) = try fixture() + defer { try? FileManager.default.removeItem(at: directory) } + try enqueue(["accepted-1", "oversized", "accepted-2"], store: store) + MeasurementURLProtocol.state.set { request in + let body = try XCTUnwrap(requestBody(request)) + let object = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any]) + let events = try XCTUnwrap(object["events"] as? [[String: Any]]) + let ids = events.compactMap { $0["uuid"] as? String } + if ids.count > 1 || ids == ["oversized"] { return (413, [:], Data()) } + return (200, [:], try JSONSerialization.data(withJSONObject: ["accepted": ids, "rejected": []])) + } + let result = await MeasurementDelivery( + store: store, + publishableKey: "pk_test", + ingestOrigin: URL(string: "https://ingest.example")!, + session: session + ).flush() + XCTAssertEqual(result.accepted, 2) + XCTAssertEqual(result.quarantined, 1) + XCTAssertEqual(result.scheduled, 0) + XCTAssertTrue(try store.peekEligible(limit: 10).isEmpty) + } + + func testRateLimitHonorsRetryAfterAndDoesNotAcknowledge() async throws { + let (directory, store, session) = try fixture() + defer { try? FileManager.default.removeItem(at: directory) } + try enqueue(["retry-me"], store: store) + MeasurementURLProtocol.state.set { _ in (429, ["retry-after": "120"], Data()) } + let result = await MeasurementDelivery( + store: store, + publishableKey: "pk_test", + ingestOrigin: URL(string: "https://ingest.example")!, + session: session + ).flush() + XCTAssertEqual(result.scheduled, 1) + XCTAssertEqual(result.accepted, 0) + XCTAssertTrue(try store.peekEligible(limit: 10).isEmpty) + } + + func testPartialAcknowledgementQuarantinesRejectedAndSchedulesMissingRecords() async throws { + let (directory, store, session) = try fixture() + defer { try? FileManager.default.removeItem(at: directory) } + try enqueue(["accepted", "rejected", "missing"], store: store) + MeasurementURLProtocol.state.set { _ in + let response: [String: Any] = [ + "accepted": ["accepted"], + "rejected": [["recordId": "rejected", "reason": "invalid_record"]], + ] + return (200, [:], try JSONSerialization.data(withJSONObject: response)) + } + let result = await MeasurementDelivery( + store: store, + publishableKey: "pk_test", + ingestOrigin: URL(string: "https://ingest.example")!, + session: session + ).flush() + XCTAssertEqual(result.accepted, 1) + XCTAssertEqual(result.quarantined, 1) + XCTAssertEqual(result.scheduled, 1) + XCTAssertTrue(try store.peekEligible(limit: 10).isEmpty) + } +} diff --git a/libraries/react-native/ios-tests/MeasurementStoreTests.swift b/libraries/react-native/ios-tests/MeasurementStoreTests.swift new file mode 100644 index 000000000..621cb9a14 --- /dev/null +++ b/libraries/react-native/ios-tests/MeasurementStoreTests.swift @@ -0,0 +1,194 @@ +import Foundation +import SQLite3 +import XCTest +@testable import VoidhashPurchaseCoordinators + +private final class LockedSequences: @unchecked Sendable { + private let lock = NSLock() + private var values: [Int64] = [] + + func append(_ value: Int64) { + lock.lock() + values.append(value) + lock.unlock() + } + + func snapshot() -> [Int64] { + lock.lock() + defer { lock.unlock() } + return values + } +} + +final class MeasurementStoreTests: XCTestCase { + private func temporaryDatabase() throws -> (URL, MeasurementStore) { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("voidhash-measurement-tests-(UUID().uuidString)", isDirectory: true) + let url = directory.appendingPathComponent("measurement.sqlite") + return (directory, try MeasurementStore(databaseURL: url)) + } + + func testSequenceOrderingAndIdempotentAcknowledgement() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + let second = try store.enqueue( + recordId: "normal", recordType: "analytics.capture.v1", occurredAt: "2026-01-01T00:00:00Z", + priority: "normal", source: "javascript", publicPayload: "{}", protectedPayloadRef: nil + ) + let first = try store.enqueue( + recordId: "critical", recordType: "consent.changed.v1", occurredAt: "2026-01-01T00:00:01Z", + priority: "critical", source: "javascript", publicPayload: "{}", protectedPayloadRef: nil + ) + XCTAssertEqual([second, first], [1, 2]) + XCTAssertEqual(try store.peekEligible(limit: 10).map(\.recordId), ["critical", "normal"]) + XCTAssertTrue(try store.acknowledge(recordId: "critical")) + XCTAssertFalse(try store.acknowledge(recordId: "critical")) + XCTAssertEqual(try store.peekEligible(limit: 10).map(\.recordId), ["normal"]) + } + + func testConcurrentAllocationIsUniqueAndAtomic() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + let sequences = LockedSequences() + DispatchQueue.concurrentPerform(iterations: 100) { index in + let sequence = try! store.enqueue( + recordId: "record-\(index)", recordType: "analytics.capture.v1", + occurredAt: "2026-01-01T00:00:00Z", priority: "normal", source: "javascript", + publicPayload: "{\"index\":\(index)}", protectedPayloadRef: nil + ) + sequences.append(sequence) + } + XCTAssertEqual(Set(sequences.snapshot()).count, 100) + XCTAssertEqual(try store.peekEligible(limit: 200).count, 100) + } + + func testProtectedEvidenceIsEncryptedAndRotates() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + let secret = Data("raw-secret-value".utf8) + let id = try store.putProtectedEvidence( + blobId: "blob-1", purpose: "purchase-receipt", consentRevision: 3, + retentionClass: "transaction", value: secret + ) + XCTAssertEqual(try store.getProtectedEvidence(blobId: id)?.value, secret) + XCTAssertFalse((try Data(contentsOf: store.databaseURL)).contains(secret)) + XCTAssertEqual(try store.rotateProtectedEvidenceKey(to: 2), 1) + let rotated = try store.getProtectedEvidence(blobId: id) + XCTAssertEqual(rotated?.value, secret) + XCTAssertEqual(rotated?.encryptionKeyVersion, 2) + let upload = try store.getProtectedUpload(blobId: id) + XCTAssertEqual(upload?.uploadState, "pending") + XCTAssertNotEqual(upload?.ciphertext, secret) + XCTAssertTrue(try store.acknowledgeProtectedUpload(blobId: id)) + XCTAssertEqual(try store.getProtectedUpload(blobId: id)?.uploadState, "acknowledged") + } + + func testDedupeAndInboxAreDurableAndIdempotent() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + XCTAssertTrue(try store.checkAndSetDedupe(namespace: "transaction", key: "tx-1", expiresAtMs: .max)) + XCTAssertFalse(try store.checkAndSetDedupe(namespace: "transaction", key: "tx-1", expiresAtMs: .max)) + XCTAssertTrue(try store.appendInbox( + id: "inbox-1", kind: "link", source: "universalLink", appState: "cold", + receivedAt: "2026-01-01T00:00:00Z", protectedPayloadRef: "blob-1" + )) + XCTAssertFalse(try store.appendInbox( + id: "inbox-1", kind: "link", source: "universalLink", appState: "cold", + receivedAt: "2026-01-01T00:00:00Z", protectedPayloadRef: "blob-1" + )) + XCTAssertEqual(try store.peekInbox(limit: 10).map(\.id), ["inbox-1"]) + XCTAssertTrue(try store.acknowledgeInbox(id: "inbox-1")) + XCTAssertFalse(try store.acknowledgeInbox(id: "inbox-1")) + XCTAssertTrue(try store.peekInbox(limit: 10).isEmpty) + } + + func testProtectedDeletionPurgesValuesAndPersistsMarkerAtomically() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + _ = try store.putProtectedEvidence( + blobId: "blob-delete", purpose: "email", consentRevision: 4, + retentionClass: "installation", value: Data("protected@example.com".utf8) + ) + XCTAssertTrue(try store.deleteProtectedData(requestId: "delete-1")) + XCTAssertNil(try? store.getProtectedEvidence(blobId: "blob-delete")) + + var database: OpaquePointer? + XCTAssertEqual(sqlite3_open_v2(store.databaseURL.path, &database, SQLITE_OPEN_READONLY, nil), SQLITE_OK) + defer { sqlite3_close(database) } + var statement: OpaquePointer? + XCTAssertEqual( + sqlite3_prepare_v2(database, "SELECT payload FROM state_revision WHERE kind = 'deletion'", -1, &statement, nil), + SQLITE_OK + ) + defer { sqlite3_finalize(statement) } + XCTAssertEqual(sqlite3_step(statement), SQLITE_ROW) + XCTAssertEqual(String(cString: sqlite3_column_text(statement, 0)), "delete-1") + } + + func testPriorityEvictionRetainsProtectedRecordClasses() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("voidhash-measurement-tests-(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: directory) } + let store = try MeasurementStore( + maxOutboxRecords: 3, + databaseURL: directory.appendingPathComponent("measurement.sqlite") + ) + for (id, type, priority) in [ + ("install", "installation.created.v1", "critical"), + ("consent", "consent.changed.v1", "critical"), + ("analytics", "analytics.capture.v1", "low"), + ("link", "link.received.v1", "high"), + ] { + _ = try store.enqueue( + recordId: id, recordType: type, occurredAt: "2026-01-01T00:00:00Z", + priority: priority, source: "native", publicPayload: "{}", protectedPayloadRef: nil + ) + } + XCTAssertEqual(Set(try store.peekEligible(limit: 10).map(\.recordId)), ["install", "consent", "link"]) + } + + func testSignedConfigurationVersionAndPayloadSurviveRestart() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("voidhash-measurement-tests-(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("measurement.sqlite") + let payload = Data("{\"keyId\":\"rotation-2\"}".utf8) + do { + let store = try MeasurementStore(databaseURL: url) + XCTAssertTrue(try store.persistMeasurementConfiguration(version: 4, payload: payload)) + XCTAssertFalse(try store.persistMeasurementConfiguration(version: 3, payload: Data("{}".utf8))) + } + let reopened = try MeasurementStore(databaseURL: url) + XCTAssertEqual(try reopened.measurementConfigurationState().version, 4) + XCTAssertEqual(try reopened.measurementConfigurationState().payload, payload) + } + + func testRemoteStorageLimitsApplyToExistingStore() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + try store.applyStorageLimits( + maxOutboxRecords: 2, + maxOutboxBytes: 1_000_000, + maxProtectedBytes: 1_000_000 + ) + for index in 0 ..< 3 { + _ = try store.enqueue( + recordId: "analytics-\(index)", recordType: "analytics.capture.v1", + occurredAt: "2026-01-01T00:00:00Z", priority: "low", source: "javascript", + publicPayload: "{}", protectedPayloadRef: nil + ) + } + XCTAssertEqual(try store.peekEligible(limit: 10).map(\.recordId), ["analytics-1", "analytics-2"]) + } + + func testOpaquePushRegistrationPersistsAndClears() throws { + let (directory, store) = try temporaryDatabase() + defer { try? FileManager.default.removeItem(at: directory) } + let payload = Data("{\"pushDeviceTokenId\":\"push_tok_1\"}".utf8) + XCTAssertTrue(try store.persistPushRegistration(payload: payload)) + XCTAssertEqual(try store.pushRegistrationState().payload, payload) + XCTAssertTrue(try store.clearPushRegistration()) + XCTAssertNil(try store.pushRegistrationState().payload) + XCTAssertFalse(try store.clearPushRegistration()) + } +} diff --git a/libraries/react-native/ios-tests/PushCollectorTests.swift b/libraries/react-native/ios-tests/PushCollectorTests.swift new file mode 100644 index 000000000..6080eb7f8 --- /dev/null +++ b/libraries/react-native/ios-tests/PushCollectorTests.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTest +@testable import VoidhashPurchaseCoordinators + +final class PushCollectorTests: XCTestCase { + func testAPNSTokenConversionIsZeroPaddedLowercaseHex() { + let collector = VoidhashPushCollector.shared + collector.didRegister( + deviceToken: Data([0x00, 0x01, 0x0f, 0x10, 0xab, 0xff]), + environment: .development + ) + XCTAssertEqual(collector.currentToken()?.token, "00010f10abff") + XCTAssertEqual(collector.currentToken()?.environment, .development) + } + + func testCollectorEmitsTokenChangeAndTypedFailureWithoutTokenMaterial() { + let collector = VoidhashPushCollector.shared + var events: [String] = [] + let subscription = collector.subscribe { event in + switch event { + case .tokenChanged: events.append("changed") + case .registrationError(let code): events.append(code) + } + } + defer { collector.unsubscribe(subscription) } + collector.didRegister(deviceToken: Data([0xde, 0xad]), environment: .production) + collector.didFailToRegister(code: "APNS_DISABLED") + XCTAssertEqual(events, ["changed", "APNS_DISABLED"]) + XCTAssertFalse(events.joined().contains("dead")) + } +} diff --git a/libraries/react-native/ios/HybridMeasurement.swift b/libraries/react-native/ios/HybridMeasurement.swift new file mode 100644 index 000000000..b3fb1ea22 --- /dev/null +++ b/libraries/react-native/ios/HybridMeasurement.swift @@ -0,0 +1,263 @@ +import Foundation +import NitroModules + +final class HybridMeasurement: HybridMeasurementSpec { + private struct RemoteConfiguration: Decodable { + struct Rule: Decodable { + let coarseValue: ConversionValueCoarse? + let eventName: String + let fineValue: Int + let lockWindow: Bool? + let minimumCount: Int + let window: Int + } + let conversionRules: [Rule] + let schemaVersion: Int + } + private var readiness = "uninitialized" + private var consentRevision = 0.0 + private var configurationRevision = 0.0 + private var listeners: [String: (MeasurementBridgeEvent) -> Void] = [:] + private var publishableKey: String? + private var ingestOrigin: URL? + private lazy var store: MeasurementStore = { + do { return try MeasurementStore() } + catch { fatalError("MEASUREMENT_STORE_INITIALIZATION_FAILED") } + }() + private lazy var conversionEngine = ConversionValueEngine( + adapter: SystemConversionValuePlatformAdapter(), + persistRules: { _, _ in true }, + evidenceSink: { _ in } + ) + + private func snapshot() throws -> MeasurementStateBridge { + let state = try store.snapshot() + return MeasurementStateBridge( + installationId: state.installationId, + firstOpenedAt: state.firstOpenedAt, + installationSequence: Double(state.sequence), + readiness: readiness, + currentSessionId: nil, + currentSessionSequence: nil, + consentRevision: consentRevision, + configurationRevision: configurationRevision, + outboxCritical: Double(state.counts["critical"] ?? 0), + outboxHigh: Double(state.counts["high"] ?? 0), + outboxNormal: Double(state.counts["normal"] ?? 0), + outboxLow: Double(state.counts["low"] ?? 0), + oldestRecordAgeMs: state.oldestQueuedAtMs.map { + Double(max(0, Int64(Date().timeIntervalSince1970 * 1_000) - $0)) + } + ) + } + + func initialize(publishableKey: String, configuration: MeasurementInitializeConfiguration) throws -> Promise { + guard !publishableKey.isEmpty else { throw RuntimeError.error(withMessage: "INVALID_PUBLISHABLE_KEY") } + return Promise.async { + _ = try self.store.snapshot() + self.publishableKey = publishableKey + self.ingestOrigin = URL(string: configuration.ingestUrl) + self.configurationRevision += 1 + self.readiness = "sdkReady" + return try self.snapshot() + } + } + + func enqueue(command: MeasurementCommand) throws -> Promise { + let publicPayload = Data(bytes: command.publicPayload.data, count: command.publicPayload.size) + return Promise.async { + let recordId = command.commandId + let sequence = try self.store.enqueue( + recordId: recordId, + recordType: command.recordType, + occurredAt: command.occurredAt, + priority: String(describing: command.priority), + source: String(describing: command.source), + publicPayload: String(decoding: publicPayload, as: UTF8.self), + protectedPayloadRef: command.protectedEvidenceRef + ) + if let consent = command.consent { self.consentRevision = consent.revision } + return MeasurementCommandResult( + accepted: true, + recordId: recordId, + installationSequence: Double(sequence), + error: nil + ) + } + } + + func flush() throws -> Promise { + Promise.async { + guard let key = self.publishableKey, let origin = self.ingestOrigin else { + let count = try self.store.peekEligible(limit: Int.max).count + return MeasurementFlushBridgeResult(accepted: 0, scheduled: Double(count), quarantined: 0, policyBlocked: 0) + } + let result = await MeasurementDelivery( + store: self.store, + publishableKey: key, + ingestOrigin: origin + ).flush() + return MeasurementFlushBridgeResult( + accepted: Double(result.accepted), + scheduled: Double(result.scheduled), + quarantined: Double(result.quarantined), + policyBlocked: Double(result.policyBlocked) + ) + } + } + + func getInstallationId() throws -> Promise { + Promise.async { try self.store.snapshot().installationId } + } + + func getState() throws -> Promise { Promise.async { try self.snapshot() } } + + func subscribe(subscriptionId: String, listener: @escaping (MeasurementBridgeEvent) -> Void) throws { + listeners[subscriptionId] = listener + } + + func unsubscribe(subscriptionId: String) throws { listeners.removeValue(forKey: subscriptionId) } + + func peekInbox(limit: Double) throws -> Promise<[MeasurementInboxEntry]> { + Promise.async { + try self.store.peekInbox(limit: Int(limit)).map { + MeasurementInboxEntry( + id: $0.id, + kind: $0.kind, + source: $0.source, + appState: $0.appState, + receivedAt: $0.receivedAt, + protectedEvidenceRef: $0.protectedPayloadRef + ) + } + } + } + + func acknowledgeInbox(entryId: String) throws -> Promise { + Promise.async { try self.store.acknowledgeInbox(id: entryId) } + } + + func readProtectedEvidence(blobId: String) throws -> Promise { + Promise.async { + guard let evidence = try self.store.getProtectedEvidence(blobId: blobId) else { + throw RuntimeError.error(withMessage: "PROTECTED_EVIDENCE_NOT_FOUND") + } + return try ArrayBuffer.copy(data: evidence.value) + } + } + + func putProtectedEvidence(input: MeasurementProtectedEvidenceInput) throws -> Promise { + let value = Data(bytes: input.value.data, count: input.value.size) + return Promise.async { + try self.store.putProtectedEvidence( + blobId: input.blobId, + purpose: input.purpose.stringValue, + consentRevision: Int64(input.consentRevision), + retentionClass: input.retentionClass.stringValue, + value: value + ) + } + } + + func deleteProtectedEvidence(blobId: String) throws -> Promise { + Promise.async { try self.store.deleteProtectedEvidence(blobId: blobId) } + } + + func deleteProtectedData(requestId: String) throws -> Promise { + Promise.async { try self.store.deleteProtectedData(requestId: requestId) } + } + + func getMeasurementConfigurationState() throws -> Promise { + Promise.async { + let state = try self.store.measurementConfigurationState() + return MeasurementConfigurationStateBridge( + version: Double(state.version), + payload: try state.payload.map { try ArrayBuffer.copy(data: $0) } + ) + } + } + + func persistMeasurementConfigurationState(version: Double, payload: ArrayBuffer) throws -> Promise { + let bytes = Data(bytes: payload.data, count: payload.size) + return Promise.async { + try self.store.persistMeasurementConfiguration(version: Int64(version), payload: bytes) + } + } + + func applyMeasurementConfiguration(version: Double, payload: ArrayBuffer) throws -> Promise { + let bytes = Data(bytes: payload.data, count: payload.size) + return Promise.async { + let configuration = try JSONDecoder().decode(RemoteConfiguration.self, from: bytes) + guard configuration.schemaVersion == 1 else { + throw RuntimeError.error(withMessage: "MEASUREMENT_CONFIGURATION_INVALID") + } + let rules = configuration.conversionRules.map { + ConversionValueRule( + eventName: $0.eventName, + minimumCount: $0.minimumCount, + fineValue: $0.fineValue, + coarseValue: $0.coarseValue, + lockWindow: $0.lockWindow ?? false, + window: $0.window + ) + } + try self.conversionEngine.applyRules(version: Int64(version), rules: rules) + } + } + + func applyMeasurementStorageLimits( + maxOutboxRecords: Double, + maxOutboxBytes: Double, + maxProtectedBytes: Double + ) throws -> Promise { + Promise.async { + try self.store.applyStorageLimits( + maxOutboxRecords: Int64(maxOutboxRecords), + maxOutboxBytes: Int64(maxOutboxBytes), + maxProtectedBytes: Int64(maxProtectedBytes) + ) + } + } + + func getPushRegistrationState() throws -> Promise { + Promise.async { + let state = try self.store.pushRegistrationState() + return MeasurementConfigurationStateBridge( + version: Double(state.version), + payload: try state.payload.map { try ArrayBuffer.copy(data: $0) } + ) + } + } + + func persistPushRegistrationState(payload: ArrayBuffer) throws -> Promise { + let bytes = Data(bytes: payload.data, count: payload.size) + return Promise.async { try self.store.persistPushRegistration(payload: bytes) } + } + + func clearPushRegistrationState() throws -> Promise { + Promise.async { try self.store.clearPushRegistration() } + } + + func getTestDeviceState() throws -> Promise { + Promise.async { try self.store.testDeviceState() } + } + + func persistTestDeviceState(enabled: Bool) throws -> Promise { + Promise.async { try self.store.persistTestDeviceState(enabled) } + } + + func checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Double) throws -> Promise { + Promise.async { + try self.store.checkAndSetDedupe( + namespace: namespace, + key: key, + expiresAtMs: Int64(expiresAtMs) + ) + } + } + + + func hasDedupe(namespace: String, key: String) throws -> Promise { + Promise.async { try self.store.hasDedupe(namespace: namespace, key: key) } + } +} diff --git a/libraries/react-native/ios/HybridNotifications.swift b/libraries/react-native/ios/HybridNotifications.swift new file mode 100644 index 000000000..b57d61bae --- /dev/null +++ b/libraries/react-native/ios/HybridNotifications.swift @@ -0,0 +1,92 @@ +import Foundation +import NitroModules +import UserNotifications +import UIKit + +final class HybridNotifications: HybridNotificationsSpec { + private var listeners: [String: (NativeNotificationEvent) -> Void] = [:] + private lazy var collectorSubscription = VoidhashPushCollector.shared.subscribe { [weak self] event in + guard let self else { return } + let kind: NativeNotificationEventKind + let errorCode: String? + switch event { + case .tokenChanged: + kind = .tokenchanged + errorCode = nil + case .registrationError(let code): + kind = .registrationerror + errorCode = code + } + let observed = NativeNotificationEvent( + id: "notification_\(UUID().uuidString.lowercased())", + kind: kind, + occurredAt: ISO8601DateFormatter().string(from: Date()), + protectedPayloadRef: nil, + pushNotificationSendId: nil, + link: nil, + errorCode: errorCode + ) + self.listeners.values.forEach { $0(observed) } + } + + init() { _ = collectorSubscription } + + deinit { VoidhashPushCollector.shared.unsubscribe(collectorSubscription) } + + func getPermissionStatus() throws -> Promise { + Promise.async { + let settings = await UNUserNotificationCenter.current().notificationSettings() + switch settings.authorizationStatus { + case .authorized: return "authorized" + case .provisional, .ephemeral: return "provisional" + case .denied: return "denied" + case .notDetermined: return "notDetermined" + @unknown default: return "notDetermined" + } + } + } + + func requestPermission(provisional: Bool) throws -> Promise { + Promise.async { + var options: UNAuthorizationOptions = [.alert, .badge, .sound] + if provisional { options.insert(.provisional) } + let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: options) + if granted { + await MainActor.run { UIApplication.shared.registerForRemoteNotifications() } + } + return granted ? (provisional ? "provisional" : "authorized") : "denied" + } + } + + func getToken() throws -> Promise { + Promise.async { + guard let observed = VoidhashPushCollector.shared.currentToken() else { + throw RuntimeError.error(withMessage: "PUSH_TOKEN_NOT_OBSERVED") + } + return NativePushToken( + token: observed.token, + provider: .apns, + environment: observed.environment == .development ? .development : .production + ) + } + } + + func setBadgeCount(count: Double) throws -> Promise { + guard count >= 0, count.rounded() == count else { + throw RuntimeError.error(withMessage: "INVALID_BADGE_COUNT") + } + return Promise.async { + if #available(iOS 16.0, *) { + try await UNUserNotificationCenter.current().setBadgeCount(Int(count)) + } else { + await MainActor.run { UIApplication.shared.applicationIconBadgeNumber = Int(count) } + } + } + } + + func subscribe(subscriptionId: String, listener: @escaping (NativeNotificationEvent) -> Void) throws { + listeners[subscriptionId] = listener + } + + func unsubscribe(subscriptionId: String) throws { listeners.removeValue(forKey: subscriptionId) } +} diff --git a/libraries/react-native/ios/measurement/AppleIdentifierPolicy.swift b/libraries/react-native/ios/measurement/AppleIdentifierPolicy.swift new file mode 100644 index 000000000..c128196f4 --- /dev/null +++ b/libraries/react-native/ios/measurement/AppleIdentifierPolicy.swift @@ -0,0 +1,67 @@ +import Foundation + +enum AppleIdentifierKind: String, Sendable { + case idfa + case idfv + case appleAdsToken +} + +struct AppleIdentifierPolicy: Sendable { + let advertisingIdentifiers: Bool + let vendorIdentifiers: Bool + let collectionOptOut: Bool + let attStatus: String + let strictNoIdfa: Bool + + func permits(_ kind: AppleIdentifierKind) -> Bool { + guard !collectionOptOut else { return false } + switch kind { + case .idfa: + return !strictNoIdfa && advertisingIdentifiers && attStatus == "authorized" + case .idfv: + return vendorIdentifiers + case .appleAdsToken: + return advertisingIdentifiers + } + } +} + +struct AppleIdentifierObservation: Equatable, Sendable { + let kind: AppleIdentifierKind + let outcome: String + let protectedReference: String? +} + +final class AppleIdentifierCollector: @unchecked Sendable { + typealias Provider = @Sendable () throws -> String? + typealias Vault = @Sendable (_ kind: AppleIdentifierKind, _ value: String) throws -> String + + private let providers: [AppleIdentifierKind: Provider] + private let vault: Vault + + init(providers: [AppleIdentifierKind: Provider], vault: @escaping Vault) { + self.providers = providers + self.vault = vault + } + + func collect(_ kind: AppleIdentifierKind, policy: AppleIdentifierPolicy) -> AppleIdentifierObservation { + guard policy.permits(kind) else { + return AppleIdentifierObservation(kind: kind, outcome: "permissionDenied", protectedReference: nil) + } + guard let provider = providers[kind] else { + return AppleIdentifierObservation(kind: kind, outcome: "notInstalled", protectedReference: nil) + } + do { + guard let value = try provider(), !value.isEmpty else { + return AppleIdentifierObservation(kind: kind, outcome: "unavailable", protectedReference: nil) + } + return AppleIdentifierObservation( + kind: kind, + outcome: "collected", + protectedReference: try vault(kind, value) + ) + } catch { + return AppleIdentifierObservation(kind: kind, outcome: "error", protectedReference: nil) + } + } +} diff --git a/libraries/react-native/ios/measurement/AppleSystemIdentifiers.swift b/libraries/react-native/ios/measurement/AppleSystemIdentifiers.swift new file mode 100644 index 000000000..1825a9b19 --- /dev/null +++ b/libraries/react-native/ios/measurement/AppleSystemIdentifiers.swift @@ -0,0 +1,92 @@ +import Foundation + +#if os(iOS) && canImport(AdServices) +import AdServices +#endif + +#if os(iOS) && canImport(UIKit) +import UIKit +#endif + +#if os(iOS) && canImport(AppTrackingTransparency) && canImport(AdSupport) && !VOIDHASH_STRICT_NO_IDFA +import AdSupport +import AppTrackingTransparency +#endif + +struct AppleAttTransition: Equatable, Sendable { + let previous: String? + let current: String + let source: String + let observedAt: String +} + +final class AppleAttTransitionObserver: @unchecked Sendable { + typealias StatusProvider = @Sendable () -> String + + private let lock = NSLock() + private let statusProvider: StatusProvider + private var previous: String? + + init(previous: String? = nil, statusProvider: @escaping StatusProvider) { + self.previous = previous + self.statusProvider = statusProvider + } + + func observe(source: String, observedAt: String) -> AppleAttTransition? { + lock.lock() + defer { lock.unlock() } + let current = statusProvider() + guard current != previous else { return nil } + let transition = AppleAttTransition( + previous: previous, + current: current, + source: source, + observedAt: observedAt + ) + previous = current + return transition + } +} + +enum AppleSystemIdentifiers { + static var strictNoIdfa: Bool { + #if VOIDHASH_STRICT_NO_IDFA + true + #else + false + #endif + } + + static func attStatus() -> String { + #if os(iOS) && canImport(AppTrackingTransparency) && !VOIDHASH_STRICT_NO_IDFA + switch ATTrackingManager.trackingAuthorizationStatus { + case .notDetermined: return "notDetermined" + case .restricted: return "restricted" + case .denied: return "denied" + case .authorized: return "authorized" + @unknown default: return "restricted" + } + #else + return "restricted" + #endif + } + + static func providers() -> [AppleIdentifierKind: AppleIdentifierCollector.Provider] { + var result: [AppleIdentifierKind: AppleIdentifierCollector.Provider] = [:] + #if os(iOS) && canImport(UIKit) + result[.idfv] = { UIDevice.current.identifierForVendor?.uuidString } + #endif + #if os(iOS) && canImport(AppTrackingTransparency) && canImport(AdSupport) && !VOIDHASH_STRICT_NO_IDFA + result[.idfa] = { + let value = ASIdentifierManager.shared().advertisingIdentifier + return value == UUID(uuidString: "00000000-0000-0000-0000-000000000000") ? nil : value.uuidString + } + #endif + #if os(iOS) && canImport(AdServices) + if #available(iOS 14.3, *) { + result[.appleAdsToken] = { try AAAttribution.attributionToken() } + } + #endif + return result + } +} diff --git a/libraries/react-native/ios/measurement/ConversionValueEngine.swift b/libraries/react-native/ios/measurement/ConversionValueEngine.swift new file mode 100644 index 000000000..122102bbb --- /dev/null +++ b/libraries/react-native/ios/measurement/ConversionValueEngine.swift @@ -0,0 +1,285 @@ +import Foundation +#if os(iOS) +import StoreKit +#endif + +enum ConversionValueCoarse: String, Codable, Sendable { + case low + case medium + case high +} + +struct ConversionValueRule: Codable, Equatable, Sendable { + let eventName: String + let minimumCount: Int + let fineValue: Int + let coarseValue: ConversionValueCoarse? + let lockWindow: Bool + let window: Int +} + +struct ConversionValueEvaluation: Equatable, Sendable { + let fineValue: Int + let coarseValue: ConversionValueCoarse? + let lockWindow: Bool + let window: Int + let trace: [ConversionValueTraceEntry] +} + +struct ConversionValueTraceEntry: Codable, Equatable, Sendable { + let eventName: String + let matched: Bool + let minimumCount: Int + let observedCount: Int + let window: Int +} + +struct ConversionValueUpdate: Equatable, Sendable { + let fineValue: Int + let coarseValue: ConversionValueCoarse? + let lockWindow: Bool + let window: Int +} + +enum ConversionValueUpdateOutcome: String, Codable, Sendable { + case succeeded + case failed + case policyBlocked +} + +struct ConversionValueEvidence: Codable, Equatable, Sendable { + let ruleVersion: Int64 + let fineValue: Int + let coarseValue: ConversionValueCoarse? + let lockWindow: Bool + let window: Int + let outcome: ConversionValueUpdateOutcome + let errorCode: String? + let trace: [ConversionValueTraceEntry] +} + +enum ConversionValueEngineError: Error, Equatable { + case invalidRules + case versionReplay +} + +protocol ConversionValuePlatformAdapter: Sendable { + var supportsSKAdNetwork: Bool { get } + var supportsAdAttributionKit: Bool { get } + func updateSKAdNetwork(_ update: ConversionValueUpdate) async throws + func updateAdAttributionKit(_ update: ConversionValueUpdate) async throws +} + +struct SystemConversionValuePlatformAdapter: ConversionValuePlatformAdapter { + var supportsSKAdNetwork: Bool { + #if os(iOS) + if #available(iOS 14.0, *) { return true } + #endif + return false + } + + var supportsAdAttributionKit: Bool { false } + + func updateSKAdNetwork(_ update: ConversionValueUpdate) async throws { + #if os(iOS) + if #available(iOS 16.1, *) { + let coarse = update.coarseValue.map { + switch $0 { + case .low: return SKAdNetwork.CoarseConversionValue.low + case .medium: return SKAdNetwork.CoarseConversionValue.medium + case .high: return SKAdNetwork.CoarseConversionValue.high + } + } + try await withCheckedThrowingContinuation { continuation in + SKAdNetwork.updatePostbackConversionValue( + update.fineValue, + coarseValue: coarse, + lockWindow: update.lockWindow + ) { error in + if let error { continuation.resume(throwing: error) } + else { continuation.resume() } + } + } + return + } + if #available(iOS 14.0, *) { + SKAdNetwork.updateConversionValue(update.fineValue) + return + } + #endif + throw ConversionValuePlatformError.unsupported + } + + func updateAdAttributionKit(_ update: ConversionValueUpdate) async throws { + _ = update + throw ConversionValuePlatformError.unsupported + } +} + +private enum ConversionValuePlatformError: Error { case unsupported } + +final class ConversionValueEngine: @unchecked Sendable { + typealias PersistRules = @Sendable (_ version: Int64, _ rules: [ConversionValueRule]) throws -> Bool + typealias EvidenceSink = @Sendable (ConversionValueEvidence) throws -> Void + + private let adapter: ConversionValuePlatformAdapter + private let evidenceSink: EvidenceSink + private let persistRules: PersistRules + private let lock = NSLock() + private var rules: [ConversionValueRule] + private var version: Int64 + private var lockedWindows = Set() + + init( + version: Int64 = 0, + rules: [ConversionValueRule] = [], + adapter: ConversionValuePlatformAdapter, + persistRules: @escaping PersistRules, + evidenceSink: @escaping EvidenceSink + ) { + self.version = version + self.rules = rules + self.adapter = adapter + self.persistRules = persistRules + self.evidenceSink = evidenceSink + } + + var capabilityState: String { + lock.withLock { + if rules.isEmpty { return "noRules" } + if !adapter.supportsSKAdNetwork && !adapter.supportsAdAttributionKit { return "unavailable" } + return "available" + } + } + + func applyRules(version: Int64, rules: [ConversionValueRule]) throws { + guard version > 0, rules.allSatisfy(Self.validRule) else { + throw ConversionValueEngineError.invalidRules + } + try lock.withLock { + guard version > self.version else { throw ConversionValueEngineError.versionReplay } + guard try persistRules(version, rules) else { throw ConversionValueEngineError.versionReplay } + self.version = version + self.rules = rules + } + } + + static func conversionWindow(elapsedSinceFirstLaunch: TimeInterval) -> Int? { + guard elapsedSinceFirstLaunch >= 0 else { return nil } + let day: TimeInterval = 24 * 60 * 60 + if elapsedSinceFirstLaunch < 2 * day { return 1 } + if elapsedSinceFirstLaunch < 7 * day { return 2 } + if elapsedSinceFirstLaunch < 35 * day { return 3 } + return nil + } + + static func evaluate( + rules: [ConversionValueRule], + eventCounts: [String: Int], + window: Int + ) -> ConversionValueEvaluation? { + let windowRules = rules.filter { $0.window == window } + let trace = windowRules.map { rule in + let observed = max(0, eventCounts[rule.eventName] ?? 0) + return ConversionValueTraceEntry( + eventName: rule.eventName, + matched: observed >= rule.minimumCount, + minimumCount: rule.minimumCount, + observedCount: observed, + window: rule.window + ) + } + guard let selected = zip(windowRules, trace) + .filter({ $0.1.matched }) + .map(\.0) + .sorted(by: { + if $0.fineValue != $1.fineValue { return $0.fineValue > $1.fineValue } + return $0.eventName < $1.eventName + }) + .first + else { return nil } + return ConversionValueEvaluation( + fineValue: selected.fineValue, + coarseValue: selected.coarseValue, + lockWindow: selected.lockWindow, + window: window, + trace: trace + ) + } + + @discardableResult + func update( + eventCounts: [String: Int], + elapsedSinceFirstLaunch: TimeInterval, + attributionAllowed: Bool + ) async -> ConversionValueEvidence? { + guard let window = Self.conversionWindow(elapsedSinceFirstLaunch: elapsedSinceFirstLaunch) else { + return recordFailure(window: 3, eventCounts: eventCounts, code: "windowClosed") + } + let snapshot = lock.withLock { (version, rules, lockedWindows.contains(window)) } + guard let evaluation = Self.evaluate(rules: snapshot.1, eventCounts: eventCounts, window: window) else { + return nil + } + if !attributionAllowed { + return record(evaluation, version: snapshot.0, outcome: .policyBlocked, errorCode: "policyDenied") + } + if snapshot.2 { + return record(evaluation, version: snapshot.0, outcome: .failed, errorCode: "windowLocked") + } + + let update = ConversionValueUpdate( + fineValue: evaluation.fineValue, + coarseValue: evaluation.coarseValue, + lockWindow: evaluation.lockWindow, + window: evaluation.window + ) + do { + if adapter.supportsSKAdNetwork { try await adapter.updateSKAdNetwork(update) } + if adapter.supportsAdAttributionKit { try await adapter.updateAdAttributionKit(update) } + if !adapter.supportsSKAdNetwork && !adapter.supportsAdAttributionKit { + return record(evaluation, version: snapshot.0, outcome: .failed, errorCode: "frameworkUnavailable") + } + if evaluation.lockWindow { lock.withLock { _ = lockedWindows.insert(window) } } + return record(evaluation, version: snapshot.0, outcome: .succeeded, errorCode: nil) + } catch { + return record(evaluation, version: snapshot.0, outcome: .failed, errorCode: "platformApiFailed") + } + } + + private static func validRule(_ rule: ConversionValueRule) -> Bool { + !rule.eventName.isEmpty && rule.minimumCount > 0 && (0 ... 63).contains(rule.fineValue) && + (1 ... 3).contains(rule.window) + } + + private func recordFailure( + window: Int, + eventCounts: [String: Int], + code: String + ) -> ConversionValueEvidence? { + let snapshot = lock.withLock { (version, rules) } + guard let evaluation = Self.evaluate(rules: snapshot.1, eventCounts: eventCounts, window: window) else { + return nil + } + return record(evaluation, version: snapshot.0, outcome: .failed, errorCode: code) + } + + private func record( + _ evaluation: ConversionValueEvaluation, + version: Int64, + outcome: ConversionValueUpdateOutcome, + errorCode: String? + ) -> ConversionValueEvidence { + let evidence = ConversionValueEvidence( + ruleVersion: version, + fineValue: evaluation.fineValue, + coarseValue: evaluation.coarseValue, + lockWindow: evaluation.lockWindow, + window: evaluation.window, + outcome: outcome, + errorCode: errorCode, + trace: evaluation.trace + ) + try? evidenceSink(evidence) + return evidence + } +} diff --git a/libraries/react-native/ios/measurement/LinkCollector.swift b/libraries/react-native/ios/measurement/LinkCollector.swift new file mode 100644 index 000000000..15796e382 --- /dev/null +++ b/libraries/react-native/ios/measurement/LinkCollector.swift @@ -0,0 +1,61 @@ +import CryptoKit +import Foundation + +/** Captures iOS link callbacks into the encrypted, pre-JavaScript inbox. */ +@objcMembers public final class VoidhashLinkCollector: NSObject, @unchecked Sendable { + public static let shared = VoidhashLinkCollector() + + private let lock = NSLock() + private lazy var store: MeasurementStore? = try? MeasurementStore() + + /** Captures a Universal Link, custom-scheme URL, or launch URL before React Native starts. */ + @discardableResult + public func capture( + url: URL, + source: String, + appState: String + ) -> Bool { + lock.withLock { + guard let store else { return false } + return (try? Self.capture( + store: store, + raw: url.absoluteString, + source: source, + appState: appState + )) ?? false + } + } + + @discardableResult + static func capture( + store: MeasurementStore, + raw: String, + source: String, + appState: String, + now: Date = Date() + ) throws -> Bool { + let digest = SHA256.hash(data: Data(raw.utf8)).map { String(format: "%02x", $0) }.joined() + let nowMs = Int64(now.timeIntervalSince1970 * 1_000) + guard try store.checkAndSetDedupe( + namespace: "native-link-capture", + key: digest, + expiresAtMs: nowMs + 30_000 + ) else { return false } + let blobId = "link-\(UUID().uuidString.lowercased())" + _ = try store.putProtectedEvidence( + blobId: blobId, + purpose: "link-capture", + consentRevision: 0, + retentionClass: "installation", + value: Data(raw.utf8) + ) + return try store.appendInbox( + id: "inbox-\(UUID().uuidString.lowercased())", + kind: "link", + source: source, + appState: appState, + receivedAt: ISO8601DateFormatter().string(from: now), + protectedPayloadRef: blobId + ) + } +} diff --git a/libraries/react-native/ios/measurement/MeasurementDelivery.swift b/libraries/react-native/ios/measurement/MeasurementDelivery.swift new file mode 100644 index 000000000..1a33797c9 --- /dev/null +++ b/libraries/react-native/ios/measurement/MeasurementDelivery.swift @@ -0,0 +1,356 @@ +import Foundation + +struct MeasurementNativeDeliveryResult { + let accepted: Int + let scheduled: Int + let quarantined: Int + let policyBlocked: Int +} + +final class MeasurementDelivery { + private let store: MeasurementStore + private let publishableKey: String + private let ingestOrigin: URL + private let session: URLSession + + init(store: MeasurementStore, publishableKey: String, ingestOrigin: URL, session: URLSession = .shared) { + self.store = store + self.publishableKey = publishableKey + self.ingestOrigin = ingestOrigin + self.session = session + } + + func flush() async -> MeasurementNativeDeliveryResult { + do { + let records = try store.peekEligible(limit: 100) + guard !records.isEmpty else { return .empty } + let deletions = records.filter { $0.recordType == "measurement.deletion_requested.v1" } + var deletionResult = MeasurementNativeDeliveryResult.empty + for record in deletions { + deletionResult = combine(deletionResult, await deliverDeletion(record)) + } + let protected = await prepareProtectedEvidence( + records.filter { $0.recordType != "measurement.deletion_requested.v1" } + ) + return combine(deletionResult, combine(protected.result, await deliver(protected.ready))) + } catch { + return .empty + } + } + + private func deliverDeletion( + _ record: MeasurementStoredOutboxRecord + ) async -> MeasurementNativeDeliveryResult { + let response: (Data, HTTPURLResponse) + do { + response = try await sendDeletion(record) + } catch { + schedule([record], retryAfterMs: nil) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: 1, quarantined: 0, policyBlocked: 0) + } + if response.1.statusCode == 429 || response.1.statusCode >= 500 { + let retryAfter = response.1.value(forHTTPHeaderField: "retry-after") + .flatMap(Int64.init) + .map { $0 * 1_000 } + schedule([record], retryAfterMs: retryAfter) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: 1, quarantined: 0, policyBlocked: 0) + } + if (200...299).contains(response.1.statusCode) { + _ = try? store.acknowledge(recordId: record.recordId) + return MeasurementNativeDeliveryResult(accepted: 1, scheduled: 0, quarantined: 0, policyBlocked: 0) + } + _ = try? store.reject(recordId: record.recordId, reason: "deletion_http_\(response.1.statusCode)") + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: 0, quarantined: 1, policyBlocked: 0) + } + + private func prepareProtectedEvidence( + _ records: [MeasurementStoredOutboxRecord] + ) async -> (ready: [MeasurementStoredOutboxRecord], result: MeasurementNativeDeliveryResult) { + var ready: [MeasurementStoredOutboxRecord] = [] + var scheduled = 0 + var quarantined = 0 + var outcomes: [String: ProtectedOutcome] = [:] + for record in records { + guard let reference = record.protectedPayloadRef else { + ready.append(record) + continue + } + let outcome: ProtectedOutcome + if let existing = outcomes[reference] { + outcome = existing + } else { + outcome = await uploadProtectedEvidence(reference) + outcomes[reference] = outcome + } + switch outcome { + case .accepted: + ready.append(record) + case .retry: + _ = try? store.scheduleRetry( + recordId: record.recordId, + eligibleAtMs: Int64(Date().timeIntervalSince1970 * 1_000) + 1_000 + ) + scheduled += 1 + case .rejected: + _ = try? store.reject(recordId: record.recordId, reason: "protected_evidence_rejected") + quarantined += 1 + } + } + return ( + ready, + MeasurementNativeDeliveryResult( + accepted: 0, + scheduled: scheduled, + quarantined: quarantined, + policyBlocked: 0 + ) + ) + } + + private func uploadProtectedEvidence(_ blobId: String) async -> ProtectedOutcome { + guard let evidence = try? store.getProtectedUpload(blobId: blobId) else { + return .rejected + } + if evidence.uploadState == "acknowledged" { return .accepted } + guard evidence.uploadState == "pending", + evidence.deletionState == "active", + evidence.ciphertext != nil else { return .rejected } + let now = Int64(Date().timeIntervalSince1970 * 1_000) + if evidence.eligibleAtMs > now { return .retry } + let response: (Data, HTTPURLResponse) + do { + response = try await sendProtected(evidence) + } catch { + scheduleProtected(evidence, retryAfterMs: nil) + return .retry + } + if (200...299).contains(response.1.statusCode) { + _ = try? store.acknowledgeProtectedUpload(blobId: blobId) + return .accepted + } + if response.1.statusCode == 429 || response.1.statusCode >= 500 { + let retryAfter = response.1.value(forHTTPHeaderField: "retry-after") + .flatMap(Int64.init) + .map { $0 * 1_000 } + scheduleProtected(evidence, retryAfterMs: retryAfter) + return .retry + } + _ = try? store.rejectProtectedUpload(blobId: blobId) + return .rejected + } + + private func deliver(_ records: [MeasurementStoredOutboxRecord]) async -> MeasurementNativeDeliveryResult { + guard !records.isEmpty else { return .empty } + let response: (Data, HTTPURLResponse) + do { + response = try await send(records) + } catch { + schedule(records, retryAfterMs: nil) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: records.count, quarantined: 0, policyBlocked: 0) + } + if response.1.statusCode == 413 { + if records.count == 1 { + _ = try? store.reject(recordId: records[0].recordId, reason: "payload_too_large", quarantine: true) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: 0, quarantined: 1, policyBlocked: 0) + } + let middle = records.count / 2 + return combine( + await deliver(Array(records[..= 500 { + let retryAfter = response.1.value(forHTTPHeaderField: "retry-after") + .flatMap(Int64.init) + .map { $0 * 1_000 } + schedule(records, retryAfterMs: retryAfter) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: records.count, quarantined: 0, policyBlocked: 0) + } + guard (200...299).contains(response.1.statusCode) else { + for record in records { + _ = try? store.reject(recordId: record.recordId, reason: "http_\(response.1.statusCode)") + } + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: 0, quarantined: records.count, policyBlocked: 0) + } + guard let body = try? JSONSerialization.jsonObject(with: response.0) as? [String: Any] else { + schedule(records, retryAfterMs: nil) + return MeasurementNativeDeliveryResult(accepted: 0, scheduled: records.count, quarantined: 0, policyBlocked: 0) + } + let acceptedIds = body["accepted"] as? [String] ?? [] + let rejected = body["rejected"] as? [[String: Any]] ?? [] + var accepted = 0 + for id in acceptedIds where (try? store.acknowledge(recordId: id)) == true { accepted += 1 } + var quarantined = 0 + for item in rejected { + guard let id = item["recordId"] as? String, let reason = item["reason"] as? String else { continue } + if (try? store.reject(recordId: id, reason: reason)) == true { quarantined += 1 } + } + let handled = Set(acceptedIds).union(rejected.compactMap { $0["recordId"] as? String }) + let missing = records.filter { !handled.contains($0.recordId) } + schedule(missing, retryAfterMs: nil) + return MeasurementNativeDeliveryResult( + accepted: accepted, + scheduled: missing.count, + quarantined: quarantined, + policyBlocked: 0 + ) + } + + private func send(_ records: [MeasurementStoredOutboxRecord]) async throws -> (Data, HTTPURLResponse) { + var request = URLRequest(url: ingestOrigin.appendingPathComponent("i/v1/batch")) + request.httpMethod = "POST" + request.timeoutInterval = 15 + request.setValue("application/json", forHTTPHeaderField: "content-type") + request.setValue("application/json", forHTTPHeaderField: "accept") + request.httpBody = try JSONSerialization.data(withJSONObject: [ + "token": publishableKey, + "sent_at": ISO8601DateFormatter().string(from: Date()), + "events": records.map(captureEvent), + ], options: [.sortedKeys]) + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + return (data, http) + } + + private func sendProtected( + _ evidence: MeasurementStoredProtectedUpload + ) async throws -> (Data, HTTPURLResponse) { + var request = URLRequest(url: ingestOrigin.appendingPathComponent("i/v1/measurement/protected")) + request.httpMethod = "POST" + request.timeoutInterval = 15 + request.setValue("application/json", forHTTPHeaderField: "content-type") + request.setValue("application/json", forHTTPHeaderField: "accept") + request.httpBody = try JSONSerialization.data(withJSONObject: [ + "blobId": evidence.blobId, + "ciphertext": evidence.ciphertext?.base64EncodedString() ?? "", + "consentRevision": evidence.consentRevision, + "deletionState": evidence.deletionState, + "encryptionKeyVersion": evidence.encryptionKeyVersion, + "installationId": try store.snapshot().installationId, + "purpose": evidence.purpose, + "retentionClass": evidence.retentionClass, + "token": publishableKey, + ], options: [.sortedKeys]) + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + return (data, http) + } + + private func sendDeletion( + _ record: MeasurementStoredOutboxRecord + ) async throws -> (Data, HTTPURLResponse) { + guard let data = record.publicPayload.data(using: .utf8), + let envelope = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw URLError(.cannotParseResponse) + } + let payload = envelope["publicPayload"] as? [String: Any] ?? [:] + let identity = envelope["identity"] as? [String: Any] ?? [:] + var body: [String: Any] = [ + "installationId": envelope["installationId"] as? String ?? "", + "requestId": payload["requestId"] as? String ?? record.recordId, + "requestedAt": envelope["occurredAt"] as? String ?? ISO8601DateFormatter().string(from: Date()), + "token": publishableKey, + ] + if let personId = identity["personId"] as? String, !personId.isEmpty { + body["personId"] = personId + } + var request = URLRequest(url: ingestOrigin.appendingPathComponent("i/v1/measurement/delete")) + request.httpMethod = "POST" + request.timeoutInterval = 15 + request.setValue("application/json", forHTTPHeaderField: "content-type") + request.setValue("application/json", forHTTPHeaderField: "accept") + request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.sortedKeys]) + let (responseData, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } + return (responseData, http) + } + + private func captureEvent(_ record: MeasurementStoredOutboxRecord) -> [String: Any] { + guard let data = record.publicPayload.data(using: .utf8), + let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return ["uuid": record.recordId, "event": record.recordType] + } + let identity = envelope["identity"] as? [String: Any] ?? [:] + let consent = envelope["consent"] as? [String: Any] ?? [:] + var event: [String: Any] = [ + "uuid": record.recordId, + "event": record.recordType, + "timestamp": envelope["occurredAt"] as? String ?? ISO8601DateFormatter().string(from: Date()), + "distinct_id": identity["distinctId"] as? String ?? envelope["installationId"] as? String ?? "unknown", + "properties": envelope["publicPayload"] as? [String: Any] ?? [:], + "context": [ + "schemaVersion": 1, + "installation": [ + "id": envelope["installationId"] as? String ?? "", + "sequence": envelope["installationSequence"] as? Int64 ?? record.sequence, + ], + "identity": identity, + "consentRevision": consent["revision"] as? Int64 ?? 0, + "app": envelope["app"] as? [String: Any] ?? [:], + "device": envelope["device"] as? [String: Any] ?? [:], + "measurement": [ + "recordType": record.recordType, + "source": envelope["source"] as? String ?? "native", + ], + ], + ] + if let session = envelope["session"] as? [String: Any], let id = session["id"] as? String { + event["session_id"] = id + } + return event + } + + private func schedule(_ records: [MeasurementStoredOutboxRecord], retryAfterMs: Int64?) { + let now = Int64(Date().timeIntervalSince1970 * 1_000) + for record in records { + let exponent = min(record.attemptCount, 12) + let exponential = min(Int64(3_600_000), Int64(1_000) << exponent) + let stableHash = record.recordId.utf8.reduce(UInt64(2_166_136_261)) { + ($0 ^ UInt64($1)) &* 16_777_619 + } + let jitter = Int64(800 + stableHash % 401) + let computed = exponential * jitter / 1_000 + _ = try? store.scheduleRetry( + recordId: record.recordId, + eligibleAtMs: now + max(computed, retryAfterMs ?? 0) + ) + } + } + + private func scheduleProtected(_ evidence: MeasurementStoredProtectedUpload, retryAfterMs: Int64?) { + let exponent = min(evidence.attemptCount, 12) + let exponential = min(Int64(3_600_000), Int64(1_000) << exponent) + let stableHash = evidence.blobId.utf8.reduce(UInt64(2_166_136_261)) { + ($0 ^ UInt64($1)) &* 16_777_619 + } + let jitter = Int64(800 + stableHash % 401) + let computed = exponential * jitter / 1_000 + _ = try? store.scheduleProtectedUpload( + blobId: evidence.blobId, + eligibleAtMs: Int64(Date().timeIntervalSince1970 * 1_000) + max(computed, retryAfterMs ?? 0) + ) + } + + private func combine(_ left: MeasurementNativeDeliveryResult, _ right: MeasurementNativeDeliveryResult) -> MeasurementNativeDeliveryResult { + MeasurementNativeDeliveryResult( + accepted: left.accepted + right.accepted, + scheduled: left.scheduled + right.scheduled, + quarantined: left.quarantined + right.quarantined, + policyBlocked: left.policyBlocked + right.policyBlocked + ) + } +} + +private enum ProtectedOutcome { + case accepted + case retry + case rejected +} + +private extension MeasurementNativeDeliveryResult { + static let empty = MeasurementNativeDeliveryResult(accepted: 0, scheduled: 0, quarantined: 0, policyBlocked: 0) +} diff --git a/libraries/react-native/ios/measurement/MeasurementStore.swift b/libraries/react-native/ios/measurement/MeasurementStore.swift new file mode 100644 index 000000000..2343f9741 --- /dev/null +++ b/libraries/react-native/ios/measurement/MeasurementStore.swift @@ -0,0 +1,842 @@ +import CryptoKit +import Foundation +import Security +import SQLite3 + +struct MeasurementStoredOutboxRecord { + let recordId: String + let recordType: String + let sequence: Int64 + let priority: String + let publicPayload: String + let protectedPayloadRef: String? + let attemptCount: Int +} + +struct MeasurementStoredInboxEntry { + let id: String + let kind: String + let source: String + let appState: String + let receivedAt: String + let protectedPayloadRef: String +} + +struct MeasurementStoreSnapshotValue { + let installationId: String + let firstOpenedAt: String + let sequence: Int64 + let counts: [String: Int] + let oldestQueuedAtMs: Int64? +} + +struct MeasurementStoredProtectedEvidence { + let blobId: String + let purpose: String + let consentRevision: Int64 + let retentionClass: String + let encryptionKeyVersion: Int + let deletionState: String + let value: Data +} + +struct MeasurementStoredProtectedUpload { + let blobId: String + let purpose: String + let consentRevision: Int64 + let retentionClass: String + let encryptionKeyVersion: Int + let deletionState: String + let ciphertext: Data? + let uploadState: String + let attemptCount: Int + let eligibleAtMs: Int64 +} + +struct MeasurementStoredConfigurationState { + let version: Int64 + let payload: Data? +} + +final class MeasurementStore: @unchecked Sendable { + private let lock = NSRecursiveLock() + private var database: OpaquePointer? + private let crypto = MeasurementVaultCrypto() + private var maxOutboxRecords: Int64 + private var maxOutboxBytes: Int64 + private var maxProtectedBytes: Int64 + private let maxDedupeRecords: Int64 + private let maxInboxRecords: Int64 + + let databaseURL: URL + + init( + maxOutboxRecords: Int64 = 10_000, + maxOutboxBytes: Int64 = 20 * 1024 * 1024, + maxProtectedBytes: Int64 = 20 * 1024 * 1024, + maxDedupeRecords: Int64 = 25_000, + maxInboxRecords: Int64 = 1_000, + databaseURL overrideDatabaseURL: URL? = nil + ) throws { + self.maxOutboxRecords = maxOutboxRecords + self.maxOutboxBytes = maxOutboxBytes + self.maxProtectedBytes = maxProtectedBytes + self.maxDedupeRecords = maxDedupeRecords + self.maxInboxRecords = maxInboxRecords + + let base = overrideDatabaseURL?.deletingLastPathComponent() + ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("voidhash-measurement", isDirectory: true) + try FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) + var values = URLResourceValues() + values.isExcludedFromBackup = true + var excluded = base + try excluded.setResourceValues(values) + databaseURL = overrideDatabaseURL ?? base.appendingPathComponent("measurement.sqlite") + if sqlite3_open_v2( + databaseURL.path, + &database, + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + nil + ) != SQLITE_OK { + throw failure("MEASUREMENT_STORE_OPEN_FAILED") + } + try execute("PRAGMA journal_mode=WAL") + try execute("PRAGMA synchronous=FULL") + try migrate() + try ensureInstallation() + } + + deinit { + sqlite3_close(database) + } + + func snapshot() throws -> MeasurementStoreSnapshotValue { + try locked { + try ensureInstallation() + let installation = try query( + "SELECT installation_id, first_opened_at, sequence FROM installation WHERE singleton = 1" + ) { statement in + ( + text(statement, 0), + text(statement, 1), + sqlite3_column_int64(statement, 2) + ) + }.first + guard let installation else { throw failure("MEASUREMENT_INSTALLATION_MISSING") } + var counts: [String: Int] = [:] + for (priority, count) in try query( + "SELECT priority, COUNT(*) FROM outbox WHERE acknowledgement_state = 'pending' GROUP BY priority" + , row: { (text($0, 0), Int(sqlite3_column_int64($0, 1))) }) { + counts[priority] = count + } + let oldest = try query( + "SELECT MIN(queued_at_ms) FROM outbox WHERE acknowledgement_state = 'pending'" + ) { statement -> Int64? in + sqlite3_column_type(statement, 0) == SQLITE_NULL ? nil : sqlite3_column_int64(statement, 0) + }.first ?? nil + return MeasurementStoreSnapshotValue( + installationId: installation.0, + firstOpenedAt: installation.1, + sequence: installation.2, + counts: counts, + oldestQueuedAtMs: oldest + ) + } + } + + func enqueue( + recordId: String, + recordType: String, + occurredAt: String, + priority: String, + source: String, + publicPayload: String, + protectedPayloadRef: String?, + queuedAtMs: Int64 = Int64(Date().timeIntervalSince1970 * 1_000) + ) throws -> Int64 { + try locked { + try transaction { + try ensureInstallation() + if let existing = try scalarInt64( + "SELECT installation_sequence FROM outbox WHERE record_id = ?", + bindings: [.text(recordId)] + ) { + return existing + } + let bytes = Int64(publicPayload.utf8.count) + try evictFor(additionalBytes: bytes) + try execute("UPDATE installation SET sequence = sequence + 1 WHERE singleton = 1") + guard let sequence = try scalarInt64("SELECT sequence FROM installation WHERE singleton = 1") else { + throw failure("MEASUREMENT_SEQUENCE_MISSING") + } + try execute( + """ + INSERT INTO outbox ( + record_id, record_type, installation_sequence, occurred_at, queued_at_ms, + priority, source, public_payload, public_payload_bytes, protected_payload_ref, + attempt_count, eligible_at_ms, acknowledgement_state + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 'pending') + """, + bindings: [ + .text(recordId), .text(recordType), .int(sequence), .text(occurredAt), + .int(queuedAtMs), .text(priority.lowercased()), .text(source.lowercased()), + .text(publicPayload), .int(bytes), protectedPayloadRef.map(SQLiteBinding.text) ?? .null, + .int(queuedAtMs), + ] + ) + return sequence + } + } + } + + func peekEligible(limit: Int, nowMs: Int64 = Int64(Date().timeIntervalSince1970 * 1_000)) throws -> [MeasurementStoredOutboxRecord] { + try locked { + try query( + """ + SELECT record_id, record_type, installation_sequence, priority, public_payload, + protected_payload_ref, attempt_count + FROM outbox + WHERE acknowledgement_state = 'pending' AND eligible_at_ms <= ? + ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END, + installation_sequence ASC + LIMIT ? + """, + bindings: [.int(nowMs), .int(Int64(max(0, limit)))] + ) { statement in + MeasurementStoredOutboxRecord( + recordId: text(statement, 0), + recordType: text(statement, 1), + sequence: sqlite3_column_int64(statement, 2), + priority: text(statement, 3), + publicPayload: text(statement, 4), + protectedPayloadRef: optionalText(statement, 5), + attemptCount: Int(sqlite3_column_int(statement, 6)) + ) + } + } + } + + @discardableResult + func acknowledge(recordId: String) throws -> Bool { + try locked { + try execute( + "UPDATE outbox SET acknowledgement_state = 'acknowledged' WHERE record_id = ? AND acknowledgement_state != 'acknowledged'", + bindings: [.text(recordId)] + ) + return sqlite3_changes(database) > 0 + } + } + + @discardableResult + func scheduleRetry(recordId: String, eligibleAtMs: Int64) throws -> Bool { + try locked { + try execute( + "UPDATE outbox SET attempt_count = attempt_count + 1, eligible_at_ms = ? WHERE record_id = ? AND acknowledgement_state = 'pending'", + bindings: [.int(eligibleAtMs), .text(recordId)] + ) + return sqlite3_changes(database) > 0 + } + } + + @discardableResult + func reject(recordId: String, reason: String, quarantine: Bool = false) throws -> Bool { + try locked { + try transaction { + try writeDiagnostic( + recordId: recordId, + outcome: quarantine ? "quarantined" : "rejected", + reason: reason + ) + try execute( + "UPDATE outbox SET acknowledgement_state = ? WHERE record_id = ? AND acknowledgement_state = 'pending'", + bindings: [.text(quarantine ? "quarantined" : "rejected"), .text(recordId)] + ) + return sqlite3_changes(database) > 0 + } + } + } + + func putProtectedEvidence( + blobId: String = "blob_\(UUID().uuidString.lowercased())", + purpose: String, + consentRevision: Int64, + retentionClass: String, + value: Data, + keyVersion: Int? = nil + ) throws -> String { + try locked { + let version = keyVersion ?? crypto.currentVersion + let encrypted = try crypto.encrypt(value, version: version) + let current = try scalarInt64( + "SELECT COALESCE(SUM(LENGTH(ciphertext)), 0) FROM protected_evidence WHERE deletion_state = 'active'" + ) ?? 0 + guard current + Int64(encrypted.count) <= maxProtectedBytes else { + throw failure("MEASUREMENT_PROTECTED_VAULT_BOUND") + } + try execute( + """ + INSERT OR IGNORE INTO protected_evidence ( + blob_id, purpose, consent_revision, retention_class, encryption_key_version, + deletion_state, ciphertext, created_at_ms + ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?) + """, + bindings: [ + .text(blobId), .text(purpose), .int(consentRevision), .text(retentionClass), + .int(Int64(version)), .blob(encrypted), + .int(Int64(Date().timeIntervalSince1970 * 1_000)), + ] + ) + return blobId + } + } + + func getProtectedEvidence(blobId: String) throws -> MeasurementStoredProtectedEvidence? { + try locked { + try query( + "SELECT purpose, consent_revision, retention_class, encryption_key_version, deletion_state, ciphertext FROM protected_evidence WHERE blob_id = ?", + bindings: [.text(blobId)] + ) { statement in + let version = Int(sqlite3_column_int(statement, 3)) + return MeasurementStoredProtectedEvidence( + blobId: blobId, + purpose: text(statement, 0), + consentRevision: sqlite3_column_int64(statement, 1), + retentionClass: text(statement, 2), + encryptionKeyVersion: version, + deletionState: text(statement, 4), + value: try crypto.decrypt(blob(statement, 5), version: version) + ) + }.first + } + } + + @discardableResult + func deleteProtectedEvidence(blobId: String) throws -> Bool { + try locked { + try execute( + "UPDATE protected_evidence SET deletion_state = 'deleted', ciphertext = NULL WHERE blob_id = ? AND deletion_state != 'deleted'", + bindings: [.text(blobId)] + ) + return sqlite3_changes(database) > 0 + } + } + + func deleteProtectedData(requestId: String) throws -> Bool { + try locked { + try transaction { + try execute( + "UPDATE protected_evidence SET deletion_state = 'deleted', ciphertext = NULL, upload_state = 'rejected' WHERE deletion_state != 'deleted'" + ) + try execute( + "INSERT INTO state_revision (kind, revision, payload) VALUES ('deletion', 1, ?) ON CONFLICT(kind) DO UPDATE SET revision = state_revision.revision + 1, payload = excluded.payload", + bindings: [.text(requestId)] + ) + return true + } + } + } + + func measurementConfigurationState() throws -> MeasurementStoredConfigurationState { + try locked { + let row = try query( + "SELECT revision, payload FROM state_revision WHERE kind = 'measurement_configuration'" + ) { statement in + MeasurementStoredConfigurationState( + version: sqlite3_column_int64(statement, 0), + payload: optionalText(statement, 1)?.data(using: .utf8) + ) + }.first + return row ?? MeasurementStoredConfigurationState(version: 0, payload: nil) + } + } + + @discardableResult + func persistMeasurementConfiguration(version: Int64, payload: Data) throws -> Bool { + try locked { + try execute( + "INSERT INTO state_revision (kind, revision, payload) VALUES ('measurement_configuration', ?, ?) ON CONFLICT(kind) DO UPDATE SET revision = excluded.revision, payload = excluded.payload WHERE excluded.revision > state_revision.revision", + bindings: [.int(version), .text(String(decoding: payload, as: UTF8.self))] + ) + return sqlite3_changes(database) > 0 + } + } + + func pushRegistrationState() throws -> MeasurementStoredConfigurationState { + try locked { + let row = try query( + "SELECT revision, payload FROM state_revision WHERE kind = 'push_registration'" + ) { statement in + MeasurementStoredConfigurationState( + version: sqlite3_column_int64(statement, 0), + payload: optionalText(statement, 1)?.data(using: .utf8) + ) + }.first + return row ?? MeasurementStoredConfigurationState(version: 0, payload: nil) + } + } + + @discardableResult + func persistPushRegistration(payload: Data) throws -> Bool { + try locked { + try execute( + "INSERT INTO state_revision (kind, revision, payload) VALUES ('push_registration', 1, ?) ON CONFLICT(kind) DO UPDATE SET revision = state_revision.revision + 1, payload = excluded.payload", + bindings: [.text(String(decoding: payload, as: UTF8.self))] + ) + return sqlite3_changes(database) > 0 + } + } + + @discardableResult + func clearPushRegistration() throws -> Bool { + try locked { + try execute("DELETE FROM state_revision WHERE kind = 'push_registration'") + return sqlite3_changes(database) > 0 + } + } + + func testDeviceState() throws -> Bool { + try locked { + try query("SELECT payload FROM state_revision WHERE kind = 'test_device'") { + optionalText($0, 0) == "true" + }.first ?? false + } + } + + @discardableResult + func persistTestDeviceState(_ enabled: Bool) throws -> Bool { + try locked { + try execute( + "INSERT INTO state_revision (kind, revision, payload) VALUES ('test_device', 1, ?) ON CONFLICT(kind) DO UPDATE SET payload = excluded.payload", + bindings: [.text(enabled ? "true" : "false")] + ) + return sqlite3_changes(database) > 0 + } + } + + func applyStorageLimits( + maxOutboxRecords: Int64, + maxOutboxBytes: Int64, + maxProtectedBytes: Int64 + ) throws { + try locked { + guard maxOutboxRecords > 0, maxOutboxBytes > 0, maxProtectedBytes > 0 else { + throw failure("MEASUREMENT_INVALID_STORAGE_LIMITS") + } + self.maxOutboxRecords = maxOutboxRecords + self.maxOutboxBytes = maxOutboxBytes + self.maxProtectedBytes = maxProtectedBytes + try evictFor(additionalBytes: 0) + } + } + + func getProtectedUpload(blobId: String) throws -> MeasurementStoredProtectedUpload? { + try locked { + try query( + "SELECT purpose, consent_revision, retention_class, encryption_key_version, deletion_state, ciphertext, upload_state, upload_attempt_count, upload_eligible_at_ms FROM protected_evidence WHERE blob_id = ?", + bindings: [.text(blobId)] + ) { statement in + MeasurementStoredProtectedUpload( + blobId: blobId, + purpose: text(statement, 0), + consentRevision: sqlite3_column_int64(statement, 1), + retentionClass: text(statement, 2), + encryptionKeyVersion: Int(sqlite3_column_int(statement, 3)), + deletionState: text(statement, 4), + ciphertext: sqlite3_column_type(statement, 5) == SQLITE_NULL ? nil : blob(statement, 5), + uploadState: text(statement, 6), + attemptCount: Int(sqlite3_column_int(statement, 7)), + eligibleAtMs: sqlite3_column_int64(statement, 8) + ) + }.first + } + } + + @discardableResult + func acknowledgeProtectedUpload(blobId: String) throws -> Bool { + try locked { + try execute( + "UPDATE protected_evidence SET upload_state = 'acknowledged' WHERE blob_id = ? AND upload_state != 'acknowledged'", + bindings: [.text(blobId)] + ) + return sqlite3_changes(database) > 0 + } + } + + @discardableResult + func scheduleProtectedUpload(blobId: String, eligibleAtMs: Int64) throws -> Bool { + try locked { + try execute( + "UPDATE protected_evidence SET upload_attempt_count = upload_attempt_count + 1, upload_eligible_at_ms = ? WHERE blob_id = ? AND upload_state = 'pending'", + bindings: [.int(eligibleAtMs), .text(blobId)] + ) + return sqlite3_changes(database) > 0 + } + } + + @discardableResult + func rejectProtectedUpload(blobId: String) throws -> Bool { + try locked { + try execute( + "UPDATE protected_evidence SET upload_state = 'rejected' WHERE blob_id = ? AND upload_state = 'pending'", + bindings: [.text(blobId)] + ) + return sqlite3_changes(database) > 0 + } + } + + func rotateProtectedEvidenceKey(to version: Int) throws -> Int { + try locked { + guard version > 0 else { throw failure("MEASUREMENT_INVALID_KEY_VERSION") } + let rows = try query( + "SELECT blob_id, encryption_key_version, ciphertext FROM protected_evidence WHERE deletion_state = 'active'" + ) { statement in + ( + text(statement, 0), + Int(sqlite3_column_int(statement, 1)), + blob(statement, 2) + ) + } + try transaction { + for (blobId, oldVersion, ciphertext) in rows { + let plaintext = try crypto.decrypt(ciphertext, version: oldVersion) + try execute( + "UPDATE protected_evidence SET encryption_key_version = ?, ciphertext = ? WHERE blob_id = ?", + bindings: [.int(Int64(version)), .blob(try crypto.encrypt(plaintext, version: version)), .text(blobId)] + ) + } + } + crypto.currentVersion = version + return rows.count + } + } + + func checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Int64) throws -> Bool { + try locked { + try execute( + "DELETE FROM dedupe WHERE expires_at_ms <= ?", + bindings: [.int(Int64(Date().timeIntervalSince1970 * 1_000))] + ) + try execute( + "INSERT OR IGNORE INTO dedupe (namespace, dedupe_key, created_at_ms, expires_at_ms) VALUES (?, ?, ?, ?)", + bindings: [ + .text(namespace), .text(key), .int(Int64(Date().timeIntervalSince1970 * 1_000)), + .int(expiresAtMs), + ] + ) + let inserted = sqlite3_changes(database) > 0 + try trim(table: "dedupe", maximum: maxDedupeRecords, orderBy: "created_at_ms") + return inserted + } + } + + func hasDedupe(namespace: String, key: String) throws -> Bool { + try locked { + try execute( + "DELETE FROM dedupe WHERE expires_at_ms <= ?", + bindings: [.int(Int64(Date().timeIntervalSince1970 * 1_000))] + ) + return try scalarInt64( + "SELECT 1 FROM dedupe WHERE namespace = ? AND dedupe_key = ? LIMIT 1", + bindings: [.text(namespace), .text(key)] + ) != nil + } + } + + func appendInbox( + id: String, + kind: String, + source: String, + appState: String, + receivedAt: String, + protectedPayloadRef: String + ) throws -> Bool { + try locked { + try execute( + "INSERT OR IGNORE INTO inbox (entry_id, kind, source, app_state, received_at, protected_payload_ref, acknowledged) VALUES (?, ?, ?, ?, ?, ?, 0)", + bindings: [ + .text(id), .text(kind), .text(source), .text(appState), .text(receivedAt), + .text(protectedPayloadRef), + ] + ) + let inserted = sqlite3_changes(database) > 0 + try trim(table: "inbox", maximum: maxInboxRecords, orderBy: "rowid", where: "acknowledged = 1") + return inserted + } + } + + func peekInbox(limit: Int) throws -> [MeasurementStoredInboxEntry] { + try locked { + try query( + "SELECT entry_id, kind, source, app_state, received_at, protected_payload_ref FROM inbox WHERE acknowledged = 0 ORDER BY rowid ASC LIMIT ?", + bindings: [.int(Int64(max(0, limit)))] + ) { statement in + MeasurementStoredInboxEntry( + id: text(statement, 0), + kind: text(statement, 1), + source: text(statement, 2), + appState: text(statement, 3), + receivedAt: text(statement, 4), + protectedPayloadRef: text(statement, 5) + ) + } + } + } + + @discardableResult + func acknowledgeInbox(id: String) throws -> Bool { + try locked { + try execute( + "UPDATE inbox SET acknowledged = 1 WHERE entry_id = ? AND acknowledged = 0", + bindings: [.text(id)] + ) + return sqlite3_changes(database) > 0 + } + } + + private func migrate() throws { + let version = Int(try scalarInt64("PRAGMA user_version") ?? 0) + if version < 1 { + try transaction { + try execute("CREATE TABLE IF NOT EXISTS installation (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), installation_id TEXT NOT NULL, first_opened_at TEXT NOT NULL, sequence INTEGER NOT NULL DEFAULT 0, first_release TEXT, last_release TEXT)") + try execute("CREATE TABLE IF NOT EXISTS session (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), session_id TEXT, sequence INTEGER NOT NULL DEFAULT 0, started_at TEXT, last_foreground_monotonic_ms INTEGER, last_background_monotonic_ms INTEGER)") + try execute("CREATE TABLE IF NOT EXISTS state_revision (kind TEXT PRIMARY KEY, revision INTEGER NOT NULL, payload TEXT)") + try execute("CREATE TABLE IF NOT EXISTS outbox (record_id TEXT PRIMARY KEY, record_type TEXT NOT NULL, installation_sequence INTEGER NOT NULL, occurred_at TEXT NOT NULL, queued_at_ms INTEGER NOT NULL, priority TEXT NOT NULL, source TEXT NOT NULL, public_payload TEXT NOT NULL, public_payload_bytes INTEGER NOT NULL, protected_payload_ref TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, eligible_at_ms INTEGER NOT NULL, acknowledgement_state TEXT NOT NULL DEFAULT 'pending')") + try execute("CREATE INDEX IF NOT EXISTS outbox_eligible_idx ON outbox (acknowledgement_state, priority, eligible_at_ms, installation_sequence)") + try execute("CREATE TABLE IF NOT EXISTS protected_evidence (blob_id TEXT PRIMARY KEY, purpose TEXT NOT NULL, consent_revision INTEGER NOT NULL, retention_class TEXT NOT NULL, encryption_key_version INTEGER NOT NULL, deletion_state TEXT NOT NULL, ciphertext BLOB, created_at_ms INTEGER NOT NULL)") + try execute("CREATE TABLE IF NOT EXISTS dedupe (namespace TEXT NOT NULL, dedupe_key TEXT NOT NULL, created_at_ms INTEGER NOT NULL, expires_at_ms INTEGER NOT NULL, PRIMARY KEY (namespace, dedupe_key))") + try execute("PRAGMA user_version = 1") + } + } + if version < 2 { + try transaction { + try execute("CREATE TABLE IF NOT EXISTS inbox (entry_id TEXT PRIMARY KEY, kind TEXT NOT NULL, source TEXT NOT NULL, app_state TEXT NOT NULL, received_at TEXT NOT NULL, protected_payload_ref TEXT NOT NULL, acknowledged INTEGER NOT NULL DEFAULT 0)") + try execute("CREATE TABLE IF NOT EXISTS delivery_diagnostic (diagnostic_id TEXT PRIMARY KEY, record_id TEXT NOT NULL, outcome TEXT NOT NULL, reason TEXT NOT NULL, occurred_at_ms INTEGER NOT NULL)") + try execute("PRAGMA user_version = 2") + } + } + if version < 3 { + try transaction { + try execute("ALTER TABLE protected_evidence ADD COLUMN upload_state TEXT NOT NULL DEFAULT 'pending'") + try execute("ALTER TABLE protected_evidence ADD COLUMN upload_attempt_count INTEGER NOT NULL DEFAULT 0") + try execute("ALTER TABLE protected_evidence ADD COLUMN upload_eligible_at_ms INTEGER NOT NULL DEFAULT 0") + try execute("CREATE INDEX IF NOT EXISTS protected_evidence_upload_idx ON protected_evidence (upload_state, upload_eligible_at_ms)") + try execute("PRAGMA user_version = 3") + } + } + } + + private func ensureInstallation() throws { + try execute( + "INSERT OR IGNORE INTO installation (singleton, installation_id, first_opened_at, sequence) VALUES (1, ?, ?, 0)", + bindings: [ + .text("install_\(UUID().uuidString.lowercased())"), + .text(ISO8601DateFormatter().string(from: Date())), + ] + ) + } + + private func evictFor(additionalBytes: Int64) throws { + while try isOutboxOverBound(additionalBytes: additionalBytes) { + let candidate = try scalarText( + """ + SELECT record_id FROM outbox + WHERE acknowledgement_state = 'pending' + AND priority IN ('low', 'normal') + AND record_type NOT LIKE 'installation.%' + AND record_type NOT LIKE 'consent.%' + AND record_type NOT LIKE 'link.%' + AND record_type NOT LIKE 'referrer.%' + AND record_type NOT LIKE 'purchase.%' + ORDER BY CASE priority WHEN 'low' THEN 0 ELSE 1 END, installation_sequence ASC LIMIT 1 + """ + ) + guard let candidate else { throw failure("MEASUREMENT_OUTBOX_PROTECTED_BOUND") } + try writeDiagnostic(recordId: candidate, outcome: "evicted", reason: "storage_bound") + try execute("DELETE FROM outbox WHERE record_id = ?", bindings: [.text(candidate)]) + } + } + + private func isOutboxOverBound(additionalBytes: Int64) throws -> Bool { + let count = try scalarInt64( + "SELECT COUNT(*) FROM outbox WHERE acknowledgement_state = 'pending'" + ) ?? 0 + let bytes = try scalarInt64( + "SELECT COALESCE(SUM(public_payload_bytes), 0) FROM outbox WHERE acknowledgement_state = 'pending'" + ) ?? 0 + return count >= maxOutboxRecords || bytes + additionalBytes > maxOutboxBytes + } + + private func writeDiagnostic(recordId: String, outcome: String, reason: String) throws { + try execute( + "INSERT INTO delivery_diagnostic (diagnostic_id, record_id, outcome, reason, occurred_at_ms) VALUES (?, ?, ?, ?, ?)", + bindings: [ + .text("diag_\(UUID().uuidString.lowercased())"), .text(recordId), .text(outcome), + .text(String(reason.prefix(128))), .int(Int64(Date().timeIntervalSince1970 * 1_000)), + ] + ) + } + + private func trim(table: String, maximum: Int64, orderBy: String, where predicate: String? = nil) throws { + let clause = predicate.map { " WHERE \($0)" } ?? "" + let count = try scalarInt64("SELECT COUNT(*) FROM \(table)\(clause)") ?? 0 + guard count > maximum else { return } + try execute( + "DELETE FROM \(table) WHERE rowid IN (SELECT rowid FROM \(table)\(clause) ORDER BY \(orderBy) ASC LIMIT ?)", + bindings: [.int(count - maximum)] + ) + } + + private func transaction(_ operation: () throws -> T) throws -> T { + try execute("BEGIN IMMEDIATE TRANSACTION") + do { + let result = try operation() + try execute("COMMIT") + return result + } catch { + try? execute("ROLLBACK") + throw error + } + } + + private func locked(_ operation: () throws -> T) rethrows -> T { + lock.lock() + defer { lock.unlock() } + return try operation() + } + + private func execute(_ sql: String, bindings: [SQLiteBinding] = []) throws { + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else { + throw failure("MEASUREMENT_STORE_PREPARE_FAILED") + } + defer { sqlite3_finalize(statement) } + try bind(bindings, to: statement) + guard sqlite3_step(statement) == SQLITE_DONE || sql.hasPrefix("PRAGMA journal_mode") else { + throw failure("MEASUREMENT_STORE_EXECUTE_FAILED") + } + } + + private func query( + _ sql: String, + bindings: [SQLiteBinding] = [], + row: (OpaquePointer) throws -> T + ) throws -> [T] { + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK, + let statement else { throw failure("MEASUREMENT_STORE_PREPARE_FAILED") } + defer { sqlite3_finalize(statement) } + try bind(bindings, to: statement) + var rows: [T] = [] + while sqlite3_step(statement) == SQLITE_ROW { + rows.append(try row(statement)) + } + return rows + } + + private func bind(_ bindings: [SQLiteBinding], to statement: OpaquePointer?) throws { + for (offset, binding) in bindings.enumerated() { + let index = Int32(offset + 1) + let status: Int32 + switch binding { + case .text(let value): + status = sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT) + case .int(let value): + status = sqlite3_bind_int64(statement, index, value) + case .blob(let value): + status = value.withUnsafeBytes { pointer in + sqlite3_bind_blob(statement, index, pointer.baseAddress, Int32(value.count), SQLITE_TRANSIENT) + } + case .null: + status = sqlite3_bind_null(statement, index) + } + guard status == SQLITE_OK else { throw failure("MEASUREMENT_STORE_BIND_FAILED") } + } + } + + private func scalarInt64(_ sql: String, bindings: [SQLiteBinding] = []) throws -> Int64? { + try query(sql, bindings: bindings) { statement -> Int64? in + sqlite3_column_type(statement, 0) == SQLITE_NULL ? nil : sqlite3_column_int64(statement, 0) + }.first ?? nil + } + + private func scalarText(_ sql: String, bindings: [SQLiteBinding] = []) throws -> String? { + try query(sql, bindings: bindings) { statement -> String? in optionalText(statement, 0) }.first ?? nil + } + + private func failure(_ fallback: String) -> NSError { + let message = database.flatMap(sqlite3_errmsg).map(String.init(cString:)) ?? fallback + return NSError(domain: "com.voidhash.measurement.store", code: Int(sqlite3_errcode(database)), userInfo: [NSLocalizedDescriptionKey: message]) + } +} + +private enum SQLiteBinding { + case text(String) + case int(Int64) + case blob(Data) + case null +} + +private func text(_ statement: OpaquePointer, _ index: Int32) -> String { + guard let value = sqlite3_column_text(statement, index) else { return "" } + return String(cString: value) +} + +private func optionalText(_ statement: OpaquePointer, _ index: Int32) -> String? { + sqlite3_column_type(statement, index) == SQLITE_NULL ? nil : text(statement, index) +} + +private func blob(_ statement: OpaquePointer, _ index: Int32) -> Data { + guard let bytes = sqlite3_column_blob(statement, index) else { return Data() } + return Data(bytes: bytes, count: Int(sqlite3_column_bytes(statement, index))) +} + +private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +private final class MeasurementVaultCrypto { + var currentVersion = 1 + + func encrypt(_ value: Data, version: Int) throws -> Data { + let sealed = try AES.GCM.seal(value, using: try key(version: version)) + guard let combined = sealed.combined else { + throw NSError(domain: "com.voidhash.measurement.crypto", code: 1) + } + return combined + } + + func decrypt(_ value: Data, version: Int) throws -> Data { + try AES.GCM.open(AES.GCM.SealedBox(combined: value), using: try key(version: version)) + } + + private func key(version: Int) throws -> SymmetricKey { + let account = "voidhash-measurement-vault-v\(version)" + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: "com.voidhash.measurement", + kSecAttrAccount as String: account, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + ] + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecSuccess, let data = result as? Data { + return SymmetricKey(data: data) + } + guard status == errSecItemNotFound else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) + } + let data = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) } + let insert: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: "com.voidhash.measurement", + kSecAttrAccount as String: account, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecValueData as String: data, + ] + let insertStatus = SecItemAdd(insert as CFDictionary, nil) + guard insertStatus == errSecSuccess || insertStatus == errSecDuplicateItem else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(insertStatus)) + } + return SymmetricKey(data: data) + } +} diff --git a/libraries/react-native/ios/measurement/PushCollector.swift b/libraries/react-native/ios/measurement/PushCollector.swift new file mode 100644 index 000000000..940f99c7d --- /dev/null +++ b/libraries/react-native/ios/measurement/PushCollector.swift @@ -0,0 +1,62 @@ +import Foundation + +/** Build environment associated with an APNs device token. */ +@objc public enum VoidhashPushEnvironment: Int { + case development + case production +} + +struct VoidhashObservedPushToken { + let token: String + let environment: VoidhashPushEnvironment +} + +enum VoidhashPushCollectorEvent { + case tokenChanged + case registrationError(String) +} + +/** Native APNs callback sink that is safe to invoke before the JavaScript runtime starts. */ +@objcMembers public final class VoidhashPushCollector: NSObject, @unchecked Sendable { + public static let shared = VoidhashPushCollector() + + private let lock = NSLock() + private var token: VoidhashObservedPushToken? + private var listeners: [UUID: (VoidhashPushCollectorEvent) -> Void] = [:] + + /** Stores an APNs token using a lowercase, zero-padded hexadecimal representation. */ + public func didRegister( + deviceToken: Data, + environment: VoidhashPushEnvironment + ) { + let value = deviceToken.map { String(format: "%02x", $0) }.joined() + guard !value.isEmpty else { + didFailToRegister(code: "APNS_EMPTY_DEVICE_TOKEN") + return + } + let callbacks = lock.withLock { () -> [(VoidhashPushCollectorEvent) -> Void] in + token = VoidhashObservedPushToken(token: value, environment: environment) + return Array(listeners.values) + } + callbacks.forEach { $0(.tokenChanged) } + } + + /** Records a typed APNs registration failure without retaining its message or credentials. */ + public func didFailToRegister(code: String) { + let safeCode = code.isEmpty ? "APNS_REGISTRATION_FAILED" : code + let callbacks = lock.withLock { Array(listeners.values) } + callbacks.forEach { $0(.registrationError(safeCode)) } + } + + func currentToken() -> VoidhashObservedPushToken? { lock.withLock { token } } + + func subscribe(_ listener: @escaping (VoidhashPushCollectorEvent) -> Void) -> UUID { + lock.withLock { + let id = UUID() + listeners[id] = listener + return id + } + } + + func unsubscribe(_ id: UUID) { lock.withLock { _ = listeners.removeValue(forKey: id) } } +} diff --git a/libraries/react-native/nitro.json b/libraries/react-native/nitro.json index c7096ff49..1bc3ace77 100644 --- a/libraries/react-native/nitro.json +++ b/libraries/react-native/nitro.json @@ -26,6 +26,14 @@ "PaywallPresenter": { "swift": "HybridPaywallPresenter", "kotlin": "HybridPaywallPresenter" + }, + "Measurement": { + "swift": "HybridMeasurement", + "kotlin": "HybridMeasurement" + }, + "Notifications": { + "swift": "HybridNotifications", + "kotlin": "HybridNotifications" } }, "ignorePaths": ["**/node_modules"] diff --git a/libraries/react-native/nitrogen/generated/android/NitroVoidhash+autolinking.cmake b/libraries/react-native/nitrogen/generated/android/NitroVoidhash+autolinking.cmake index f873d5ed9..a79a8562e 100644 --- a/libraries/react-native/nitrogen/generated/android/NitroVoidhash+autolinking.cmake +++ b/libraries/react-native/nitrogen/generated/android/NitroVoidhash+autolinking.cmake @@ -47,6 +47,8 @@ target_sources( ../nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionSpec.cpp ../nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionPeriodSpec.cpp ../nitrogen/generated/shared/c++/HybridStorekitTransactionSpec.cpp + ../nitrogen/generated/shared/c++/HybridMeasurementSpec.cpp + ../nitrogen/generated/shared/c++/HybridNotificationsSpec.cpp # Android-specific Nitrogen C++ sources ../nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.cpp ../nitrogen/generated/android/c++/JHybridPaywallWebViewSpec.cpp @@ -62,6 +64,8 @@ target_sources( ../nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.cpp ../nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.cpp ../nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.cpp + ../nitrogen/generated/android/c++/JHybridMeasurementSpec.cpp + ../nitrogen/generated/android/c++/JHybridNotificationsSpec.cpp ) # Define a flag to check if we are building properly diff --git a/libraries/react-native/nitrogen/generated/android/NitroVoidhashOnLoad.cpp b/libraries/react-native/nitrogen/generated/android/NitroVoidhashOnLoad.cpp index c9649d1cc..2bfc167bf 100644 --- a/libraries/react-native/nitrogen/generated/android/NitroVoidhashOnLoad.cpp +++ b/libraries/react-native/nitrogen/generated/android/NitroVoidhashOnLoad.cpp @@ -42,6 +42,10 @@ #include "JHybridGoogleBillingPricingPhaseSpec.hpp" #include "JHybridGoogleBillingPricingPhasesSpec.hpp" #include "JHybridGoogleBillingSubscriptionOfferDetailsSpec.hpp" +#include "JHybridMeasurementSpec.hpp" +#include "JFunc_void_MeasurementBridgeEvent.hpp" +#include "JHybridNotificationsSpec.hpp" +#include "JFunc_void_NativeNotificationEvent.hpp" #include #include @@ -81,6 +85,10 @@ int initialize(JavaVM* vm) { margelo::nitro::voidhash::JHybridGoogleBillingPricingPhaseSpec::registerNatives(); margelo::nitro::voidhash::JHybridGoogleBillingPricingPhasesSpec::registerNatives(); margelo::nitro::voidhash::JHybridGoogleBillingSubscriptionOfferDetailsSpec::registerNatives(); + margelo::nitro::voidhash::JHybridMeasurementSpec::registerNatives(); + margelo::nitro::voidhash::JFunc_void_MeasurementBridgeEvent_cxx::registerNatives(); + margelo::nitro::voidhash::JHybridNotificationsSpec::registerNatives(); + margelo::nitro::voidhash::JFunc_void_NativeNotificationEvent_cxx::registerNatives(); // Register Nitro Hybrid Objects HybridObjectRegistry::registerHybridObjectConstructor( @@ -119,6 +127,24 @@ int initialize(JavaVM* vm) { return JNISharedPtr::make_shared_from_jni(globalRef); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Measurement", + []() -> std::shared_ptr { + static DefaultConstructableObject object("com/margelo/nitro/voidhash/HybridMeasurement"); + auto instance = object.create(); + auto globalRef = jni::make_global(instance); + return JNISharedPtr::make_shared_from_jni(globalRef); + } + ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Notifications", + []() -> std::shared_ptr { + static DefaultConstructableObject object("com/margelo/nitro/voidhash/HybridNotifications"); + auto instance = object.create(); + auto globalRef = jni::make_global(instance); + return JNISharedPtr::make_shared_from_jni(globalRef); + } + ); }); } diff --git a/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_MeasurementBridgeEvent.hpp b/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_MeasurementBridgeEvent.hpp new file mode 100644 index 000000000..58184c5e4 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_MeasurementBridgeEvent.hpp @@ -0,0 +1,85 @@ +/// +/// JFunc_void_MeasurementBridgeEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include "MeasurementBridgeEvent.hpp" +#include "JMeasurementBridgeEvent.hpp" +#include +#include +#include +#include +#include +#include "MeasurementBridgeError.hpp" +#include "JMeasurementBridgeError.hpp" +#include "MeasurementBridgeSource.hpp" +#include "JMeasurementBridgeSource.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(event: MeasurementBridgeEvent) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_MeasurementBridgeEvent: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/Func_void_MeasurementBridgeEvent;"; + + public: + /** + * Invokes the function this `JFunc_void_MeasurementBridgeEvent` instance holds through JNI. + */ + void invoke(const MeasurementBridgeEvent& event) const { + static const auto method = javaClassStatic()->getMethod /* event */)>("invoke"); + method(self(), JMeasurementBridgeEvent::fromCpp(event)); + } + }; + + /** + * An implementation of Func_void_MeasurementBridgeEvent that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_MeasurementBridgeEvent_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_MeasurementBridgeEvent_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_MeasurementBridgeEvent_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref event) { + _func(event->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/Func_void_MeasurementBridgeEvent_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_MeasurementBridgeEvent_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_MeasurementBridgeEvent_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_NativeNotificationEvent.hpp b/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_NativeNotificationEvent.hpp new file mode 100644 index 000000000..1c160de06 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JFunc_void_NativeNotificationEvent.hpp @@ -0,0 +1,80 @@ +/// +/// JFunc_void_NativeNotificationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include "NativeNotificationEvent.hpp" +#include "JNativeNotificationEvent.hpp" +#include +#include "NativeNotificationEventKind.hpp" +#include "JNativeNotificationEventKind.hpp" +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(event: NativeNotificationEvent) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_NativeNotificationEvent: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/Func_void_NativeNotificationEvent;"; + + public: + /** + * Invokes the function this `JFunc_void_NativeNotificationEvent` instance holds through JNI. + */ + void invoke(const NativeNotificationEvent& event) const { + static const auto method = javaClassStatic()->getMethod /* event */)>("invoke"); + method(self(), JNativeNotificationEvent::fromCpp(event)); + } + }; + + /** + * An implementation of Func_void_NativeNotificationEvent that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_NativeNotificationEvent_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_NativeNotificationEvent_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_NativeNotificationEvent_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref event) { + _func(event->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/Func_void_NativeNotificationEvent_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_NativeNotificationEvent_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_NativeNotificationEvent_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JGoogleBillingProductType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JGoogleBillingProductType.hpp index 97e498aea..1e5007c49 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JGoogleBillingProductType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JGoogleBillingProductType.hpp @@ -43,7 +43,7 @@ namespace margelo::nitro::voidhash { static const auto clazz = javaClassStatic(); static const auto fieldINAPP = clazz->getStaticField("INAPP"); static const auto fieldSUBS = clazz->getStaticField("SUBS"); - + switch (value) { case GoogleBillingProductType::INAPP: return clazz->getStaticFieldValue(fieldINAPP); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.cpp index a8f356262..4c2d3f18d 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.cpp @@ -52,6 +52,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.hpp index b4426ea18..8c00e00e1 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingAcknowledgeResultSpec.hpp @@ -54,7 +54,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.cpp index 1b5d43866..a70ba5314 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.cpp @@ -57,6 +57,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.hpp index 596b288b1..009b682e4 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingConsumeResultSpec.hpp @@ -55,7 +55,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.cpp index b14d51a0c..3f78dab98 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.cpp @@ -46,6 +46,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp index 4ad24c48e..46e82f472 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp @@ -53,7 +53,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.cpp index f210ae58f..5cd609cf7 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.cpp @@ -61,6 +61,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.hpp index 569bf1888..9569d56db 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhaseSpec.hpp @@ -56,7 +56,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.cpp index 3ab48a5af..7057b03cc 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.cpp @@ -50,6 +50,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.hpp index b9d647561..8951f1883 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPricingPhasesSpec.hpp @@ -51,7 +51,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.cpp index af1548e6d..e6b5e4bd2 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.cpp @@ -101,6 +101,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.hpp index 4aacef474..70ca64ca3 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingProductDetailSpec.hpp @@ -60,7 +60,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.cpp index 4560cd6e2..c1c0f2249 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.cpp @@ -112,6 +112,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.hpp index ca385d02a..926e9c25f 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingPurchaseSpec.hpp @@ -64,7 +64,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.cpp index 7799a3711..7f7a670ab 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.cpp @@ -56,7 +56,7 @@ namespace margelo::nitro::voidhash { } // Properties - + // Methods std::shared_ptr> JHybridGoogleBillingSpec::initConnection(const std::optional& /* purchase */)>>& onPurchase) { diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.hpp index eeaec6711..d49101788 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSpec.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.cpp index 2251d40b3..2046c9fc9 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.cpp @@ -72,6 +72,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.hpp index fbc7462de..3f2662afc 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridGoogleBillingSubscriptionOfferDetailsSpec.hpp @@ -55,7 +55,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.cpp new file mode 100644 index 000000000..05f0094c5 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.cpp @@ -0,0 +1,489 @@ +/// +/// JHybridMeasurementSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "JHybridMeasurementSpec.hpp" + +// Forward declaration of `MeasurementStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementStateBridge; } +// Forward declaration of `MeasurementCommandResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommandResult; } +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } +// Forward declaration of `MeasurementBridgeSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementBridgeSource; } +// Forward declaration of `MeasurementFlushBridgeResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementFlushBridgeResult; } +// Forward declaration of `MeasurementInboxEntry` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInboxEntry; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `MeasurementConfigurationStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConfigurationStateBridge; } +// Forward declaration of `MeasurementInitializeConfiguration` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInitializeConfiguration; } +// Forward declaration of `MeasurementCommand` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommand; } +// Forward declaration of `MeasurementCommandKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementCommandKind; } +// Forward declaration of `MeasurementRecordSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordSource; } +// Forward declaration of `MeasurementRecordPriority` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordPriority; } +// Forward declaration of `MeasurementIdentitySnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementIdentitySnapshot; } +// Forward declaration of `MeasurementConsentSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConsentSnapshot; } +// Forward declaration of `MeasurementSessionSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementSessionSnapshot; } +// Forward declaration of `MeasurementBridgeEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeEvent; } +// Forward declaration of `MeasurementProtectedEvidenceInput` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementProtectedEvidenceInput; } +// Forward declaration of `MeasurementProtectedPurpose` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedPurpose; } +// Forward declaration of `MeasurementProtectedRetention` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedRetention; } + +#include +#include "MeasurementStateBridge.hpp" +#include +#include "JMeasurementStateBridge.hpp" +#include +#include +#include "MeasurementCommandResult.hpp" +#include "JMeasurementCommandResult.hpp" +#include "MeasurementBridgeError.hpp" +#include "JMeasurementBridgeError.hpp" +#include "MeasurementBridgeSource.hpp" +#include "JMeasurementBridgeSource.hpp" +#include "MeasurementFlushBridgeResult.hpp" +#include "JMeasurementFlushBridgeResult.hpp" +#include +#include "MeasurementInboxEntry.hpp" +#include "JMeasurementInboxEntry.hpp" +#include +#include +#include +#include "MeasurementConfigurationStateBridge.hpp" +#include "JMeasurementConfigurationStateBridge.hpp" +#include "MeasurementInitializeConfiguration.hpp" +#include "JMeasurementInitializeConfiguration.hpp" +#include "MeasurementCommand.hpp" +#include "JMeasurementCommand.hpp" +#include "MeasurementCommandKind.hpp" +#include "JMeasurementCommandKind.hpp" +#include "MeasurementRecordSource.hpp" +#include "JMeasurementRecordSource.hpp" +#include "MeasurementRecordPriority.hpp" +#include "JMeasurementRecordPriority.hpp" +#include "MeasurementIdentitySnapshot.hpp" +#include "JMeasurementIdentitySnapshot.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "JMeasurementConsentSnapshot.hpp" +#include "MeasurementSessionSnapshot.hpp" +#include "JMeasurementSessionSnapshot.hpp" +#include +#include "MeasurementBridgeEvent.hpp" +#include "JFunc_void_MeasurementBridgeEvent.hpp" +#include "JMeasurementBridgeEvent.hpp" +#include "MeasurementProtectedEvidenceInput.hpp" +#include "JMeasurementProtectedEvidenceInput.hpp" +#include "MeasurementProtectedPurpose.hpp" +#include "JMeasurementProtectedPurpose.hpp" +#include "MeasurementProtectedRetention.hpp" +#include "JMeasurementProtectedRetention.hpp" + +namespace margelo::nitro::voidhash { + + jni::local_ref JHybridMeasurementSpec::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + void JHybridMeasurementSpec::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridMeasurementSpec::initHybrid), + }); + } + + size_t JHybridMeasurementSpec::getExternalMemorySize() noexcept { + static const auto method = javaClassStatic()->getMethod("getMemorySize"); + return method(_javaPart); + } + + // Properties + + + // Methods + std::shared_ptr> JHybridMeasurementSpec::initialize(const std::string& publishableKey, const MeasurementInitializeConfiguration& configuration) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* publishableKey */, jni::alias_ref /* configuration */)>("initialize"); + auto __result = method(_javaPart, jni::make_jstring(publishableKey), JMeasurementInitializeConfiguration::fromCpp(configuration)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::enqueue(const MeasurementCommand& command) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* command */)>("enqueue"); + auto __result = method(_javaPart, JMeasurementCommand::fromCpp(command)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::flush() { + static const auto method = javaClassStatic()->getMethod()>("flush"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::getInstallationId() { + static const auto method = javaClassStatic()->getMethod()>("getInstallationId"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::getState() { + static const auto method = javaClassStatic()->getMethod()>("getState"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + void JHybridMeasurementSpec::subscribe(const std::string& subscriptionId, const std::function& listener) { + static const auto method = javaClassStatic()->getMethod /* subscriptionId */, jni::alias_ref /* listener */)>("subscribe_cxx"); + method(_javaPart, jni::make_jstring(subscriptionId), JFunc_void_MeasurementBridgeEvent_cxx::fromCpp(listener)); + } + void JHybridMeasurementSpec::unsubscribe(const std::string& subscriptionId) { + static const auto method = javaClassStatic()->getMethod /* subscriptionId */)>("unsubscribe"); + method(_javaPart, jni::make_jstring(subscriptionId)); + } + std::shared_ptr>> JHybridMeasurementSpec::peekInbox(double limit) { + static const auto method = javaClassStatic()->getMethod(double /* limit */)>("peekInbox"); + auto __result = method(_javaPart, limit); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast>(__boxedResult); + __promise->resolve([&]() { + size_t __size = __result->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __result->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::acknowledgeInbox(const std::string& entryId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* entryId */)>("acknowledgeInbox"); + auto __result = method(_javaPart, jni::make_jstring(entryId)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr>> JHybridMeasurementSpec::readProtectedEvidence(const std::string& blobId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* blobId */)>("readProtectedEvidence"); + auto __result = method(_javaPart, jni::make_jstring(blobId)); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->cthis()->getArrayBuffer()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::putProtectedEvidence(const MeasurementProtectedEvidenceInput& input) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* input */)>("putProtectedEvidence"); + auto __result = method(_javaPart, JMeasurementProtectedEvidenceInput::fromCpp(input)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::deleteProtectedEvidence(const std::string& blobId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* blobId */)>("deleteProtectedEvidence"); + auto __result = method(_javaPart, jni::make_jstring(blobId)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::deleteProtectedData(const std::string& requestId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* requestId */)>("deleteProtectedData"); + auto __result = method(_javaPart, jni::make_jstring(requestId)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::getMeasurementConfigurationState() { + static const auto method = javaClassStatic()->getMethod()>("getMeasurementConfigurationState"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::persistMeasurementConfigurationState(double version, const std::shared_ptr& payload) { + static const auto method = javaClassStatic()->getMethod(double /* version */, jni::alias_ref /* payload */)>("persistMeasurementConfigurationState"); + auto __result = method(_javaPart, version, JArrayBuffer::wrap(payload)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::applyMeasurementConfiguration(double version, const std::shared_ptr& payload) { + static const auto method = javaClassStatic()->getMethod(double /* version */, jni::alias_ref /* payload */)>("applyMeasurementConfiguration"); + auto __result = method(_javaPart, version, JArrayBuffer::wrap(payload)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) { + __promise->resolve(); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::applyMeasurementStorageLimits(double maxOutboxRecords, double maxOutboxBytes, double maxProtectedBytes) { + static const auto method = javaClassStatic()->getMethod(double /* maxOutboxRecords */, double /* maxOutboxBytes */, double /* maxProtectedBytes */)>("applyMeasurementStorageLimits"); + auto __result = method(_javaPart, maxOutboxRecords, maxOutboxBytes, maxProtectedBytes); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) { + __promise->resolve(); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::getPushRegistrationState() { + static const auto method = javaClassStatic()->getMethod()>("getPushRegistrationState"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::persistPushRegistrationState(const std::shared_ptr& payload) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* payload */)>("persistPushRegistrationState"); + auto __result = method(_javaPart, JArrayBuffer::wrap(payload)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::clearPushRegistrationState() { + static const auto method = javaClassStatic()->getMethod()>("clearPushRegistrationState"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::getTestDeviceState() { + static const auto method = javaClassStatic()->getMethod()>("getTestDeviceState"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::persistTestDeviceState(bool enabled) { + static const auto method = javaClassStatic()->getMethod(jboolean /* enabled */)>("persistTestDeviceState"); + auto __result = method(_javaPart, enabled); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::hasDedupe(const std::string& namespace, const std::string& key) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* namespace */, jni::alias_ref /* key */)>("hasDedupe"); + auto __result = method(_javaPart, jni::make_jstring(namespace), jni::make_jstring(key)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridMeasurementSpec::checkAndSetDedupe(const std::string& namespace, const std::string& key, double expiresAtMs) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* namespace */, jni::alias_ref /* key */, double /* expiresAtMs */)>("checkAndSetDedupe"); + auto __result = method(_javaPart, jni::make_jstring(namespace), jni::make_jstring(key), expiresAtMs); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.hpp new file mode 100644 index 000000000..7cfd491f8 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridMeasurementSpec.hpp @@ -0,0 +1,85 @@ +/// +/// HybridMeasurementSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridMeasurementSpec.hpp" + + + + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + class JHybridMeasurementSpec: public jni::HybridClass, + public virtual HybridMeasurementSpec { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/HybridMeasurementSpec;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + protected: + // C++ constructor (called from Java via `initHybrid()`) + explicit JHybridMeasurementSpec(jni::alias_ref jThis) : + HybridObject(HybridMeasurementSpec::TAG), + _javaPart(jni::make_global(jThis)) {} + + public: + ~JHybridMeasurementSpec() override { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + size_t getExternalMemorySize() noexcept override; + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr> initialize(const std::string& publishableKey, const MeasurementInitializeConfiguration& configuration) override; + std::shared_ptr> enqueue(const MeasurementCommand& command) override; + std::shared_ptr> flush() override; + std::shared_ptr> getInstallationId() override; + std::shared_ptr> getState() override; + void subscribe(const std::string& subscriptionId, const std::function& listener) override; + void unsubscribe(const std::string& subscriptionId) override; + std::shared_ptr>> peekInbox(double limit) override; + std::shared_ptr> acknowledgeInbox(const std::string& entryId) override; + std::shared_ptr>> readProtectedEvidence(const std::string& blobId) override; + std::shared_ptr> putProtectedEvidence(const MeasurementProtectedEvidenceInput& input) override; + std::shared_ptr> deleteProtectedEvidence(const std::string& blobId) override; + std::shared_ptr> deleteProtectedData(const std::string& requestId) override; + std::shared_ptr> getMeasurementConfigurationState() override; + std::shared_ptr> persistMeasurementConfigurationState(double version, const std::shared_ptr& payload) override; + std::shared_ptr> applyMeasurementConfiguration(double version, const std::shared_ptr& payload) override; + std::shared_ptr> applyMeasurementStorageLimits(double maxOutboxRecords, double maxOutboxBytes, double maxProtectedBytes) override; + std::shared_ptr> getPushRegistrationState() override; + std::shared_ptr> persistPushRegistrationState(const std::shared_ptr& payload) override; + std::shared_ptr> clearPushRegistrationState() override; + std::shared_ptr> getTestDeviceState() override; + std::shared_ptr> persistTestDeviceState(bool enabled) override; + std::shared_ptr> hasDedupe(const std::string& namespace, const std::string& key) override; + std::shared_ptr> checkAndSetDedupe(const std::string& namespace, const std::string& key, double expiresAtMs) override; + + private: + friend HybridBase; + using HybridBase::HybridBase; + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.cpp new file mode 100644 index 000000000..27b2f937e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.cpp @@ -0,0 +1,131 @@ +/// +/// JHybridNotificationsSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "JHybridNotificationsSpec.hpp" + +// Forward declaration of `NativePushToken` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativePushToken; } +// Forward declaration of `NativePushProvider` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushProvider; } +// Forward declaration of `NativePushEnvironment` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushEnvironment; } +// Forward declaration of `NativeNotificationEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativeNotificationEvent; } +// Forward declaration of `NativeNotificationEventKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativeNotificationEventKind; } + +#include +#include +#include +#include "NativePushToken.hpp" +#include "JNativePushToken.hpp" +#include "NativePushProvider.hpp" +#include "JNativePushProvider.hpp" +#include "NativePushEnvironment.hpp" +#include "JNativePushEnvironment.hpp" +#include +#include "NativeNotificationEvent.hpp" +#include "JFunc_void_NativeNotificationEvent.hpp" +#include "JNativeNotificationEvent.hpp" +#include "NativeNotificationEventKind.hpp" +#include "JNativeNotificationEventKind.hpp" +#include + +namespace margelo::nitro::voidhash { + + jni::local_ref JHybridNotificationsSpec::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + void JHybridNotificationsSpec::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridNotificationsSpec::initHybrid), + }); + } + + size_t JHybridNotificationsSpec::getExternalMemorySize() noexcept { + static const auto method = javaClassStatic()->getMethod("getMemorySize"); + return method(_javaPart); + } + + // Properties + + + // Methods + std::shared_ptr> JHybridNotificationsSpec::getPermissionStatus() { + static const auto method = javaClassStatic()->getMethod()>("getPermissionStatus"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNotificationsSpec::requestPermission(bool provisional) { + static const auto method = javaClassStatic()->getMethod(jboolean /* provisional */)>("requestPermission"); + auto __result = method(_javaPart, provisional); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNotificationsSpec::getToken() { + static const auto method = javaClassStatic()->getMethod()>("getToken"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNotificationsSpec::setBadgeCount(double count) { + static const auto method = javaClassStatic()->getMethod(double /* count */)>("setBadgeCount"); + auto __result = method(_javaPart, count); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) { + __promise->resolve(); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + void JHybridNotificationsSpec::subscribe(const std::string& subscriptionId, const std::function& listener) { + static const auto method = javaClassStatic()->getMethod /* subscriptionId */, jni::alias_ref /* listener */)>("subscribe_cxx"); + method(_javaPart, jni::make_jstring(subscriptionId), JFunc_void_NativeNotificationEvent_cxx::fromCpp(listener)); + } + void JHybridNotificationsSpec::unsubscribe(const std::string& subscriptionId) { + static const auto method = javaClassStatic()->getMethod /* subscriptionId */)>("unsubscribe"); + method(_javaPart, jni::make_jstring(subscriptionId)); + } + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.hpp new file mode 100644 index 000000000..9b9f362ad --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridNotificationsSpec.hpp @@ -0,0 +1,67 @@ +/// +/// HybridNotificationsSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridNotificationsSpec.hpp" + + + + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + class JHybridNotificationsSpec: public jni::HybridClass, + public virtual HybridNotificationsSpec { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/HybridNotificationsSpec;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + protected: + // C++ constructor (called from Java via `initHybrid()`) + explicit JHybridNotificationsSpec(jni::alias_ref jThis) : + HybridObject(HybridNotificationsSpec::TAG), + _javaPart(jni::make_global(jThis)) {} + + public: + ~JHybridNotificationsSpec() override { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + size_t getExternalMemorySize() noexcept override; + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr> getPermissionStatus() override; + std::shared_ptr> requestPermission(bool provisional) override; + std::shared_ptr> getToken() override; + std::shared_ptr> setBadgeCount(double count) override; + void subscribe(const std::string& subscriptionId, const std::function& listener) override; + void unsubscribe(const std::string& subscriptionId) override; + + private: + friend HybridBase; + using HybridBase::HybridBase; + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.cpp index aef4e0477..5a7ff8402 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.cpp @@ -35,7 +35,7 @@ namespace margelo::nitro::voidhash { } // Properties - + // Methods std::shared_ptr> JHybridPaywallPresenterSpec::preload(const std::string& locationSlug, const std::string& htmlUrl) { diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.hpp index 67c0a54d8..ccb9fd507 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridPaywallPresenterSpec.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.cpp index 841239e4a..27e174c4f 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.cpp @@ -52,6 +52,6 @@ namespace margelo::nitro::voidhash { } // Methods - + } // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.hpp index b132d4ad0..e7bfa5d4c 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridPurchasedItemSpec.hpp @@ -54,7 +54,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: friend HybridBase; diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.cpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.cpp index c924c2370..ef1eee8d4 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.cpp @@ -36,7 +36,7 @@ namespace margelo::nitro::voidhash { } // Properties - + // Methods std::shared_ptr>> JHybridVoidhashSpec::purchase(const std::string& sku) { diff --git a/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.hpp b/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.hpp index 4d008fc08..3a9e47ee9 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JHybridVoidhashSpec.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeError.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeError.hpp new file mode 100644 index 000000000..6f238644c --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeError.hpp @@ -0,0 +1,72 @@ +/// +/// JMeasurementBridgeError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementBridgeError.hpp" + +#include "JMeasurementBridgeSource.hpp" +#include "MeasurementBridgeSource.hpp" +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementBridgeError" and the the Kotlin data class "MeasurementBridgeError". + */ + struct JMeasurementBridgeError final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementBridgeError;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementBridgeError by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementBridgeError toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldCode = clazz->getField("code"); + jni::local_ref code = this->getFieldValue(fieldCode); + static const auto fieldMessage = clazz->getField("message"); + jni::local_ref message = this->getFieldValue(fieldMessage); + static const auto fieldSource = clazz->getField("source"); + jni::local_ref source = this->getFieldValue(fieldSource); + static const auto fieldCapability = clazz->getField("capability"); + jni::local_ref capability = this->getFieldValue(fieldCapability); + static const auto fieldReason = clazz->getField("reason"); + jni::local_ref reason = this->getFieldValue(fieldReason); + return MeasurementBridgeError( + code->toStdString(), + message->toStdString(), + source->toCpp(), + capability != nullptr ? std::make_optional(capability->toStdString()) : std::nullopt, + reason != nullptr ? std::make_optional(reason->toStdString()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementBridgeError& value) { + return newInstance( + jni::make_jstring(value.code), + jni::make_jstring(value.message), + JMeasurementBridgeSource::fromCpp(value.source), + value.capability.has_value() ? jni::make_jstring(value.capability.value()) : nullptr, + value.reason.has_value() ? jni::make_jstring(value.reason.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeEvent.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeEvent.hpp new file mode 100644 index 000000000..7333ccd9a --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeEvent.hpp @@ -0,0 +1,81 @@ +/// +/// JMeasurementBridgeEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementBridgeEvent.hpp" + +#include "JMeasurementBridgeError.hpp" +#include "JMeasurementBridgeSource.hpp" +#include "MeasurementBridgeError.hpp" +#include "MeasurementBridgeSource.hpp" +#include +#include +#include +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementBridgeEvent" and the the Kotlin data class "MeasurementBridgeEvent". + */ + struct JMeasurementBridgeEvent final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementBridgeEvent;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementBridgeEvent by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementBridgeEvent toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldSubscriptionId = clazz->getField("subscriptionId"); + jni::local_ref subscriptionId = this->getFieldValue(fieldSubscriptionId); + static const auto fieldEvent = clazz->getField("event"); + jni::local_ref event = this->getFieldValue(fieldEvent); + static const auto fieldRecordId = clazz->getField("recordId"); + jni::local_ref recordId = this->getFieldValue(fieldRecordId); + static const auto fieldRequestId = clazz->getField("requestId"); + jni::local_ref requestId = this->getFieldValue(fieldRequestId); + static const auto fieldPayload = clazz->getField("payload"); + jni::local_ref payload = this->getFieldValue(fieldPayload); + static const auto fieldError = clazz->getField("error"); + jni::local_ref error = this->getFieldValue(fieldError); + return MeasurementBridgeEvent( + subscriptionId->toStdString(), + event->toStdString(), + recordId != nullptr ? std::make_optional(recordId->toStdString()) : std::nullopt, + requestId != nullptr ? std::make_optional(requestId->toStdString()) : std::nullopt, + payload != nullptr ? std::make_optional(payload->cthis()->getArrayBuffer()) : std::nullopt, + error != nullptr ? std::make_optional(error->toCpp()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementBridgeEvent& value) { + return newInstance( + jni::make_jstring(value.subscriptionId), + jni::make_jstring(value.event), + value.recordId.has_value() ? jni::make_jstring(value.recordId.value()) : nullptr, + value.requestId.has_value() ? jni::make_jstring(value.requestId.value()) : nullptr, + value.payload.has_value() ? JArrayBuffer::wrap(value.payload.value()) : nullptr, + value.error.has_value() ? JMeasurementBridgeError::fromCpp(value.error.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeSource.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeSource.hpp new file mode 100644 index 000000000..25faf71cf --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementBridgeSource.hpp @@ -0,0 +1,62 @@ +/// +/// JMeasurementBridgeSource.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementBridgeSource.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementBridgeSource" and the the Kotlin enum "MeasurementBridgeSource". + */ + struct JMeasurementBridgeSource final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementBridgeSource;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementBridgeSource. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementBridgeSource toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementBridgeSource value) { + static const auto clazz = javaClassStatic(); + static const auto fieldIOS = clazz->getStaticField("IOS"); + static const auto fieldANDROID = clazz->getStaticField("ANDROID"); + static const auto fieldCORE = clazz->getStaticField("CORE"); + + switch (value) { + case MeasurementBridgeSource::IOS: + return clazz->getStaticFieldValue(fieldIOS); + case MeasurementBridgeSource::ANDROID: + return clazz->getStaticFieldValue(fieldANDROID); + case MeasurementBridgeSource::CORE: + return clazz->getStaticFieldValue(fieldCORE); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommand.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommand.hpp new file mode 100644 index 000000000..88e0879c8 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommand.hpp @@ -0,0 +1,109 @@ +/// +/// JMeasurementCommand.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementCommand.hpp" + +#include "JMeasurementCommandKind.hpp" +#include "JMeasurementConsentSnapshot.hpp" +#include "JMeasurementIdentitySnapshot.hpp" +#include "JMeasurementRecordPriority.hpp" +#include "JMeasurementRecordSource.hpp" +#include "JMeasurementSessionSnapshot.hpp" +#include "MeasurementCommandKind.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "MeasurementIdentitySnapshot.hpp" +#include "MeasurementRecordPriority.hpp" +#include "MeasurementRecordSource.hpp" +#include "MeasurementSessionSnapshot.hpp" +#include +#include +#include +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementCommand" and the the Kotlin data class "MeasurementCommand". + */ + struct JMeasurementCommand final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementCommand;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementCommand by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementCommand toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldKind = clazz->getField("kind"); + jni::local_ref kind = this->getFieldValue(fieldKind); + static const auto fieldCommandId = clazz->getField("commandId"); + jni::local_ref commandId = this->getFieldValue(fieldCommandId); + static const auto fieldRecordType = clazz->getField("recordType"); + jni::local_ref recordType = this->getFieldValue(fieldRecordType); + static const auto fieldOccurredAt = clazz->getField("occurredAt"); + jni::local_ref occurredAt = this->getFieldValue(fieldOccurredAt); + static const auto fieldSource = clazz->getField("source"); + jni::local_ref source = this->getFieldValue(fieldSource); + static const auto fieldPriority = clazz->getField("priority"); + jni::local_ref priority = this->getFieldValue(fieldPriority); + static const auto fieldPublicPayload = clazz->getField("publicPayload"); + jni::local_ref publicPayload = this->getFieldValue(fieldPublicPayload); + static const auto fieldProtectedEvidenceRef = clazz->getField("protectedEvidenceRef"); + jni::local_ref protectedEvidenceRef = this->getFieldValue(fieldProtectedEvidenceRef); + static const auto fieldIdentity = clazz->getField("identity"); + jni::local_ref identity = this->getFieldValue(fieldIdentity); + static const auto fieldConsent = clazz->getField("consent"); + jni::local_ref consent = this->getFieldValue(fieldConsent); + static const auto fieldSession = clazz->getField("session"); + jni::local_ref session = this->getFieldValue(fieldSession); + return MeasurementCommand( + kind->toCpp(), + commandId->toStdString(), + recordType->toStdString(), + occurredAt->toStdString(), + source->toCpp(), + priority->toCpp(), + publicPayload->cthis()->getArrayBuffer(), + protectedEvidenceRef != nullptr ? std::make_optional(protectedEvidenceRef->toStdString()) : std::nullopt, + identity != nullptr ? std::make_optional(identity->toCpp()) : std::nullopt, + consent != nullptr ? std::make_optional(consent->toCpp()) : std::nullopt, + session != nullptr ? std::make_optional(session->toCpp()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementCommand& value) { + return newInstance( + JMeasurementCommandKind::fromCpp(value.kind), + jni::make_jstring(value.commandId), + jni::make_jstring(value.recordType), + jni::make_jstring(value.occurredAt), + JMeasurementRecordSource::fromCpp(value.source), + JMeasurementRecordPriority::fromCpp(value.priority), + JArrayBuffer::wrap(value.publicPayload), + value.protectedEvidenceRef.has_value() ? jni::make_jstring(value.protectedEvidenceRef.value()) : nullptr, + value.identity.has_value() ? JMeasurementIdentitySnapshot::fromCpp(value.identity.value()) : nullptr, + value.consent.has_value() ? JMeasurementConsentSnapshot::fromCpp(value.consent.value()) : nullptr, + value.session.has_value() ? JMeasurementSessionSnapshot::fromCpp(value.session.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandKind.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandKind.hpp new file mode 100644 index 000000000..3ff40cd16 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandKind.hpp @@ -0,0 +1,83 @@ +/// +/// JMeasurementCommandKind.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementCommandKind.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementCommandKind" and the the Kotlin enum "MeasurementCommandKind". + */ + struct JMeasurementCommandKind final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementCommandKind;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementCommandKind. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementCommandKind toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementCommandKind value) { + static const auto clazz = javaClassStatic(); + static const auto fieldENQUEUERECORD = clazz->getStaticField("ENQUEUERECORD"); + static const auto fieldIDENTITYTRANSITION = clazz->getStaticField("IDENTITYTRANSITION"); + static const auto fieldCONSENTTRANSITION = clazz->getStaticField("CONSENTTRANSITION"); + static const auto fieldSESSIONSIGNAL = clazz->getStaticField("SESSIONSIGNAL"); + static const auto fieldCOLDLAUNCHINPUT = clazz->getStaticField("COLDLAUNCHINPUT"); + static const auto fieldTRANSACTIONDEDUP = clazz->getStaticField("TRANSACTIONDEDUP"); + static const auto fieldLINKINPUT = clazz->getStaticField("LINKINPUT"); + static const auto fieldPUSHINPUT = clazz->getStaticField("PUSHINPUT"); + static const auto fieldPURCHASEINPUT = clazz->getStaticField("PURCHASEINPUT"); + static const auto fieldIDENTIFIERINPUT = clazz->getStaticField("IDENTIFIERINPUT"); + + switch (value) { + case MeasurementCommandKind::ENQUEUERECORD: + return clazz->getStaticFieldValue(fieldENQUEUERECORD); + case MeasurementCommandKind::IDENTITYTRANSITION: + return clazz->getStaticFieldValue(fieldIDENTITYTRANSITION); + case MeasurementCommandKind::CONSENTTRANSITION: + return clazz->getStaticFieldValue(fieldCONSENTTRANSITION); + case MeasurementCommandKind::SESSIONSIGNAL: + return clazz->getStaticFieldValue(fieldSESSIONSIGNAL); + case MeasurementCommandKind::COLDLAUNCHINPUT: + return clazz->getStaticFieldValue(fieldCOLDLAUNCHINPUT); + case MeasurementCommandKind::TRANSACTIONDEDUP: + return clazz->getStaticFieldValue(fieldTRANSACTIONDEDUP); + case MeasurementCommandKind::LINKINPUT: + return clazz->getStaticFieldValue(fieldLINKINPUT); + case MeasurementCommandKind::PUSHINPUT: + return clazz->getStaticFieldValue(fieldPUSHINPUT); + case MeasurementCommandKind::PURCHASEINPUT: + return clazz->getStaticFieldValue(fieldPURCHASEINPUT); + case MeasurementCommandKind::IDENTIFIERINPUT: + return clazz->getStaticFieldValue(fieldIDENTIFIERINPUT); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandResult.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandResult.hpp new file mode 100644 index 000000000..4e59e14e0 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementCommandResult.hpp @@ -0,0 +1,70 @@ +/// +/// JMeasurementCommandResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementCommandResult.hpp" + +#include "JMeasurementBridgeError.hpp" +#include "JMeasurementBridgeSource.hpp" +#include "MeasurementBridgeError.hpp" +#include "MeasurementBridgeSource.hpp" +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementCommandResult" and the the Kotlin data class "MeasurementCommandResult". + */ + struct JMeasurementCommandResult final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementCommandResult;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementCommandResult by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementCommandResult toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldAccepted = clazz->getField("accepted"); + jboolean accepted = this->getFieldValue(fieldAccepted); + static const auto fieldRecordId = clazz->getField("recordId"); + jni::local_ref recordId = this->getFieldValue(fieldRecordId); + static const auto fieldInstallationSequence = clazz->getField("installationSequence"); + jni::local_ref installationSequence = this->getFieldValue(fieldInstallationSequence); + static const auto fieldError = clazz->getField("error"); + jni::local_ref error = this->getFieldValue(fieldError); + return MeasurementCommandResult( + static_cast(accepted), + recordId != nullptr ? std::make_optional(recordId->toStdString()) : std::nullopt, + installationSequence != nullptr ? std::make_optional(installationSequence->value()) : std::nullopt, + error != nullptr ? std::make_optional(error->toCpp()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementCommandResult& value) { + return newInstance( + value.accepted, + value.recordId.has_value() ? jni::make_jstring(value.recordId.value()) : nullptr, + value.installationSequence.has_value() ? jni::JDouble::valueOf(value.installationSequence.value()) : nullptr, + value.error.has_value() ? JMeasurementBridgeError::fromCpp(value.error.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConfigurationStateBridge.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConfigurationStateBridge.hpp new file mode 100644 index 000000000..52152feab --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConfigurationStateBridge.hpp @@ -0,0 +1,60 @@ +/// +/// JMeasurementConfigurationStateBridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementConfigurationStateBridge.hpp" + +#include +#include +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementConfigurationStateBridge" and the the Kotlin data class "MeasurementConfigurationStateBridge". + */ + struct JMeasurementConfigurationStateBridge final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementConfigurationStateBridge;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementConfigurationStateBridge by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementConfigurationStateBridge toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldVersion = clazz->getField("version"); + double version = this->getFieldValue(fieldVersion); + static const auto fieldPayload = clazz->getField("payload"); + jni::local_ref payload = this->getFieldValue(fieldPayload); + return MeasurementConfigurationStateBridge( + version, + payload != nullptr ? std::make_optional(payload->cthis()->getArrayBuffer()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementConfigurationStateBridge& value) { + return newInstance( + value.version, + value.payload.has_value() ? JArrayBuffer::wrap(value.payload.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConsentSnapshot.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConsentSnapshot.hpp new file mode 100644 index 000000000..c1d7a406a --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementConsentSnapshot.hpp @@ -0,0 +1,86 @@ +/// +/// JMeasurementConsentSnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementConsentSnapshot.hpp" + +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementConsentSnapshot" and the the Kotlin data class "MeasurementConsentSnapshot". + */ + struct JMeasurementConsentSnapshot final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementConsentSnapshot;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementConsentSnapshot by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementConsentSnapshot toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldRevision = clazz->getField("revision"); + double revision = this->getFieldValue(fieldRevision); + static const auto fieldDecidedAt = clazz->getField("decidedAt"); + jni::local_ref decidedAt = this->getFieldValue(fieldDecidedAt); + static const auto fieldSource = clazz->getField("source"); + jni::local_ref source = this->getFieldValue(fieldSource); + static const auto fieldGdprApplies = clazz->getField("gdprApplies"); + jni::local_ref gdprApplies = this->getFieldValue(fieldGdprApplies); + static const auto fieldDataUsage = clazz->getField("dataUsage"); + jni::local_ref dataUsage = this->getFieldValue(fieldDataUsage); + static const auto fieldAdsPersonalization = clazz->getField("adsPersonalization"); + jni::local_ref adsPersonalization = this->getFieldValue(fieldAdsPersonalization); + static const auto fieldAdStorage = clazz->getField("adStorage"); + jni::local_ref adStorage = this->getFieldValue(fieldAdStorage); + static const auto fieldCollectionOptOut = clazz->getField("collectionOptOut"); + jni::local_ref collectionOptOut = this->getFieldValue(fieldCollectionOptOut); + static const auto fieldPartnerSharingOptOut = clazz->getField("partnerSharingOptOut"); + jni::local_ref partnerSharingOptOut = this->getFieldValue(fieldPartnerSharingOptOut); + return MeasurementConsentSnapshot( + revision, + decidedAt->toStdString(), + source->toStdString(), + gdprApplies != nullptr ? std::make_optional(static_cast(gdprApplies->value())) : std::nullopt, + dataUsage != nullptr ? std::make_optional(static_cast(dataUsage->value())) : std::nullopt, + adsPersonalization != nullptr ? std::make_optional(static_cast(adsPersonalization->value())) : std::nullopt, + adStorage != nullptr ? std::make_optional(static_cast(adStorage->value())) : std::nullopt, + collectionOptOut != nullptr ? std::make_optional(static_cast(collectionOptOut->value())) : std::nullopt, + partnerSharingOptOut != nullptr ? std::make_optional(static_cast(partnerSharingOptOut->value())) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementConsentSnapshot& value) { + return newInstance( + value.revision, + jni::make_jstring(value.decidedAt), + jni::make_jstring(value.source), + value.gdprApplies.has_value() ? jni::JBoolean::valueOf(value.gdprApplies.value()) : nullptr, + value.dataUsage.has_value() ? jni::JBoolean::valueOf(value.dataUsage.value()) : nullptr, + value.adsPersonalization.has_value() ? jni::JBoolean::valueOf(value.adsPersonalization.value()) : nullptr, + value.adStorage.has_value() ? jni::JBoolean::valueOf(value.adStorage.value()) : nullptr, + value.collectionOptOut.has_value() ? jni::JBoolean::valueOf(value.collectionOptOut.value()) : nullptr, + value.partnerSharingOptOut.has_value() ? jni::JBoolean::valueOf(value.partnerSharingOptOut.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementFlushBridgeResult.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementFlushBridgeResult.hpp new file mode 100644 index 000000000..e74f0d8b0 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementFlushBridgeResult.hpp @@ -0,0 +1,65 @@ +/// +/// JMeasurementFlushBridgeResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementFlushBridgeResult.hpp" + + + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementFlushBridgeResult" and the the Kotlin data class "MeasurementFlushBridgeResult". + */ + struct JMeasurementFlushBridgeResult final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementFlushBridgeResult;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementFlushBridgeResult by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementFlushBridgeResult toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldAccepted = clazz->getField("accepted"); + double accepted = this->getFieldValue(fieldAccepted); + static const auto fieldScheduled = clazz->getField("scheduled"); + double scheduled = this->getFieldValue(fieldScheduled); + static const auto fieldQuarantined = clazz->getField("quarantined"); + double quarantined = this->getFieldValue(fieldQuarantined); + static const auto fieldPolicyBlocked = clazz->getField("policyBlocked"); + double policyBlocked = this->getFieldValue(fieldPolicyBlocked); + return MeasurementFlushBridgeResult( + accepted, + scheduled, + quarantined, + policyBlocked + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementFlushBridgeResult& value) { + return newInstance( + value.accepted, + value.scheduled, + value.quarantined, + value.policyBlocked + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementIdentitySnapshot.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementIdentitySnapshot.hpp new file mode 100644 index 000000000..d5bb18cbe --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementIdentitySnapshot.hpp @@ -0,0 +1,66 @@ +/// +/// JMeasurementIdentitySnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementIdentitySnapshot.hpp" + +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementIdentitySnapshot" and the the Kotlin data class "MeasurementIdentitySnapshot". + */ + struct JMeasurementIdentitySnapshot final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementIdentitySnapshot;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementIdentitySnapshot by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementIdentitySnapshot toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldDistinctId = clazz->getField("distinctId"); + jni::local_ref distinctId = this->getFieldValue(fieldDistinctId); + static const auto fieldAnonymousId = clazz->getField("anonymousId"); + jni::local_ref anonymousId = this->getFieldValue(fieldAnonymousId); + static const auto fieldPersonId = clazz->getField("personId"); + jni::local_ref personId = this->getFieldValue(fieldPersonId); + static const auto fieldRevision = clazz->getField("revision"); + double revision = this->getFieldValue(fieldRevision); + return MeasurementIdentitySnapshot( + distinctId->toStdString(), + anonymousId != nullptr ? std::make_optional(anonymousId->toStdString()) : std::nullopt, + personId != nullptr ? std::make_optional(personId->toStdString()) : std::nullopt, + revision + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementIdentitySnapshot& value) { + return newInstance( + jni::make_jstring(value.distinctId), + value.anonymousId.has_value() ? jni::make_jstring(value.anonymousId.value()) : nullptr, + value.personId.has_value() ? jni::make_jstring(value.personId.value()) : nullptr, + value.revision + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInboxEntry.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInboxEntry.hpp new file mode 100644 index 000000000..ac9a49cfb --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInboxEntry.hpp @@ -0,0 +1,73 @@ +/// +/// JMeasurementInboxEntry.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementInboxEntry.hpp" + +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementInboxEntry" and the the Kotlin data class "MeasurementInboxEntry". + */ + struct JMeasurementInboxEntry final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementInboxEntry;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementInboxEntry by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementInboxEntry toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldId = clazz->getField("id"); + jni::local_ref id = this->getFieldValue(fieldId); + static const auto fieldKind = clazz->getField("kind"); + jni::local_ref kind = this->getFieldValue(fieldKind); + static const auto fieldSource = clazz->getField("source"); + jni::local_ref source = this->getFieldValue(fieldSource); + static const auto fieldAppState = clazz->getField("appState"); + jni::local_ref appState = this->getFieldValue(fieldAppState); + static const auto fieldReceivedAt = clazz->getField("receivedAt"); + jni::local_ref receivedAt = this->getFieldValue(fieldReceivedAt); + static const auto fieldProtectedEvidenceRef = clazz->getField("protectedEvidenceRef"); + jni::local_ref protectedEvidenceRef = this->getFieldValue(fieldProtectedEvidenceRef); + return MeasurementInboxEntry( + id->toStdString(), + kind->toStdString(), + source->toStdString(), + appState->toStdString(), + receivedAt->toStdString(), + protectedEvidenceRef->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementInboxEntry& value) { + return newInstance( + jni::make_jstring(value.id), + jni::make_jstring(value.kind), + jni::make_jstring(value.source), + jni::make_jstring(value.appState), + jni::make_jstring(value.receivedAt), + jni::make_jstring(value.protectedEvidenceRef) + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInitializeConfiguration.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInitializeConfiguration.hpp new file mode 100644 index 000000000..621e3042b --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementInitializeConfiguration.hpp @@ -0,0 +1,83 @@ +/// +/// JMeasurementInitializeConfiguration.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementInitializeConfiguration.hpp" + +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementInitializeConfiguration" and the the Kotlin data class "MeasurementInitializeConfiguration". + */ + struct JMeasurementInitializeConfiguration final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementInitializeConfiguration;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementInitializeConfiguration by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementInitializeConfiguration toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldApiUrl = clazz->getField("apiUrl"); + jni::local_ref apiUrl = this->getFieldValue(fieldApiUrl); + static const auto fieldIngestUrl = clazz->getField("ingestUrl"); + jni::local_ref ingestUrl = this->getFieldValue(fieldIngestUrl); + static const auto fieldLinksUrl = clazz->getField("linksUrl"); + jni::local_ref linksUrl = this->getFieldValue(fieldLinksUrl); + static const auto fieldTrustedConfigKeyIds = clazz->getField>("trustedConfigKeyIds"); + jni::local_ref> trustedConfigKeyIds = this->getFieldValue(fieldTrustedConfigKeyIds); + return MeasurementInitializeConfiguration( + apiUrl->toStdString(), + ingestUrl->toStdString(), + linksUrl->toStdString(), + [&]() { + size_t __size = trustedConfigKeyIds->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = trustedConfigKeyIds->getElement(__i); + __vector.push_back(__element->toStdString()); + } + return __vector; + }() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementInitializeConfiguration& value) { + return newInstance( + jni::make_jstring(value.apiUrl), + jni::make_jstring(value.ingestUrl), + jni::make_jstring(value.linksUrl), + [&]() { + size_t __size = value.trustedConfigKeyIds.size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = value.trustedConfigKeyIds[__i]; + __array->setElement(__i, *jni::make_jstring(__element)); + } + return __array; + }() + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedEvidenceInput.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedEvidenceInput.hpp new file mode 100644 index 000000000..f77fdcde6 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedEvidenceInput.hpp @@ -0,0 +1,76 @@ +/// +/// JMeasurementProtectedEvidenceInput.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementProtectedEvidenceInput.hpp" + +#include "JMeasurementProtectedPurpose.hpp" +#include "JMeasurementProtectedRetention.hpp" +#include "MeasurementProtectedPurpose.hpp" +#include "MeasurementProtectedRetention.hpp" +#include +#include +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementProtectedEvidenceInput" and the the Kotlin data class "MeasurementProtectedEvidenceInput". + */ + struct JMeasurementProtectedEvidenceInput final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementProtectedEvidenceInput;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementProtectedEvidenceInput by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementProtectedEvidenceInput toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldBlobId = clazz->getField("blobId"); + jni::local_ref blobId = this->getFieldValue(fieldBlobId); + static const auto fieldPurpose = clazz->getField("purpose"); + jni::local_ref purpose = this->getFieldValue(fieldPurpose); + static const auto fieldConsentRevision = clazz->getField("consentRevision"); + double consentRevision = this->getFieldValue(fieldConsentRevision); + static const auto fieldRetentionClass = clazz->getField("retentionClass"); + jni::local_ref retentionClass = this->getFieldValue(fieldRetentionClass); + static const auto fieldValue = clazz->getField("value"); + jni::local_ref value = this->getFieldValue(fieldValue); + return MeasurementProtectedEvidenceInput( + blobId->toStdString(), + purpose->toCpp(), + consentRevision, + retentionClass->toCpp(), + value->cthis()->getArrayBuffer() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementProtectedEvidenceInput& value) { + return newInstance( + jni::make_jstring(value.blobId), + JMeasurementProtectedPurpose::fromCpp(value.purpose), + value.consentRevision, + JMeasurementProtectedRetention::fromCpp(value.retentionClass), + JArrayBuffer::wrap(value.value) + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedPurpose.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedPurpose.hpp new file mode 100644 index 000000000..d94f74b5f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedPurpose.hpp @@ -0,0 +1,80 @@ +/// +/// JMeasurementProtectedPurpose.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementProtectedPurpose.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementProtectedPurpose" and the the Kotlin enum "MeasurementProtectedPurpose". + */ + struct JMeasurementProtectedPurpose final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementProtectedPurpose;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementProtectedPurpose. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementProtectedPurpose toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementProtectedPurpose value) { + static const auto clazz = javaClassStatic(); + static const auto fieldADVERTISING_IDENTIFIER = clazz->getStaticField("ADVERTISING_IDENTIFIER"); + static const auto fieldDIAGNOSTIC_AUTHORIZATION = clazz->getStaticField("DIAGNOSTIC_AUTHORIZATION"); + static const auto fieldEMAIL = clazz->getStaticField("EMAIL"); + static const auto fieldINSTALL_REFERRER = clazz->getStaticField("INSTALL_REFERRER"); + static const auto fieldLINK_CAPTURE = clazz->getStaticField("LINK_CAPTURE"); + static const auto fieldPARTNER_CONTEXT = clazz->getStaticField("PARTNER_CONTEXT"); + static const auto fieldPHONE = clazz->getStaticField("PHONE"); + static const auto fieldPURCHASE_RECEIPT = clazz->getStaticField("PURCHASE_RECEIPT"); + static const auto fieldPUSH_TOKEN = clazz->getStaticField("PUSH_TOKEN"); + + switch (value) { + case MeasurementProtectedPurpose::ADVERTISING_IDENTIFIER: + return clazz->getStaticFieldValue(fieldADVERTISING_IDENTIFIER); + case MeasurementProtectedPurpose::DIAGNOSTIC_AUTHORIZATION: + return clazz->getStaticFieldValue(fieldDIAGNOSTIC_AUTHORIZATION); + case MeasurementProtectedPurpose::EMAIL: + return clazz->getStaticFieldValue(fieldEMAIL); + case MeasurementProtectedPurpose::INSTALL_REFERRER: + return clazz->getStaticFieldValue(fieldINSTALL_REFERRER); + case MeasurementProtectedPurpose::LINK_CAPTURE: + return clazz->getStaticFieldValue(fieldLINK_CAPTURE); + case MeasurementProtectedPurpose::PARTNER_CONTEXT: + return clazz->getStaticFieldValue(fieldPARTNER_CONTEXT); + case MeasurementProtectedPurpose::PHONE: + return clazz->getStaticFieldValue(fieldPHONE); + case MeasurementProtectedPurpose::PURCHASE_RECEIPT: + return clazz->getStaticFieldValue(fieldPURCHASE_RECEIPT); + case MeasurementProtectedPurpose::PUSH_TOKEN: + return clazz->getStaticFieldValue(fieldPUSH_TOKEN); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedRetention.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedRetention.hpp new file mode 100644 index 000000000..93b7f0ff8 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementProtectedRetention.hpp @@ -0,0 +1,65 @@ +/// +/// JMeasurementProtectedRetention.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementProtectedRetention.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementProtectedRetention" and the the Kotlin enum "MeasurementProtectedRetention". + */ + struct JMeasurementProtectedRetention final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementProtectedRetention;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementProtectedRetention. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementProtectedRetention toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementProtectedRetention value) { + static const auto clazz = javaClassStatic(); + static const auto fieldEPHEMERAL = clazz->getStaticField("EPHEMERAL"); + static const auto fieldINSTALLATION = clazz->getStaticField("INSTALLATION"); + static const auto fieldLEGAL = clazz->getStaticField("LEGAL"); + static const auto fieldTRANSACTION = clazz->getStaticField("TRANSACTION"); + + switch (value) { + case MeasurementProtectedRetention::EPHEMERAL: + return clazz->getStaticFieldValue(fieldEPHEMERAL); + case MeasurementProtectedRetention::INSTALLATION: + return clazz->getStaticFieldValue(fieldINSTALLATION); + case MeasurementProtectedRetention::LEGAL: + return clazz->getStaticFieldValue(fieldLEGAL); + case MeasurementProtectedRetention::TRANSACTION: + return clazz->getStaticFieldValue(fieldTRANSACTION); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordPriority.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordPriority.hpp new file mode 100644 index 000000000..929a8da59 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordPriority.hpp @@ -0,0 +1,65 @@ +/// +/// JMeasurementRecordPriority.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementRecordPriority.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementRecordPriority" and the the Kotlin enum "MeasurementRecordPriority". + */ + struct JMeasurementRecordPriority final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementRecordPriority;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementRecordPriority. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementRecordPriority toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementRecordPriority value) { + static const auto clazz = javaClassStatic(); + static const auto fieldCRITICAL = clazz->getStaticField("CRITICAL"); + static const auto fieldHIGH = clazz->getStaticField("HIGH"); + static const auto fieldNORMAL = clazz->getStaticField("NORMAL"); + static const auto fieldLOW = clazz->getStaticField("LOW"); + + switch (value) { + case MeasurementRecordPriority::CRITICAL: + return clazz->getStaticFieldValue(fieldCRITICAL); + case MeasurementRecordPriority::HIGH: + return clazz->getStaticFieldValue(fieldHIGH); + case MeasurementRecordPriority::NORMAL: + return clazz->getStaticFieldValue(fieldNORMAL); + case MeasurementRecordPriority::LOW: + return clazz->getStaticFieldValue(fieldLOW); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordSource.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordSource.hpp new file mode 100644 index 000000000..92e7c3849 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementRecordSource.hpp @@ -0,0 +1,68 @@ +/// +/// JMeasurementRecordSource.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementRecordSource.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "MeasurementRecordSource" and the the Kotlin enum "MeasurementRecordSource". + */ + struct JMeasurementRecordSource final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementRecordSource;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum MeasurementRecordSource. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementRecordSource toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(MeasurementRecordSource value) { + static const auto clazz = javaClassStatic(); + static const auto fieldNATIVE = clazz->getStaticField("NATIVE"); + static const auto fieldJAVASCRIPT = clazz->getStaticField("JAVASCRIPT"); + static const auto fieldSTORE = clazz->getStaticField("STORE"); + static const auto fieldPUSH = clazz->getStaticField("PUSH"); + static const auto fieldSERVER_CORRELATION = clazz->getStaticField("SERVER_CORRELATION"); + + switch (value) { + case MeasurementRecordSource::NATIVE: + return clazz->getStaticFieldValue(fieldNATIVE); + case MeasurementRecordSource::JAVASCRIPT: + return clazz->getStaticFieldValue(fieldJAVASCRIPT); + case MeasurementRecordSource::STORE: + return clazz->getStaticFieldValue(fieldSTORE); + case MeasurementRecordSource::PUSH: + return clazz->getStaticFieldValue(fieldPUSH); + case MeasurementRecordSource::SERVER_CORRELATION: + return clazz->getStaticFieldValue(fieldSERVER_CORRELATION); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementSessionSnapshot.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementSessionSnapshot.hpp new file mode 100644 index 000000000..5a5fdd8f1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementSessionSnapshot.hpp @@ -0,0 +1,65 @@ +/// +/// JMeasurementSessionSnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementSessionSnapshot.hpp" + +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementSessionSnapshot" and the the Kotlin data class "MeasurementSessionSnapshot". + */ + struct JMeasurementSessionSnapshot final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementSessionSnapshot;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementSessionSnapshot by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementSessionSnapshot toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldId = clazz->getField("id"); + jni::local_ref id = this->getFieldValue(fieldId); + static const auto fieldSequence = clazz->getField("sequence"); + double sequence = this->getFieldValue(fieldSequence); + static const auto fieldStartedAt = clazz->getField("startedAt"); + jni::local_ref startedAt = this->getFieldValue(fieldStartedAt); + static const auto fieldReason = clazz->getField("reason"); + jni::local_ref reason = this->getFieldValue(fieldReason); + return MeasurementSessionSnapshot( + id->toStdString(), + sequence, + startedAt->toStdString(), + reason->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementSessionSnapshot& value) { + return newInstance( + jni::make_jstring(value.id), + value.sequence, + jni::make_jstring(value.startedAt), + jni::make_jstring(value.reason) + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JMeasurementStateBridge.hpp b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementStateBridge.hpp new file mode 100644 index 000000000..c108d487c --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JMeasurementStateBridge.hpp @@ -0,0 +1,102 @@ +/// +/// JMeasurementStateBridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "MeasurementStateBridge.hpp" + +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "MeasurementStateBridge" and the the Kotlin data class "MeasurementStateBridge". + */ + struct JMeasurementStateBridge final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/MeasurementStateBridge;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct MeasurementStateBridge by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + MeasurementStateBridge toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldInstallationId = clazz->getField("installationId"); + jni::local_ref installationId = this->getFieldValue(fieldInstallationId); + static const auto fieldFirstOpenedAt = clazz->getField("firstOpenedAt"); + jni::local_ref firstOpenedAt = this->getFieldValue(fieldFirstOpenedAt); + static const auto fieldInstallationSequence = clazz->getField("installationSequence"); + double installationSequence = this->getFieldValue(fieldInstallationSequence); + static const auto fieldReadiness = clazz->getField("readiness"); + jni::local_ref readiness = this->getFieldValue(fieldReadiness); + static const auto fieldCurrentSessionId = clazz->getField("currentSessionId"); + jni::local_ref currentSessionId = this->getFieldValue(fieldCurrentSessionId); + static const auto fieldCurrentSessionSequence = clazz->getField("currentSessionSequence"); + jni::local_ref currentSessionSequence = this->getFieldValue(fieldCurrentSessionSequence); + static const auto fieldConsentRevision = clazz->getField("consentRevision"); + double consentRevision = this->getFieldValue(fieldConsentRevision); + static const auto fieldConfigurationRevision = clazz->getField("configurationRevision"); + double configurationRevision = this->getFieldValue(fieldConfigurationRevision); + static const auto fieldOutboxCritical = clazz->getField("outboxCritical"); + double outboxCritical = this->getFieldValue(fieldOutboxCritical); + static const auto fieldOutboxHigh = clazz->getField("outboxHigh"); + double outboxHigh = this->getFieldValue(fieldOutboxHigh); + static const auto fieldOutboxNormal = clazz->getField("outboxNormal"); + double outboxNormal = this->getFieldValue(fieldOutboxNormal); + static const auto fieldOutboxLow = clazz->getField("outboxLow"); + double outboxLow = this->getFieldValue(fieldOutboxLow); + static const auto fieldOldestRecordAgeMs = clazz->getField("oldestRecordAgeMs"); + jni::local_ref oldestRecordAgeMs = this->getFieldValue(fieldOldestRecordAgeMs); + return MeasurementStateBridge( + installationId->toStdString(), + firstOpenedAt->toStdString(), + installationSequence, + readiness->toStdString(), + currentSessionId != nullptr ? std::make_optional(currentSessionId->toStdString()) : std::nullopt, + currentSessionSequence != nullptr ? std::make_optional(currentSessionSequence->value()) : std::nullopt, + consentRevision, + configurationRevision, + outboxCritical, + outboxHigh, + outboxNormal, + outboxLow, + oldestRecordAgeMs != nullptr ? std::make_optional(oldestRecordAgeMs->value()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const MeasurementStateBridge& value) { + return newInstance( + jni::make_jstring(value.installationId), + jni::make_jstring(value.firstOpenedAt), + value.installationSequence, + jni::make_jstring(value.readiness), + value.currentSessionId.has_value() ? jni::make_jstring(value.currentSessionId.value()) : nullptr, + value.currentSessionSequence.has_value() ? jni::JDouble::valueOf(value.currentSessionSequence.value()) : nullptr, + value.consentRevision, + value.configurationRevision, + value.outboxCritical, + value.outboxHigh, + value.outboxNormal, + value.outboxLow, + value.oldestRecordAgeMs.has_value() ? jni::JDouble::valueOf(value.oldestRecordAgeMs.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEvent.hpp b/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEvent.hpp new file mode 100644 index 000000000..e4eb6938c --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEvent.hpp @@ -0,0 +1,80 @@ +/// +/// JNativeNotificationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NativeNotificationEvent.hpp" + +#include "JNativeNotificationEventKind.hpp" +#include "NativeNotificationEventKind.hpp" +#include +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "NativeNotificationEvent" and the the Kotlin data class "NativeNotificationEvent". + */ + struct JNativeNotificationEvent final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/NativeNotificationEvent;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct NativeNotificationEvent by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + NativeNotificationEvent toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldId = clazz->getField("id"); + jni::local_ref id = this->getFieldValue(fieldId); + static const auto fieldKind = clazz->getField("kind"); + jni::local_ref kind = this->getFieldValue(fieldKind); + static const auto fieldOccurredAt = clazz->getField("occurredAt"); + jni::local_ref occurredAt = this->getFieldValue(fieldOccurredAt); + static const auto fieldProtectedPayloadRef = clazz->getField("protectedPayloadRef"); + jni::local_ref protectedPayloadRef = this->getFieldValue(fieldProtectedPayloadRef); + static const auto fieldPushNotificationSendId = clazz->getField("pushNotificationSendId"); + jni::local_ref pushNotificationSendId = this->getFieldValue(fieldPushNotificationSendId); + static const auto fieldLink = clazz->getField("link"); + jni::local_ref link = this->getFieldValue(fieldLink); + static const auto fieldErrorCode = clazz->getField("errorCode"); + jni::local_ref errorCode = this->getFieldValue(fieldErrorCode); + return NativeNotificationEvent( + id->toStdString(), + kind->toCpp(), + occurredAt->toStdString(), + protectedPayloadRef != nullptr ? std::make_optional(protectedPayloadRef->toStdString()) : std::nullopt, + pushNotificationSendId != nullptr ? std::make_optional(pushNotificationSendId->toStdString()) : std::nullopt, + link != nullptr ? std::make_optional(link->toStdString()) : std::nullopt, + errorCode != nullptr ? std::make_optional(errorCode->toStdString()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const NativeNotificationEvent& value) { + return newInstance( + jni::make_jstring(value.id), + JNativeNotificationEventKind::fromCpp(value.kind), + jni::make_jstring(value.occurredAt), + value.protectedPayloadRef.has_value() ? jni::make_jstring(value.protectedPayloadRef.value()) : nullptr, + value.pushNotificationSendId.has_value() ? jni::make_jstring(value.pushNotificationSendId.value()) : nullptr, + value.link.has_value() ? jni::make_jstring(value.link.value()) : nullptr, + value.errorCode.has_value() ? jni::make_jstring(value.errorCode.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEventKind.hpp b/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEventKind.hpp new file mode 100644 index 000000000..562c88663 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JNativeNotificationEventKind.hpp @@ -0,0 +1,65 @@ +/// +/// JNativeNotificationEventKind.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NativeNotificationEventKind.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "NativeNotificationEventKind" and the the Kotlin enum "NativeNotificationEventKind". + */ + struct JNativeNotificationEventKind final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/NativeNotificationEventKind;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum NativeNotificationEventKind. + */ + [[maybe_unused]] + [[nodiscard]] + NativeNotificationEventKind toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(NativeNotificationEventKind value) { + static const auto clazz = javaClassStatic(); + static const auto fieldRECEIVED = clazz->getStaticField("RECEIVED"); + static const auto fieldOPENED = clazz->getStaticField("OPENED"); + static const auto fieldTOKENCHANGED = clazz->getStaticField("TOKENCHANGED"); + static const auto fieldREGISTRATIONERROR = clazz->getStaticField("REGISTRATIONERROR"); + + switch (value) { + case NativeNotificationEventKind::RECEIVED: + return clazz->getStaticFieldValue(fieldRECEIVED); + case NativeNotificationEventKind::OPENED: + return clazz->getStaticFieldValue(fieldOPENED); + case NativeNotificationEventKind::TOKENCHANGED: + return clazz->getStaticFieldValue(fieldTOKENCHANGED); + case NativeNotificationEventKind::REGISTRATIONERROR: + return clazz->getStaticFieldValue(fieldREGISTRATIONERROR); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JNativePushEnvironment.hpp b/libraries/react-native/nitrogen/generated/android/c++/JNativePushEnvironment.hpp new file mode 100644 index 000000000..b429aaf30 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JNativePushEnvironment.hpp @@ -0,0 +1,59 @@ +/// +/// JNativePushEnvironment.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NativePushEnvironment.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "NativePushEnvironment" and the the Kotlin enum "NativePushEnvironment". + */ + struct JNativePushEnvironment final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/NativePushEnvironment;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum NativePushEnvironment. + */ + [[maybe_unused]] + [[nodiscard]] + NativePushEnvironment toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(NativePushEnvironment value) { + static const auto clazz = javaClassStatic(); + static const auto fieldDEVELOPMENT = clazz->getStaticField("DEVELOPMENT"); + static const auto fieldPRODUCTION = clazz->getStaticField("PRODUCTION"); + + switch (value) { + case NativePushEnvironment::DEVELOPMENT: + return clazz->getStaticFieldValue(fieldDEVELOPMENT); + case NativePushEnvironment::PRODUCTION: + return clazz->getStaticFieldValue(fieldPRODUCTION); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JNativePushProvider.hpp b/libraries/react-native/nitrogen/generated/android/c++/JNativePushProvider.hpp new file mode 100644 index 000000000..936ab9112 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JNativePushProvider.hpp @@ -0,0 +1,59 @@ +/// +/// JNativePushProvider.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NativePushProvider.hpp" + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "NativePushProvider" and the the Kotlin enum "NativePushProvider". + */ + struct JNativePushProvider final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/NativePushProvider;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum NativePushProvider. + */ + [[maybe_unused]] + [[nodiscard]] + NativePushProvider toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(NativePushProvider value) { + static const auto clazz = javaClassStatic(); + static const auto fieldAPNS = clazz->getStaticField("APNS"); + static const auto fieldFCM = clazz->getStaticField("FCM"); + + switch (value) { + case NativePushProvider::APNS: + return clazz->getStaticFieldValue(fieldAPNS); + case NativePushProvider::FCM: + return clazz->getStaticFieldValue(fieldFCM); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JNativePushToken.hpp b/libraries/react-native/nitrogen/generated/android/c++/JNativePushToken.hpp new file mode 100644 index 000000000..765030878 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/c++/JNativePushToken.hpp @@ -0,0 +1,65 @@ +/// +/// JNativePushToken.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NativePushToken.hpp" + +#include "JNativePushEnvironment.hpp" +#include "JNativePushProvider.hpp" +#include "NativePushEnvironment.hpp" +#include "NativePushProvider.hpp" +#include + +namespace margelo::nitro::voidhash { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "NativePushToken" and the the Kotlin data class "NativePushToken". + */ + struct JNativePushToken final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/voidhash/NativePushToken;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct NativePushToken by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + NativePushToken toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldToken = clazz->getField("token"); + jni::local_ref token = this->getFieldValue(fieldToken); + static const auto fieldProvider = clazz->getField("provider"); + jni::local_ref provider = this->getFieldValue(fieldProvider); + static const auto fieldEnvironment = clazz->getField("environment"); + jni::local_ref environment = this->getFieldValue(fieldEnvironment); + return NativePushToken( + token->toStdString(), + provider->toCpp(), + environment->toCpp() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const NativePushToken& value) { + return newInstance( + jni::make_jstring(value.token), + JNativePushProvider::fromCpp(value.provider), + JNativePushEnvironment::fromCpp(value.environment) + ); + } + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewAndroidLayerType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewAndroidLayerType.hpp index 22e57e897..4fda7376f 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewAndroidLayerType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewAndroidLayerType.hpp @@ -44,7 +44,7 @@ namespace margelo::nitro::voidhash { static const auto fieldNONE = clazz->getStaticField("NONE"); static const auto fieldSOFTWARE = clazz->getStaticField("SOFTWARE"); static const auto fieldHARDWARE = clazz->getStaticField("HARDWARE"); - + switch (value) { case PaywallWebViewAndroidLayerType::NONE: return clazz->getStaticFieldValue(fieldNONE); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewCacheMode.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewCacheMode.hpp index 7a3e843eb..be6e71e68 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewCacheMode.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewCacheMode.hpp @@ -45,7 +45,7 @@ namespace margelo::nitro::voidhash { static const auto fieldLOAD_CACHE_ONLY = clazz->getStaticField("LOAD_CACHE_ONLY"); static const auto fieldLOAD_CACHE_ELSE_NETWORK = clazz->getStaticField("LOAD_CACHE_ELSE_NETWORK"); static const auto fieldLOAD_NO_CACHE = clazz->getStaticField("LOAD_NO_CACHE"); - + switch (value) { case PaywallWebViewCacheMode::LOAD_DEFAULT: return clazz->getStaticFieldValue(fieldLOAD_DEFAULT); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewDataDetectorType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewDataDetectorType.hpp index 261567c89..015785018 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewDataDetectorType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewDataDetectorType.hpp @@ -50,7 +50,7 @@ namespace margelo::nitro::voidhash { static const auto fieldFLIGHTNUMBER = clazz->getStaticField("FLIGHTNUMBER"); static const auto fieldLOOKUPSUGGESTION = clazz->getStaticField("LOOKUPSUGGESTION"); static const auto fieldALL = clazz->getStaticField("ALL"); - + switch (value) { case PaywallWebViewDataDetectorType::NONE: return clazz->getStaticFieldValue(fieldNONE); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewMixedContentMode.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewMixedContentMode.hpp index 2ef5f3000..94d08028a 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewMixedContentMode.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewMixedContentMode.hpp @@ -44,7 +44,7 @@ namespace margelo::nitro::voidhash { static const auto fieldNEVER = clazz->getStaticField("NEVER"); static const auto fieldALWAYS = clazz->getStaticField("ALWAYS"); static const auto fieldCOMPATIBILITY = clazz->getStaticField("COMPATIBILITY"); - + switch (value) { case PaywallWebViewMixedContentMode::NEVER: return clazz->getStaticFieldValue(fieldNEVER); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewNavigationType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewNavigationType.hpp index cd57dd6b4..b3e99d380 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewNavigationType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewNavigationType.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro::voidhash { static const auto fieldRELOAD = clazz->getStaticField("RELOAD"); static const auto fieldFORMRESUBMIT = clazz->getStaticField("FORMRESUBMIT"); static const auto fieldOTHER = clazz->getStaticField("OTHER"); - + switch (value) { case PaywallWebViewNavigationType::CLICK: return clazz->getStaticFieldValue(fieldCLICK); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewOverScrollModeType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewOverScrollModeType.hpp index d29669ac1..78a2973a0 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewOverScrollModeType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPaywallWebViewOverScrollModeType.hpp @@ -44,7 +44,7 @@ namespace margelo::nitro::voidhash { static const auto fieldNEVER = clazz->getStaticField("NEVER"); static const auto fieldALWAYS = clazz->getStaticField("ALWAYS"); static const auto fieldCONTENT = clazz->getStaticField("CONTENT"); - + switch (value) { case PaywallWebViewOverScrollModeType::NEVER: return clazz->getStaticFieldValue(fieldNEVER); diff --git a/libraries/react-native/nitrogen/generated/android/c++/JPurchasedItemType.hpp b/libraries/react-native/nitrogen/generated/android/c++/JPurchasedItemType.hpp index aa4f42f66..c625d6356 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/JPurchasedItemType.hpp +++ b/libraries/react-native/nitrogen/generated/android/c++/JPurchasedItemType.hpp @@ -43,7 +43,7 @@ namespace margelo::nitro::voidhash { static const auto clazz = javaClassStatic(); static const auto fieldSUBSCRIPTION = clazz->getStaticField("SUBSCRIPTION"); static const auto fieldINAPP = clazz->getStaticField("INAPP"); - + switch (value) { case PurchasedItemType::SUBSCRIPTION: return clazz->getStaticFieldValue(fieldSUBSCRIPTION); diff --git a/libraries/react-native/nitrogen/generated/android/c++/views/JHybridPaywallWebViewStateUpdater.cpp b/libraries/react-native/nitrogen/generated/android/c++/views/JHybridPaywallWebViewStateUpdater.cpp index 5b7f7d287..79a4f99f3 100644 --- a/libraries/react-native/nitrogen/generated/android/c++/views/JHybridPaywallWebViewStateUpdater.cpp +++ b/libraries/react-native/nitrogen/generated/android/c++/views/JHybridPaywallWebViewStateUpdater.cpp @@ -19,7 +19,7 @@ void JHybridPaywallWebViewStateUpdater::updateViewProps(jni::alias_ref javaView, jni::alias_ref stateWrapperInterface) { JHybridPaywallWebViewSpec* view = javaView->cthis(); - + // Get concrete StateWrapperImpl from passed StateWrapper interface object jobject rawStateWrapper = stateWrapperInterface.get(); if (!stateWrapperInterface->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) { diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_MeasurementBridgeEvent.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_MeasurementBridgeEvent.kt new file mode 100644 index 000000000..700106337 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_MeasurementBridgeEvent.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_MeasurementBridgeEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* +import dalvik.annotation.optimization.FastNative + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_MeasurementBridgeEvent: (MeasurementBridgeEvent) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(event: MeasurementBridgeEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_MeasurementBridgeEvent_cxx: Func_void_MeasurementBridgeEvent { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(event: MeasurementBridgeEvent): Unit + = invoke_cxx(event) + + @FastNative + private external fun invoke_cxx(event: MeasurementBridgeEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in Java/Kotlin, via a `(MeasurementBridgeEvent) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_MeasurementBridgeEvent_java(private val function: (MeasurementBridgeEvent) -> Unit): Func_void_MeasurementBridgeEvent { + @DoNotStrip + @Keep + override fun invoke(event: MeasurementBridgeEvent): Unit { + return this.function(event) + } +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_NativeNotificationEvent.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_NativeNotificationEvent.kt new file mode 100644 index 000000000..b1f08bbf5 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/Func_void_NativeNotificationEvent.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_NativeNotificationEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* +import dalvik.annotation.optimization.FastNative + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_NativeNotificationEvent: (NativeNotificationEvent) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(event: NativeNotificationEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_NativeNotificationEvent_cxx: Func_void_NativeNotificationEvent { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(event: NativeNotificationEvent): Unit + = invoke_cxx(event) + + @FastNative + private external fun invoke_cxx(event: NativeNotificationEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in Java/Kotlin, via a `(NativeNotificationEvent) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_NativeNotificationEvent_java(private val function: (NativeNotificationEvent) -> Unit): Func_void_NativeNotificationEvent { + @DoNotStrip + @Keep + override fun invoke(event: NativeNotificationEvent): Unit { + return this.function(event) + } +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingAcknowledgeResultSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingAcknowledgeResultSpec.kt index 80904b116..6e3c5c3a5 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingAcknowledgeResultSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingAcknowledgeResultSpec.kt @@ -40,21 +40,21 @@ abstract class HybridGoogleBillingAcknowledgeResultSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val responseCode: Double - + @get:DoNotStrip @get:Keep abstract val debugMessage: String? - + @get:DoNotStrip @get:Keep abstract val code: String - + @get:DoNotStrip @get:Keep abstract val message: String // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingConsumeResultSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingConsumeResultSpec.kt index 07246745a..761dad890 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingConsumeResultSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingConsumeResultSpec.kt @@ -40,25 +40,25 @@ abstract class HybridGoogleBillingConsumeResultSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val responseCode: Double - + @get:DoNotStrip @get:Keep abstract val debugMessage: String? - + @get:DoNotStrip @get:Keep abstract val code: String - + @get:DoNotStrip @get:Keep abstract val message: String - + @get:DoNotStrip @get:Keep abstract val purchaseTokenAndroid: String? // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.kt index 7c37b516e..5a474f8ac 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.kt @@ -40,17 +40,17 @@ abstract class HybridGoogleBillingOneTimePurchaseOfferDetailsSpec: HybridObject( @get:DoNotStrip @get:Keep abstract val priceCurrencyCode: String - + @get:DoNotStrip @get:Keep abstract val formattedPrice: String - + @get:DoNotStrip @get:Keep abstract val priceAmountMicros: String // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhaseSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhaseSpec.kt index 6eead18a5..74cde3d1d 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhaseSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhaseSpec.kt @@ -40,29 +40,29 @@ abstract class HybridGoogleBillingPricingPhaseSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val formattedPrice: String - + @get:DoNotStrip @get:Keep abstract val priceCurrencyCode: String - + @get:DoNotStrip @get:Keep abstract val billingPeriod: String - + @get:DoNotStrip @get:Keep abstract val billingCycleCount: Double - + @get:DoNotStrip @get:Keep abstract val priceAmountMicros: String - + @get:DoNotStrip @get:Keep abstract val recurrenceMode: Double // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhasesSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhasesSpec.kt index 108f966a5..dffabf76c 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhasesSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPricingPhasesSpec.kt @@ -42,7 +42,7 @@ abstract class HybridGoogleBillingPricingPhasesSpec: HybridObject() { abstract val pricingPhaseList: Array // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingProductDetailSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingProductDetailSpec.kt index c063d3225..40c597a2f 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingProductDetailSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingProductDetailSpec.kt @@ -40,45 +40,45 @@ abstract class HybridGoogleBillingProductDetailSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val id: String - + @get:DoNotStrip @get:Keep abstract val title: String - + @get:DoNotStrip @get:Keep abstract val description: String - + @get:DoNotStrip @get:Keep abstract val type: String - + @get:DoNotStrip @get:Keep abstract val displayName: String - + @get:DoNotStrip @get:Keep abstract val platform: String - + @get:DoNotStrip @get:Keep abstract val currency: String - + @get:DoNotStrip @get:Keep abstract val displayPrice: String - + @get:DoNotStrip @get:Keep abstract val subscriptionOfferDetails: Array? - + @get:DoNotStrip @get:Keep abstract val oneTimePurchaseOfferDetails: HybridGoogleBillingOneTimePurchaseOfferDetailsSpec? // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPurchaseSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPurchaseSpec.kt index 359dce300..5de025141 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPurchaseSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingPurchaseSpec.kt @@ -40,61 +40,61 @@ abstract class HybridGoogleBillingPurchaseSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val id: String - + @get:DoNotStrip @get:Keep abstract val ids: Array - + @get:DoNotStrip @get:Keep abstract val orderId: String? - + @get:DoNotStrip @get:Keep abstract val purchaseTime: Double - + @get:DoNotStrip @get:Keep abstract val originalJson: String - + @get:DoNotStrip @get:Keep abstract val purchaseToken: String - + @get:DoNotStrip @get:Keep abstract val signature: String - + @get:DoNotStrip @get:Keep abstract val isAutoRenewing: Boolean? - + @get:DoNotStrip @get:Keep abstract val isAcknowledged: Boolean - + @get:DoNotStrip @get:Keep abstract val purchaseState: Double - + @get:DoNotStrip @get:Keep abstract val packageName: String - + @get:DoNotStrip @get:Keep abstract val developerPayload: String - + @get:DoNotStrip @get:Keep abstract val obfuscatedAccountId: String? - + @get:DoNotStrip @get:Keep abstract val obfuscatedProfileId: String? // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSpec.kt index 835b96795..0bb263b44 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSpec.kt @@ -41,26 +41,26 @@ abstract class HybridGoogleBillingSpec: HybridObject() { // Methods abstract fun initConnection(onPurchase: ((purchase: HybridGoogleBillingPurchaseSpec) -> Unit)?): Promise - + @DoNotStrip @Keep private fun initConnection_cxx(onPurchase: Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridGoogleBillingPurchaseSpec_?): Promise { val __result = initConnection(onPurchase?.let { it }) return __result } - + @DoNotStrip @Keep abstract fun endConnection(): Promise - + @DoNotStrip @Keep abstract fun getItemsByType(type: GoogleBillingProductType, skus: Array): Promise> - + @DoNotStrip @Keep abstract fun buyItemByType(params: GoogleBillingBuyItemByTypeParams): Promise> - + @DoNotStrip @Keep abstract fun acknowledgePurchase(token: String): Promise diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSubscriptionOfferDetailsSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSubscriptionOfferDetailsSpec.kt index aff256995..296766df8 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSubscriptionOfferDetailsSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridGoogleBillingSubscriptionOfferDetailsSpec.kt @@ -40,25 +40,25 @@ abstract class HybridGoogleBillingSubscriptionOfferDetailsSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val basePlanId: String - + @get:DoNotStrip @get:Keep abstract val offerId: String? - + @get:DoNotStrip @get:Keep abstract val offerToken: String - + @get:DoNotStrip @get:Keep abstract val offerTags: Array - + @get:DoNotStrip @get:Keep abstract val pricingPhases: HybridGoogleBillingPricingPhasesSpec // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridMeasurementSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridMeasurementSpec.kt new file mode 100644 index 000000000..753e35db3 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridMeasurementSpec.kt @@ -0,0 +1,149 @@ +/// +/// HybridMeasurementSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * A Kotlin class representing the Measurement HybridObject. + * Implement this abstract class to create Kotlin-based instances of Measurement. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "SimpleRedundantLet", + "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" +) +abstract class HybridMeasurementSpec: HybridObject() { + @DoNotStrip + private var mHybridData: HybridData = initHybrid() + + init { + super.updateNative(mHybridData) + } + + override fun updateNative(hybridData: HybridData) { + mHybridData = hybridData + super.updateNative(hybridData) + } + + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun initialize(publishableKey: String, configuration: MeasurementInitializeConfiguration): Promise + + @DoNotStrip + @Keep + abstract fun enqueue(command: MeasurementCommand): Promise + + @DoNotStrip + @Keep + abstract fun flush(): Promise + + @DoNotStrip + @Keep + abstract fun getInstallationId(): Promise + + @DoNotStrip + @Keep + abstract fun getState(): Promise + + abstract fun subscribe(subscriptionId: String, listener: (event: MeasurementBridgeEvent) -> Unit): Unit + + @DoNotStrip + @Keep + private fun subscribe_cxx(subscriptionId: String, listener: Func_void_MeasurementBridgeEvent): Unit { + val __result = subscribe(subscriptionId, listener) + return __result + } + + @DoNotStrip + @Keep + abstract fun unsubscribe(subscriptionId: String): Unit + + @DoNotStrip + @Keep + abstract fun peekInbox(limit: Double): Promise> + + @DoNotStrip + @Keep + abstract fun acknowledgeInbox(entryId: String): Promise + + @DoNotStrip + @Keep + abstract fun readProtectedEvidence(blobId: String): Promise + + @DoNotStrip + @Keep + abstract fun putProtectedEvidence(input: MeasurementProtectedEvidenceInput): Promise + + @DoNotStrip + @Keep + abstract fun deleteProtectedEvidence(blobId: String): Promise + + @DoNotStrip + @Keep + abstract fun deleteProtectedData(requestId: String): Promise + + @DoNotStrip + @Keep + abstract fun getMeasurementConfigurationState(): Promise + + @DoNotStrip + @Keep + abstract fun persistMeasurementConfigurationState(version: Double, payload: ArrayBuffer): Promise + + @DoNotStrip + @Keep + abstract fun applyMeasurementConfiguration(version: Double, payload: ArrayBuffer): Promise + + @DoNotStrip + @Keep + abstract fun applyMeasurementStorageLimits(maxOutboxRecords: Double, maxOutboxBytes: Double, maxProtectedBytes: Double): Promise + + @DoNotStrip + @Keep + abstract fun getPushRegistrationState(): Promise + + @DoNotStrip + @Keep + abstract fun persistPushRegistrationState(payload: ArrayBuffer): Promise + + @DoNotStrip + @Keep + abstract fun clearPushRegistrationState(): Promise + + @DoNotStrip + @Keep + abstract fun getTestDeviceState(): Promise + + @DoNotStrip + @Keep + abstract fun persistTestDeviceState(enabled: Boolean): Promise + + @DoNotStrip + @Keep + abstract fun hasDedupe(namespace: String, key: String): Promise + + @DoNotStrip + @Keep + abstract fun checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Double): Promise + + private external fun initHybrid(): HybridData + + companion object { + private const val TAG = "HybridMeasurementSpec" + } +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridNotificationsSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridNotificationsSpec.kt new file mode 100644 index 000000000..d6a1a8db6 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridNotificationsSpec.kt @@ -0,0 +1,77 @@ +/// +/// HybridNotificationsSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * A Kotlin class representing the Notifications HybridObject. + * Implement this abstract class to create Kotlin-based instances of Notifications. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "SimpleRedundantLet", + "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" +) +abstract class HybridNotificationsSpec: HybridObject() { + @DoNotStrip + private var mHybridData: HybridData = initHybrid() + + init { + super.updateNative(mHybridData) + } + + override fun updateNative(hybridData: HybridData) { + mHybridData = hybridData + super.updateNative(hybridData) + } + + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun getPermissionStatus(): Promise + + @DoNotStrip + @Keep + abstract fun requestPermission(provisional: Boolean): Promise + + @DoNotStrip + @Keep + abstract fun getToken(): Promise + + @DoNotStrip + @Keep + abstract fun setBadgeCount(count: Double): Promise + + abstract fun subscribe(subscriptionId: String, listener: (event: NativeNotificationEvent) -> Unit): Unit + + @DoNotStrip + @Keep + private fun subscribe_cxx(subscriptionId: String, listener: Func_void_NativeNotificationEvent): Unit { + val __result = subscribe(subscriptionId, listener) + return __result + } + + @DoNotStrip + @Keep + abstract fun unsubscribe(subscriptionId: String): Unit + + private external fun initHybrid(): HybridData + + companion object { + private const val TAG = "HybridNotificationsSpec" + } +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallPresenterSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallPresenterSpec.kt index 22e36b508..aea957dc0 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallPresenterSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallPresenterSpec.kt @@ -37,30 +37,30 @@ abstract class HybridPaywallPresenterSpec: HybridObject() { } // Properties - + // Methods @DoNotStrip @Keep abstract fun preload(locationSlug: String, htmlUrl: String): Promise - + abstract fun show(locationSlug: String, htmlUrl: String, onBridgeEvent: ((rawEvent: String) -> Unit)?, onDismiss: (() -> Unit)?): Promise - + @DoNotStrip @Keep private fun show_cxx(locationSlug: String, htmlUrl: String, onBridgeEvent: Func_void_std__string?, onDismiss: Func_void?): Promise { val __result = show(locationSlug, htmlUrl, onBridgeEvent?.let { it }, onDismiss?.let { it }) return __result } - + @DoNotStrip @Keep abstract fun dismiss(): Promise - + @DoNotStrip @Keep abstract fun release(locationSlug: String): Unit - + @DoNotStrip @Keep abstract fun postMessage(locationSlug: String, data: String): Unit diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallWebViewSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallWebViewSpec.kt index 1b517dcd0..8477c8d69 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallWebViewSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPaywallWebViewSpec.kt @@ -43,219 +43,219 @@ abstract class HybridPaywallWebViewSpec: HybridView() { @set:DoNotStrip @set:Keep abstract var source: PaywallWebViewSource? - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var javaScriptEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var cacheEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var incognito: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var userAgent: String? - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var applicationNameForUserAgent: String? - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var injectedJavaScript: String? - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var injectedJavaScriptBeforeContentLoaded: String? - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var injectedJavaScriptForMainFrameOnly: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var injectedJavaScriptBeforeContentLoadedForMainFrameOnly: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var mediaPlaybackRequiresUserAction: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowsInlineMediaPlayback: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowsPictureInPictureMediaPlayback: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowsAirPlayForMediaPlayback: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowsFullscreenVideo: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var setSupportMultipleWindows: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var setBuiltInZoomControls: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var setDisplayZoomControls: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var scalesPageToFit: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var thirdPartyCookiesEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var sharedCookiesEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowFileAccess: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowFileAccessFromFileURLs: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var allowUniversalAccessFromFileURLs: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var textZoom: Double - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var overScrollMode: PaywallWebViewOverScrollModeType - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var cacheMode: PaywallWebViewCacheMode - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var mixedContentMode: PaywallWebViewMixedContentMode - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var androidLayerType: PaywallWebViewAndroidLayerType - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var geolocationEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var pullToRefreshEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var nestedScrollEnabled: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var bounces: Boolean - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var dataDetectorTypes: Array - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var originWhitelist: Array - + @get:DoNotStrip @get:Keep @set:DoNotStrip @set:Keep abstract var messagingEnabled: Boolean - + abstract var onLoadingStart: ((event: PaywallWebViewNavigationEvent) -> Unit)? - + private var onLoadingStart_cxx: Func_void_PaywallWebViewNavigationEvent? @Keep @DoNotStrip @@ -267,9 +267,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onLoadingStart = value?.let { it } } - + abstract var onLoadingProgress: ((event: PaywallWebViewProgressEvent) -> Unit)? - + private var onLoadingProgress_cxx: Func_void_PaywallWebViewProgressEvent? @Keep @DoNotStrip @@ -281,9 +281,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onLoadingProgress = value?.let { it } } - + abstract var onLoadingFinish: ((event: PaywallWebViewNavigationEvent) -> Unit)? - + private var onLoadingFinish_cxx: Func_void_PaywallWebViewNavigationEvent? @Keep @DoNotStrip @@ -295,9 +295,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onLoadingFinish = value?.let { it } } - + abstract var onLoadingError: ((event: PaywallWebViewErrorEvent) -> Unit)? - + private var onLoadingError_cxx: Func_void_PaywallWebViewErrorEvent? @Keep @DoNotStrip @@ -309,9 +309,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onLoadingError = value?.let { it } } - + abstract var onHttpError: ((event: PaywallWebViewHttpErrorEvent) -> Unit)? - + private var onHttpError_cxx: Func_void_PaywallWebViewHttpErrorEvent? @Keep @DoNotStrip @@ -323,9 +323,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onHttpError = value?.let { it } } - + abstract var onMessage: ((event: PaywallWebViewMessageEvent) -> Unit)? - + private var onMessage_cxx: Func_void_PaywallWebViewMessageEvent? @Keep @DoNotStrip @@ -337,9 +337,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onMessage = value?.let { it } } - + abstract var onOpenWindow: ((event: PaywallWebViewOpenWindowEvent) -> Unit)? - + private var onOpenWindow_cxx: Func_void_PaywallWebViewOpenWindowEvent? @Keep @DoNotStrip @@ -351,9 +351,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onOpenWindow = value?.let { it } } - + abstract var onFileDownload: ((event: PaywallWebViewFileDownloadEvent) -> Unit)? - + private var onFileDownload_cxx: Func_void_PaywallWebViewFileDownloadEvent? @Keep @DoNotStrip @@ -365,9 +365,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onFileDownload = value?.let { it } } - + abstract var onRenderProcessGone: ((event: PaywallWebViewRenderProcessGoneEvent) -> Unit)? - + private var onRenderProcessGone_cxx: Func_void_PaywallWebViewRenderProcessGoneEvent? @Keep @DoNotStrip @@ -379,9 +379,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onRenderProcessGone = value?.let { it } } - + abstract var onContentProcessDidTerminate: ((event: PaywallWebViewBaseEvent) -> Unit)? - + private var onContentProcessDidTerminate_cxx: Func_void_PaywallWebViewBaseEvent? @Keep @DoNotStrip @@ -393,9 +393,9 @@ abstract class HybridPaywallWebViewSpec: HybridView() { set(value) { onContentProcessDidTerminate = value?.let { it } } - + abstract var onShouldStartLoadWithRequest: ((event: PaywallWebViewShouldStartLoadRequest) -> Boolean)? - + private var onShouldStartLoadWithRequest_cxx: Func_bool_PaywallWebViewShouldStartLoadRequest? @Keep @DoNotStrip @@ -412,43 +412,43 @@ abstract class HybridPaywallWebViewSpec: HybridView() { @DoNotStrip @Keep abstract fun goBack(): Unit - + @DoNotStrip @Keep abstract fun goForward(): Unit - + @DoNotStrip @Keep abstract fun reload(): Unit - + @DoNotStrip @Keep abstract fun stopLoading(): Unit - + @DoNotStrip @Keep abstract fun requestFocus(): Unit - + @DoNotStrip @Keep abstract fun postMessage(data: String): Unit - + @DoNotStrip @Keep abstract fun injectJavaScript(javascript: String): Unit - + @DoNotStrip @Keep abstract fun loadUrl(url: String): Unit - + @DoNotStrip @Keep abstract fun clearFormData(): Unit - + @DoNotStrip @Keep abstract fun clearHistory(): Unit - + @DoNotStrip @Keep abstract fun clearCache(includeDiskFiles: Boolean): Unit diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPurchasedItemSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPurchasedItemSpec.kt index 0bcf7cf20..2cdccd57a 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPurchasedItemSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridPurchasedItemSpec.kt @@ -42,7 +42,7 @@ abstract class HybridPurchasedItemSpec: HybridObject() { @set:DoNotStrip @set:Keep abstract var type: PurchasedItemType - + @get:DoNotStrip @get:Keep @set:DoNotStrip @@ -50,7 +50,7 @@ abstract class HybridPurchasedItemSpec: HybridObject() { abstract var sku: String // Methods - + private external fun initHybrid(): HybridData diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridVoidhashSpec.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridVoidhashSpec.kt index ce33cf1ee..78761fe7d 100644 --- a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridVoidhashSpec.kt +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/HybridVoidhashSpec.kt @@ -37,7 +37,7 @@ abstract class HybridVoidhashSpec: HybridObject() { } // Properties - + // Methods @DoNotStrip diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeError.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeError.kt new file mode 100644 index 000000000..1f0725ff1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeError.kt @@ -0,0 +1,30 @@ +/// +/// MeasurementBridgeError.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementBridgeError". + */ +@DoNotStrip +@Keep +data class MeasurementBridgeError + @DoNotStrip + @Keep + constructor( + val code: String, + val message: String, + val source: MeasurementBridgeSource, + val capability: String?, + val reason: String? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeEvent.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeEvent.kt new file mode 100644 index 000000000..04d49b023 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeEvent.kt @@ -0,0 +1,31 @@ +/// +/// MeasurementBridgeEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementBridgeEvent". + */ +@DoNotStrip +@Keep +data class MeasurementBridgeEvent + @DoNotStrip + @Keep + constructor( + val subscriptionId: String, + val event: String, + val recordId: String?, + val requestId: String?, + val payload: ArrayBuffer?, + val error: MeasurementBridgeError? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeSource.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeSource.kt new file mode 100644 index 000000000..b4b89a812 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementBridgeSource.kt @@ -0,0 +1,26 @@ +/// +/// MeasurementBridgeSource.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementBridgeSource". + */ +@DoNotStrip +@Keep +enum class MeasurementBridgeSource { + IOS, + ANDROID, + CORE; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommand.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommand.kt new file mode 100644 index 000000000..f68482a09 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommand.kt @@ -0,0 +1,36 @@ +/// +/// MeasurementCommand.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementCommand". + */ +@DoNotStrip +@Keep +data class MeasurementCommand + @DoNotStrip + @Keep + constructor( + val kind: MeasurementCommandKind, + val commandId: String, + val recordType: String, + val occurredAt: String, + val source: MeasurementRecordSource, + val priority: MeasurementRecordPriority, + val publicPayload: ArrayBuffer, + val protectedEvidenceRef: String?, + val identity: MeasurementIdentitySnapshot?, + val consent: MeasurementConsentSnapshot?, + val session: MeasurementSessionSnapshot? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandKind.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandKind.kt new file mode 100644 index 000000000..50274e9d2 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandKind.kt @@ -0,0 +1,33 @@ +/// +/// MeasurementCommandKind.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementCommandKind". + */ +@DoNotStrip +@Keep +enum class MeasurementCommandKind { + ENQUEUERECORD, + IDENTITYTRANSITION, + CONSENTTRANSITION, + SESSIONSIGNAL, + COLDLAUNCHINPUT, + TRANSACTIONDEDUP, + LINKINPUT, + PUSHINPUT, + PURCHASEINPUT, + IDENTIFIERINPUT; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandResult.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandResult.kt new file mode 100644 index 000000000..a79d8dde7 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementCommandResult.kt @@ -0,0 +1,29 @@ +/// +/// MeasurementCommandResult.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementCommandResult". + */ +@DoNotStrip +@Keep +data class MeasurementCommandResult + @DoNotStrip + @Keep + constructor( + val accepted: Boolean, + val recordId: String?, + val installationSequence: Double?, + val error: MeasurementBridgeError? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConfigurationStateBridge.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConfigurationStateBridge.kt new file mode 100644 index 000000000..f03eba821 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConfigurationStateBridge.kt @@ -0,0 +1,27 @@ +/// +/// MeasurementConfigurationStateBridge.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementConfigurationStateBridge". + */ +@DoNotStrip +@Keep +data class MeasurementConfigurationStateBridge + @DoNotStrip + @Keep + constructor( + val version: Double, + val payload: ArrayBuffer? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConsentSnapshot.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConsentSnapshot.kt new file mode 100644 index 000000000..c07f50c91 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementConsentSnapshot.kt @@ -0,0 +1,34 @@ +/// +/// MeasurementConsentSnapshot.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementConsentSnapshot". + */ +@DoNotStrip +@Keep +data class MeasurementConsentSnapshot + @DoNotStrip + @Keep + constructor( + val revision: Double, + val decidedAt: String, + val source: String, + val gdprApplies: Boolean?, + val dataUsage: Boolean?, + val adsPersonalization: Boolean?, + val adStorage: Boolean?, + val collectionOptOut: Boolean?, + val partnerSharingOptOut: Boolean? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementFlushBridgeResult.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementFlushBridgeResult.kt new file mode 100644 index 000000000..2b502f99a --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementFlushBridgeResult.kt @@ -0,0 +1,29 @@ +/// +/// MeasurementFlushBridgeResult.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementFlushBridgeResult". + */ +@DoNotStrip +@Keep +data class MeasurementFlushBridgeResult + @DoNotStrip + @Keep + constructor( + val accepted: Double, + val scheduled: Double, + val quarantined: Double, + val policyBlocked: Double + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementIdentitySnapshot.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementIdentitySnapshot.kt new file mode 100644 index 000000000..6dc63eebb --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementIdentitySnapshot.kt @@ -0,0 +1,29 @@ +/// +/// MeasurementIdentitySnapshot.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementIdentitySnapshot". + */ +@DoNotStrip +@Keep +data class MeasurementIdentitySnapshot + @DoNotStrip + @Keep + constructor( + val distinctId: String, + val anonymousId: String?, + val personId: String?, + val revision: Double + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInboxEntry.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInboxEntry.kt new file mode 100644 index 000000000..4cd7aeabc --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInboxEntry.kt @@ -0,0 +1,31 @@ +/// +/// MeasurementInboxEntry.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementInboxEntry". + */ +@DoNotStrip +@Keep +data class MeasurementInboxEntry + @DoNotStrip + @Keep + constructor( + val id: String, + val kind: String, + val source: String, + val appState: String, + val receivedAt: String, + val protectedEvidenceRef: String + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInitializeConfiguration.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInitializeConfiguration.kt new file mode 100644 index 000000000..0557f973f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementInitializeConfiguration.kt @@ -0,0 +1,29 @@ +/// +/// MeasurementInitializeConfiguration.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementInitializeConfiguration". + */ +@DoNotStrip +@Keep +data class MeasurementInitializeConfiguration + @DoNotStrip + @Keep + constructor( + val apiUrl: String, + val ingestUrl: String, + val linksUrl: String, + val trustedConfigKeyIds: Array + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedEvidenceInput.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedEvidenceInput.kt new file mode 100644 index 000000000..2317ac858 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedEvidenceInput.kt @@ -0,0 +1,30 @@ +/// +/// MeasurementProtectedEvidenceInput.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementProtectedEvidenceInput". + */ +@DoNotStrip +@Keep +data class MeasurementProtectedEvidenceInput + @DoNotStrip + @Keep + constructor( + val blobId: String, + val purpose: MeasurementProtectedPurpose, + val consentRevision: Double, + val retentionClass: MeasurementProtectedRetention, + val value: ArrayBuffer + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedPurpose.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedPurpose.kt new file mode 100644 index 000000000..f92405fe8 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedPurpose.kt @@ -0,0 +1,32 @@ +/// +/// MeasurementProtectedPurpose.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementProtectedPurpose". + */ +@DoNotStrip +@Keep +enum class MeasurementProtectedPurpose { + ADVERTISING_IDENTIFIER, + DIAGNOSTIC_AUTHORIZATION, + EMAIL, + INSTALL_REFERRER, + LINK_CAPTURE, + PARTNER_CONTEXT, + PHONE, + PURCHASE_RECEIPT, + PUSH_TOKEN; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedRetention.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedRetention.kt new file mode 100644 index 000000000..ab5c52de2 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementProtectedRetention.kt @@ -0,0 +1,27 @@ +/// +/// MeasurementProtectedRetention.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementProtectedRetention". + */ +@DoNotStrip +@Keep +enum class MeasurementProtectedRetention { + EPHEMERAL, + INSTALLATION, + LEGAL, + TRANSACTION; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordPriority.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordPriority.kt new file mode 100644 index 000000000..cd9b68b32 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordPriority.kt @@ -0,0 +1,27 @@ +/// +/// MeasurementRecordPriority.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementRecordPriority". + */ +@DoNotStrip +@Keep +enum class MeasurementRecordPriority { + CRITICAL, + HIGH, + NORMAL, + LOW; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordSource.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordSource.kt new file mode 100644 index 000000000..d51b2808d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementRecordSource.kt @@ -0,0 +1,28 @@ +/// +/// MeasurementRecordSource.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "MeasurementRecordSource". + */ +@DoNotStrip +@Keep +enum class MeasurementRecordSource { + NATIVE, + JAVASCRIPT, + STORE, + PUSH, + SERVER_CORRELATION; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementSessionSnapshot.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementSessionSnapshot.kt new file mode 100644 index 000000000..eb4c2aa72 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementSessionSnapshot.kt @@ -0,0 +1,29 @@ +/// +/// MeasurementSessionSnapshot.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementSessionSnapshot". + */ +@DoNotStrip +@Keep +data class MeasurementSessionSnapshot + @DoNotStrip + @Keep + constructor( + val id: String, + val sequence: Double, + val startedAt: String, + val reason: String + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementStateBridge.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementStateBridge.kt new file mode 100644 index 000000000..3ac352a75 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/MeasurementStateBridge.kt @@ -0,0 +1,38 @@ +/// +/// MeasurementStateBridge.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "MeasurementStateBridge". + */ +@DoNotStrip +@Keep +data class MeasurementStateBridge + @DoNotStrip + @Keep + constructor( + val installationId: String, + val firstOpenedAt: String, + val installationSequence: Double, + val readiness: String, + val currentSessionId: String?, + val currentSessionSequence: Double?, + val consentRevision: Double, + val configurationRevision: Double, + val outboxCritical: Double, + val outboxHigh: Double, + val outboxNormal: Double, + val outboxLow: Double, + val oldestRecordAgeMs: Double? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEvent.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEvent.kt new file mode 100644 index 000000000..2a182573f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEvent.kt @@ -0,0 +1,32 @@ +/// +/// NativeNotificationEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "NativeNotificationEvent". + */ +@DoNotStrip +@Keep +data class NativeNotificationEvent + @DoNotStrip + @Keep + constructor( + val id: String, + val kind: NativeNotificationEventKind, + val occurredAt: String, + val protectedPayloadRef: String?, + val pushNotificationSendId: String?, + val link: String?, + val errorCode: String? + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEventKind.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEventKind.kt new file mode 100644 index 000000000..9a99cce4e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativeNotificationEventKind.kt @@ -0,0 +1,27 @@ +/// +/// NativeNotificationEventKind.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "NativeNotificationEventKind". + */ +@DoNotStrip +@Keep +enum class NativeNotificationEventKind { + RECEIVED, + OPENED, + TOKENCHANGED, + REGISTRATIONERROR; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushEnvironment.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushEnvironment.kt new file mode 100644 index 000000000..ff1542a98 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushEnvironment.kt @@ -0,0 +1,25 @@ +/// +/// NativePushEnvironment.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "NativePushEnvironment". + */ +@DoNotStrip +@Keep +enum class NativePushEnvironment { + DEVELOPMENT, + PRODUCTION; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushProvider.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushProvider.kt new file mode 100644 index 000000000..bc54c07cb --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushProvider.kt @@ -0,0 +1,25 @@ +/// +/// NativePushProvider.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "NativePushProvider". + */ +@DoNotStrip +@Keep +enum class NativePushProvider { + APNS, + FCM; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushToken.kt b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushToken.kt new file mode 100644 index 000000000..6cc9ffcc5 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/android/kotlin/com/margelo/nitro/voidhash/NativePushToken.kt @@ -0,0 +1,28 @@ +/// +/// NativePushToken.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.voidhash + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "NativePushToken". + */ +@DoNotStrip +@Keep +data class NativePushToken + @DoNotStrip + @Keep + constructor( + val token: String, + val provider: NativePushProvider, + val environment: NativePushEnvironment + ) { + /* main constructor */ +} diff --git a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.cpp b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.cpp index 374ecd6b6..d2f3aedd3 100644 --- a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.cpp +++ b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.cpp @@ -8,6 +8,8 @@ #include "NitroVoidhash-Swift-Cxx-Bridge.hpp" // Include C++ implementation defined types +#include "HybridMeasurementSpecSwift.hpp" +#include "HybridNotificationsSpecSwift.hpp" #include "HybridPaywallPresenterSpecSwift.hpp" #include "HybridPaywallWebViewSpecSwift.hpp" #include "HybridPurchasedItemSpecSwift.hpp" @@ -29,7 +31,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(result); }; } - + // pragma MARK: std::function Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper); @@ -37,7 +39,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(error); }; } - + // pragma MARK: std::function Func_void_std__string create_Func_void_std__string(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__string::fromUnsafe(swiftClosureWrapper); @@ -45,7 +47,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(rawEvent); }; } - + // pragma MARK: std::function Func_void create_Func_void(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void::fromUnsafe(swiftClosureWrapper); @@ -53,7 +55,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(); }; } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridPaywallPresenterSpec_cxx swiftPart = NitroVoidhash::HybridPaywallPresenterSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -69,7 +71,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridPaywallPresenterSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::function Func_void_PaywallWebViewNavigationEvent create_Func_void_PaywallWebViewNavigationEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewNavigationEvent::fromUnsafe(swiftClosureWrapper); @@ -77,7 +79,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewProgressEvent create_Func_void_PaywallWebViewProgressEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewProgressEvent::fromUnsafe(swiftClosureWrapper); @@ -85,7 +87,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewErrorEvent create_Func_void_PaywallWebViewErrorEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewErrorEvent::fromUnsafe(swiftClosureWrapper); @@ -93,7 +95,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewHttpErrorEvent create_Func_void_PaywallWebViewHttpErrorEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewHttpErrorEvent::fromUnsafe(swiftClosureWrapper); @@ -101,7 +103,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewMessageEvent create_Func_void_PaywallWebViewMessageEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewMessageEvent::fromUnsafe(swiftClosureWrapper); @@ -109,7 +111,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewOpenWindowEvent create_Func_void_PaywallWebViewOpenWindowEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewOpenWindowEvent::fromUnsafe(swiftClosureWrapper); @@ -117,7 +119,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewFileDownloadEvent create_Func_void_PaywallWebViewFileDownloadEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewFileDownloadEvent::fromUnsafe(swiftClosureWrapper); @@ -125,7 +127,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewRenderProcessGoneEvent create_Func_void_PaywallWebViewRenderProcessGoneEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewRenderProcessGoneEvent::fromUnsafe(swiftClosureWrapper); @@ -133,7 +135,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_void_PaywallWebViewBaseEvent create_Func_void_PaywallWebViewBaseEvent(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_PaywallWebViewBaseEvent::fromUnsafe(swiftClosureWrapper); @@ -141,7 +143,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(event); }; } - + // pragma MARK: std::function Func_bool_PaywallWebViewShouldStartLoadRequest create_Func_bool_PaywallWebViewShouldStartLoadRequest(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_bool_PaywallWebViewShouldStartLoadRequest::fromUnsafe(swiftClosureWrapper); @@ -150,7 +152,7 @@ namespace margelo::nitro::voidhash::bridge::swift { return __result; }; } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridPaywallWebViewSpec_cxx swiftPart = NitroVoidhash::HybridPaywallWebViewSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -166,7 +168,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridPaywallWebViewSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridPurchasedItemSpec_cxx swiftPart = NitroVoidhash::HybridPurchasedItemSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -182,7 +184,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridPurchasedItemSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::function& /* result */)> Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ create_Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_::fromUnsafe(swiftClosureWrapper); @@ -190,7 +192,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(result); }; } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridVoidhashSpec_cxx swiftPart = NitroVoidhash::HybridVoidhashSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -206,7 +208,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridVoidhashSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitTransactionSpec_cxx swiftPart = NitroVoidhash::HybridStorekitTransactionSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -222,7 +224,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridStorekitTransactionSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::function& /* transaction */)> Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ create_Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_::fromUnsafe(swiftClosureWrapper); @@ -230,7 +232,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(transaction); }; } - + // pragma MARK: std::function>& /* result */)> Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__ create_Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__::fromUnsafe(swiftClosureWrapper); @@ -238,7 +240,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(result); }; } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitProductSpec_cxx swiftPart = NitroVoidhash::HybridStorekitProductSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -254,7 +256,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridStorekitProductSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::function>& /* result */)> Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec__ create_Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec__(void* _Nonnull swiftClosureWrapper) { auto swiftClosure = NitroVoidhash::Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec__::fromUnsafe(swiftClosureWrapper); @@ -262,7 +264,7 @@ namespace margelo::nitro::voidhash::bridge::swift { swiftClosure.call(result); }; } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitSpec_cxx swiftPart = NitroVoidhash::HybridStorekitSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -278,7 +280,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridStorekitSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitProductSubscriptionSpec_cxx swiftPart = NitroVoidhash::HybridStorekitProductSubscriptionSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -294,7 +296,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridStorekitProductSubscriptionSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitProductSubscriptionPeriodSpec_cxx swiftPart = NitroVoidhash::HybridStorekitProductSubscriptionPeriodSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -310,7 +312,7 @@ namespace margelo::nitro::voidhash::bridge::swift { NitroVoidhash::HybridStorekitProductSubscriptionPeriodSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } - + // pragma MARK: std::shared_ptr std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_(void* _Nonnull swiftUnsafePointer) { NitroVoidhash::HybridStorekitProductOfferSpec_cxx swiftPart = NitroVoidhash::HybridStorekitProductOfferSpec_cxx::fromUnsafe(swiftUnsafePointer); @@ -327,4 +329,108 @@ namespace margelo::nitro::voidhash::bridge::swift { return swiftPart.toUnsafe(); } + // pragma MARK: std::function + Func_void_MeasurementStateBridge create_Func_void_MeasurementStateBridge(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_MeasurementStateBridge::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const MeasurementStateBridge& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_MeasurementCommandResult create_Func_void_MeasurementCommandResult(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_MeasurementCommandResult::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const MeasurementCommandResult& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_MeasurementFlushBridgeResult create_Func_void_MeasurementFlushBridgeResult(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_MeasurementFlushBridgeResult::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const MeasurementFlushBridgeResult& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_MeasurementBridgeEvent create_Func_void_MeasurementBridgeEvent(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_MeasurementBridgeEvent::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const MeasurementBridgeEvent& event) mutable -> void { + swiftClosure.call(event); + }; + } + + // pragma MARK: std::function& /* result */)> + Func_void_std__vector_MeasurementInboxEntry_ create_Func_void_std__vector_MeasurementInboxEntry_(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_std__vector_MeasurementInboxEntry_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::vector& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function& /* result */)> + Func_void_std__shared_ptr_ArrayBuffer_ create_Func_void_std__shared_ptr_ArrayBuffer_(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_std__shared_ptr_ArrayBuffer_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::shared_ptr& result) mutable -> void { + swiftClosure.call(ArrayBufferHolder(result)); + }; + } + + // pragma MARK: std::function + Func_void_MeasurementConfigurationStateBridge create_Func_void_MeasurementConfigurationStateBridge(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_MeasurementConfigurationStateBridge::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const MeasurementConfigurationStateBridge& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(void* _Nonnull swiftUnsafePointer) { + NitroVoidhash::HybridMeasurementSpec_cxx swiftPart = NitroVoidhash::HybridMeasurementSpec_cxx::fromUnsafe(swiftUnsafePointer); + return std::make_shared(swiftPart); + } + void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridMeasurementSpec\" is not implemented in Swift!"); + } + #endif + NitroVoidhash::HybridMeasurementSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); + return swiftPart.toUnsafe(); + } + + // pragma MARK: std::function + Func_void_NativePushToken create_Func_void_NativePushToken(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_NativePushToken::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const NativePushToken& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_NativeNotificationEvent create_Func_void_NativeNotificationEvent(void* _Nonnull swiftClosureWrapper) { + auto swiftClosure = NitroVoidhash::Func_void_NativeNotificationEvent::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const NativeNotificationEvent& event) mutable -> void { + swiftClosure.call(event); + }; + } + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(void* _Nonnull swiftUnsafePointer) { + NitroVoidhash::HybridNotificationsSpec_cxx swiftPart = NitroVoidhash::HybridNotificationsSpec_cxx::fromUnsafe(swiftUnsafePointer); + return std::make_shared(swiftPart); + } + void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridNotificationsSpec\" is not implemented in Swift!"); + } + #endif + NitroVoidhash::HybridNotificationsSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); + return swiftPart.toUnsafe(); + } + } // namespace margelo::nitro::voidhash::bridge::swift diff --git a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.hpp b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.hpp index 963a98c85..29956aab4 100644 --- a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.hpp +++ b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Bridge.hpp @@ -8,6 +8,14 @@ #pragma once // Forward declarations of C++ defined types +// Forward declaration of `ArrayBufferHolder` to properly resolve imports. +namespace NitroModules { class ArrayBufferHolder; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `HybridMeasurementSpec` to properly resolve imports. +namespace margelo::nitro::voidhash { class HybridMeasurementSpec; } +// Forward declaration of `HybridNotificationsSpec` to properly resolve imports. +namespace margelo::nitro::voidhash { class HybridNotificationsSpec; } // Forward declaration of `HybridPaywallPresenterSpec` to properly resolve imports. namespace margelo::nitro::voidhash { class HybridPaywallPresenterSpec; } // Forward declaration of `HybridPaywallWebViewSpec` to properly resolve imports. @@ -28,6 +36,38 @@ namespace margelo::nitro::voidhash { class HybridStorekitSpec; } namespace margelo::nitro::voidhash { class HybridStorekitTransactionSpec; } // Forward declaration of `HybridVoidhashSpec` to properly resolve imports. namespace margelo::nitro::voidhash { class HybridVoidhashSpec; } +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } +// Forward declaration of `MeasurementBridgeEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeEvent; } +// Forward declaration of `MeasurementBridgeSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementBridgeSource; } +// Forward declaration of `MeasurementCommandResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommandResult; } +// Forward declaration of `MeasurementConfigurationStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConfigurationStateBridge; } +// Forward declaration of `MeasurementConsentSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConsentSnapshot; } +// Forward declaration of `MeasurementFlushBridgeResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementFlushBridgeResult; } +// Forward declaration of `MeasurementIdentitySnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementIdentitySnapshot; } +// Forward declaration of `MeasurementInboxEntry` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInboxEntry; } +// Forward declaration of `MeasurementSessionSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementSessionSnapshot; } +// Forward declaration of `MeasurementStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementStateBridge; } +// Forward declaration of `NativeNotificationEventKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativeNotificationEventKind; } +// Forward declaration of `NativeNotificationEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativeNotificationEvent; } +// Forward declaration of `NativePushEnvironment` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushEnvironment; } +// Forward declaration of `NativePushProvider` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushProvider; } +// Forward declaration of `NativePushToken` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativePushToken; } // Forward declaration of `PaywallWebViewBaseEvent` to properly resolve imports. namespace margelo::nitro::voidhash { struct PaywallWebViewBaseEvent; } // Forward declaration of `PaywallWebViewDataDetectorType` to properly resolve imports. @@ -60,6 +100,10 @@ namespace margelo::nitro::voidhash { struct PaywallWebViewSource; } namespace margelo::nitro::voidhash { struct StorekitProductPurchaseOffer; } // Forward declarations of Swift defined types +// Forward declaration of `HybridMeasurementSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridMeasurementSpec_cxx; } +// Forward declaration of `HybridNotificationsSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridNotificationsSpec_cxx; } // Forward declaration of `HybridPaywallPresenterSpec_cxx` to properly resolve imports. namespace NitroVoidhash { class HybridPaywallPresenterSpec_cxx; } // Forward declaration of `HybridPaywallWebViewSpec_cxx` to properly resolve imports. @@ -82,6 +126,8 @@ namespace NitroVoidhash { class HybridStorekitTransactionSpec_cxx; } namespace NitroVoidhash { class HybridVoidhashSpec_cxx; } // Include C++ defined types +#include "HybridMeasurementSpec.hpp" +#include "HybridNotificationsSpec.hpp" #include "HybridPaywallPresenterSpec.hpp" #include "HybridPaywallWebViewSpec.hpp" #include "HybridPurchasedItemSpec.hpp" @@ -92,6 +138,22 @@ namespace NitroVoidhash { class HybridVoidhashSpec_cxx; } #include "HybridStorekitSpec.hpp" #include "HybridStorekitTransactionSpec.hpp" #include "HybridVoidhashSpec.hpp" +#include "MeasurementBridgeError.hpp" +#include "MeasurementBridgeEvent.hpp" +#include "MeasurementBridgeSource.hpp" +#include "MeasurementCommandResult.hpp" +#include "MeasurementConfigurationStateBridge.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "MeasurementFlushBridgeResult.hpp" +#include "MeasurementIdentitySnapshot.hpp" +#include "MeasurementInboxEntry.hpp" +#include "MeasurementSessionSnapshot.hpp" +#include "MeasurementStateBridge.hpp" +#include "NativeNotificationEvent.hpp" +#include "NativeNotificationEventKind.hpp" +#include "NativePushEnvironment.hpp" +#include "NativePushProvider.hpp" +#include "NativePushToken.hpp" #include "PaywallWebViewBaseEvent.hpp" #include "PaywallWebViewDataDetectorType.hpp" #include "PaywallWebViewErrorEvent.hpp" @@ -107,6 +169,8 @@ namespace NitroVoidhash { class HybridVoidhashSpec_cxx; } #include "PaywallWebViewShouldStartLoadRequest.hpp" #include "PaywallWebViewSource.hpp" #include "StorekitProductPurchaseOffer.hpp" +#include +#include #include #include #include @@ -134,7 +198,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder wrap_std__shared_ptr_Promise_bool__(std::shared_ptr> promise) { return PromiseHolder(std::move(promise)); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -156,7 +220,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_bool_Wrapper wrap_Func_void_bool(Func_void_bool value) { return Func_void_bool_Wrapper(std::move(value)); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -178,7 +242,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__exception_ptr_Wrapper wrap_Func_void_std__exception_ptr(Func_void_std__exception_ptr value) { return Func_void_std__exception_ptr_Wrapper(std::move(value)); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -200,7 +264,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__string_Wrapper wrap_Func_void_std__string(Func_void_std__string value) { return Func_void_std__string_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -209,7 +273,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_std__string_____rawEvent______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -231,7 +295,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_Wrapper wrap_Func_void(Func_void value) { return Func_void_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -240,7 +304,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void____(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::shared_ptr> /** * Specialized version of `std::shared_ptr>`. @@ -252,7 +316,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder wrap_std__shared_ptr_Promise_void__(std::shared_ptr> promise) { return PromiseHolder(std::move(promise)); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -260,11 +324,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallPresenterSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: Result>> using Result_std__shared_ptr_Promise_bool___ = Result>>; inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::shared_ptr>& value) { @@ -273,7 +337,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::exception_ptr& error) { return Result>>::withError(error); } - + // pragma MARK: Result>> using Result_std__shared_ptr_Promise_void___ = Result>>; inline Result_std__shared_ptr_Promise_void___ create_Result_std__shared_ptr_Promise_void___(const std::shared_ptr>& value) { @@ -282,7 +346,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_void___ create_Result_std__shared_ptr_Promise_void___(const std::exception_ptr& error) { return Result>>::withError(error); } - + // pragma MARK: Result using Result_void_ = Result; inline Result_void_ create_Result_void_() { @@ -291,7 +355,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_void_ create_Result_void_(const std::exception_ptr& error) { return Result::withError(error); } - + // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -300,7 +364,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional create_std__optional_std__string_(const std::string& value) { return std::optional(value); } - + // pragma MARK: std::vector /** * Specialized version of `std::vector`. @@ -311,7 +375,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -320,7 +384,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__vector_PaywallWebViewHeader__(const std::vector& value) { return std::optional>(value); } - + // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -329,7 +393,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional create_std__optional_PaywallWebViewSource_(const PaywallWebViewSource& value) { return std::optional(value); } - + // pragma MARK: std::vector /** * Specialized version of `std::vector`. @@ -340,7 +404,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::vector /** * Specialized version of `std::vector`. @@ -351,7 +415,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -373,7 +437,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewNavigationEvent_Wrapper wrap_Func_void_PaywallWebViewNavigationEvent(Func_void_PaywallWebViewNavigationEvent value) { return Func_void_PaywallWebViewNavigationEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -382,7 +446,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewNavigationEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -404,7 +468,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewProgressEvent_Wrapper wrap_Func_void_PaywallWebViewProgressEvent(Func_void_PaywallWebViewProgressEvent value) { return Func_void_PaywallWebViewProgressEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -413,7 +477,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewProgressEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -435,7 +499,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewErrorEvent_Wrapper wrap_Func_void_PaywallWebViewErrorEvent(Func_void_PaywallWebViewErrorEvent value) { return Func_void_PaywallWebViewErrorEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -444,7 +508,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewErrorEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -466,7 +530,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewHttpErrorEvent_Wrapper wrap_Func_void_PaywallWebViewHttpErrorEvent(Func_void_PaywallWebViewHttpErrorEvent value) { return Func_void_PaywallWebViewHttpErrorEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -475,7 +539,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewHttpErrorEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -497,7 +561,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewMessageEvent_Wrapper wrap_Func_void_PaywallWebViewMessageEvent(Func_void_PaywallWebViewMessageEvent value) { return Func_void_PaywallWebViewMessageEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -506,7 +570,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewMessageEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -528,7 +592,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewOpenWindowEvent_Wrapper wrap_Func_void_PaywallWebViewOpenWindowEvent(Func_void_PaywallWebViewOpenWindowEvent value) { return Func_void_PaywallWebViewOpenWindowEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -537,7 +601,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewOpenWindowEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -559,7 +623,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewFileDownloadEvent_Wrapper wrap_Func_void_PaywallWebViewFileDownloadEvent(Func_void_PaywallWebViewFileDownloadEvent value) { return Func_void_PaywallWebViewFileDownloadEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -568,7 +632,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewFileDownloadEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -590,7 +654,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewRenderProcessGoneEvent_Wrapper wrap_Func_void_PaywallWebViewRenderProcessGoneEvent(Func_void_PaywallWebViewRenderProcessGoneEvent value) { return Func_void_PaywallWebViewRenderProcessGoneEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -599,7 +663,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewRenderProcessGoneEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -621,7 +685,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_PaywallWebViewBaseEvent_Wrapper wrap_Func_void_PaywallWebViewBaseEvent(Func_void_PaywallWebViewBaseEvent value) { return Func_void_PaywallWebViewBaseEvent_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -630,7 +694,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_void_const_PaywallWebViewBaseEvent_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -653,7 +717,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_bool_PaywallWebViewShouldStartLoadRequest_Wrapper wrap_Func_bool_PaywallWebViewShouldStartLoadRequest(Func_bool_PaywallWebViewShouldStartLoadRequest value) { return Func_bool_PaywallWebViewShouldStartLoadRequest_Wrapper(std::move(value)); } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -662,7 +726,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__function_bool_const_PaywallWebViewShouldStartLoadRequest_____event______(const std::function& value) { return std::optional>(value); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -670,11 +734,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridPaywallWebViewSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -682,11 +746,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::shared_ptr>> /** * Specialized version of `std::shared_ptr>>`. @@ -698,7 +762,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder> wrap_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec___(std::shared_ptr>> promise) { return PromiseHolder>(std::move(promise)); } - + // pragma MARK: std::function& /* result */)> /** * Specialized version of `std::function&)>`. @@ -720,7 +784,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec__Wrapper wrap_Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_(Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec_ value) { return Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec__Wrapper(std::move(value)); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -728,11 +792,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridVoidhashSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: Result>>> using Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec____ = Result>>>; inline Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec____ create_Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec____(const std::shared_ptr>>& value) { @@ -741,7 +805,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec____ create_Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridPurchasedItemSpec____(const std::exception_ptr& error) { return Result>>>::withError(error); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -749,11 +813,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::function& /* transaction */)> /** * Specialized version of `std::function&)>`. @@ -775,7 +839,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__Wrapper wrap_Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_(Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_ value) { return Func_void_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__Wrapper(std::move(value)); } - + // pragma MARK: std::optional& /* transaction */)>> /** * Specialized version of `std::optional& / * transaction * /)>>`. @@ -784,7 +848,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional& /* transaction */)>> create_std__optional_std__function_void_const_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec______transaction______(const std::function& /* transaction */)>& value) { return std::optional& /* transaction */)>>(value); } - + // pragma MARK: std::vector> /** * Specialized version of `std::vector>`. @@ -795,7 +859,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::shared_ptr>>> /** * Specialized version of `std::shared_ptr>>>`. @@ -807,7 +871,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder>> wrap_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____(std::shared_ptr>>> promise) { return PromiseHolder>>(std::move(promise)); } - + // pragma MARK: std::function>& /* result */)> /** * Specialized version of `std::function>&)>`. @@ -829,7 +893,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___Wrapper wrap_Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__(Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec__ value) { return Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___Wrapper(std::move(value)); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -837,11 +901,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::vector> /** * Specialized version of `std::vector>`. @@ -852,7 +916,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::shared_ptr>>> /** * Specialized version of `std::shared_ptr>>>`. @@ -864,7 +928,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder>> wrap_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec____(std::shared_ptr>>> promise) { return PromiseHolder>>(std::move(promise)); } - + // pragma MARK: std::function>& /* result */)> /** * Specialized version of `std::function>&)>`. @@ -886,7 +950,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec___Wrapper wrap_Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec__(Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec__ value) { return Func_void_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec___Wrapper(std::move(value)); } - + // pragma MARK: std::shared_ptr>> /** * Specialized version of `std::shared_ptr>>`. @@ -898,7 +962,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline PromiseHolder> wrap_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___(std::shared_ptr>> promise) { return PromiseHolder>(std::move(promise)); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -906,11 +970,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: Result>>>> using Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____ = Result>>>>; inline Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____ create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____(const std::shared_ptr>>>& value) { @@ -919,7 +983,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____ create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____(const std::exception_ptr& error) { return Result>>>>::withError(error); } - + // pragma MARK: Result>>>> using Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____ = Result>>>>; inline Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____ create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____(const std::shared_ptr>>>& value) { @@ -928,7 +992,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____ create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____(const std::exception_ptr& error) { return Result>>>>::withError(error); } - + // pragma MARK: Result>>> using Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____ = Result>>>; inline Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____ create_Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____(const std::shared_ptr>>& value) { @@ -937,7 +1001,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____ create_Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____(const std::exception_ptr& error) { return Result>>>::withError(error); } - + // pragma MARK: Result>> using Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___ = Result>>; inline Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___ create_Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___(const std::vector>& value) { @@ -946,7 +1010,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___ create_Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___(const std::exception_ptr& error) { return Result>>::withError(error); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -954,11 +1018,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -967,7 +1031,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec__(const std::shared_ptr& value) { return std::optional>(value); } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -975,11 +1039,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. @@ -987,11 +1051,11 @@ namespace margelo::nitro::voidhash::bridge::swift { using std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_ = std::shared_ptr; std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_(void* _Nonnull swiftUnsafePointer); void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_ cppType); - + // pragma MARK: std::weak_ptr using std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_ = std::weak_ptr; inline std__weak_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec_(const std::shared_ptr& strong) { return strong; } - + // pragma MARK: std::optional> /** * Specialized version of `std::optional>`. @@ -1000,7 +1064,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional> create_std__optional_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec__(const std::shared_ptr& value) { return std::optional>(value); } - + // pragma MARK: std::vector> /** * Specialized version of `std::vector>`. @@ -1011,7 +1075,7 @@ namespace margelo::nitro::voidhash::bridge::swift { vector.reserve(size); return vector; } - + // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -1020,7 +1084,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional create_std__optional_double_(const double& value) { return std::optional(value); } - + // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -1029,7 +1093,7 @@ namespace margelo::nitro::voidhash::bridge::swift { inline std::optional create_std__optional_bool_(const bool& value) { return std::optional(value); } - + // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -1039,4 +1103,450 @@ namespace margelo::nitro::voidhash::bridge::swift { return std::optional(value); } + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_MeasurementStateBridge__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_MeasurementStateBridge__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_MeasurementStateBridge__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_MeasurementStateBridge = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_MeasurementStateBridge_Wrapper final { + public: + explicit Func_void_MeasurementStateBridge_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(MeasurementStateBridge result) const { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_MeasurementStateBridge create_Func_void_MeasurementStateBridge(void* _Nonnull swiftClosureWrapper); + inline Func_void_MeasurementStateBridge_Wrapper wrap_Func_void_MeasurementStateBridge(Func_void_MeasurementStateBridge value) { + return Func_void_MeasurementStateBridge_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_MeasurementBridgeError_ = std::optional; + inline std::optional create_std__optional_MeasurementBridgeError_(const MeasurementBridgeError& value) { + return std::optional(value); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_MeasurementCommandResult__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_MeasurementCommandResult__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_MeasurementCommandResult__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_MeasurementCommandResult = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_MeasurementCommandResult_Wrapper final { + public: + explicit Func_void_MeasurementCommandResult_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(MeasurementCommandResult result) const { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_MeasurementCommandResult create_Func_void_MeasurementCommandResult(void* _Nonnull swiftClosureWrapper); + inline Func_void_MeasurementCommandResult_Wrapper wrap_Func_void_MeasurementCommandResult(Func_void_MeasurementCommandResult value) { + return Func_void_MeasurementCommandResult_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_MeasurementIdentitySnapshot_ = std::optional; + inline std::optional create_std__optional_MeasurementIdentitySnapshot_(const MeasurementIdentitySnapshot& value) { + return std::optional(value); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_MeasurementConsentSnapshot_ = std::optional; + inline std::optional create_std__optional_MeasurementConsentSnapshot_(const MeasurementConsentSnapshot& value) { + return std::optional(value); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_MeasurementSessionSnapshot_ = std::optional; + inline std::optional create_std__optional_MeasurementSessionSnapshot_(const MeasurementSessionSnapshot& value) { + return std::optional(value); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_MeasurementFlushBridgeResult__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_MeasurementFlushBridgeResult__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_MeasurementFlushBridgeResult__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_MeasurementFlushBridgeResult = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_MeasurementFlushBridgeResult_Wrapper final { + public: + explicit Func_void_MeasurementFlushBridgeResult_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(MeasurementFlushBridgeResult result) const { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_MeasurementFlushBridgeResult create_Func_void_MeasurementFlushBridgeResult(void* _Nonnull swiftClosureWrapper); + inline Func_void_MeasurementFlushBridgeResult_Wrapper wrap_Func_void_MeasurementFlushBridgeResult(Func_void_MeasurementFlushBridgeResult value) { + return Func_void_MeasurementFlushBridgeResult_Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_std__string__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_std__string__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_std__string__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__shared_ptr_ArrayBuffer__ = std::optional>; + inline std::optional> create_std__optional_std__shared_ptr_ArrayBuffer__(const std::shared_ptr& value) { + return std::optional>(value); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_MeasurementBridgeEvent = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_MeasurementBridgeEvent_Wrapper final { + public: + explicit Func_void_MeasurementBridgeEvent_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(MeasurementBridgeEvent event) const { + _function->operator()(event); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_MeasurementBridgeEvent create_Func_void_MeasurementBridgeEvent(void* _Nonnull swiftClosureWrapper); + inline Func_void_MeasurementBridgeEvent_Wrapper wrap_Func_void_MeasurementBridgeEvent(Func_void_MeasurementBridgeEvent value) { + return Func_void_MeasurementBridgeEvent_Wrapper(std::move(value)); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_MeasurementInboxEntry_ = std::vector; + inline std::vector create_std__vector_MeasurementInboxEntry_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___() { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___(std::shared_ptr>> promise) { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__vector_MeasurementInboxEntry_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__vector_MeasurementInboxEntry__Wrapper final { + public: + explicit Func_void_std__vector_MeasurementInboxEntry__Wrapper(std::function& /* result */)>&& func): _function(std::make_unique& /* result */)>>(std::move(func))) {} + inline void call(std::vector result) const { + _function->operator()(result); + } + private: + std::unique_ptr& /* result */)>> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__vector_MeasurementInboxEntry_ create_Func_void_std__vector_MeasurementInboxEntry_(void* _Nonnull swiftClosureWrapper); + inline Func_void_std__vector_MeasurementInboxEntry__Wrapper wrap_Func_void_std__vector_MeasurementInboxEntry_(Func_void_std__vector_MeasurementInboxEntry_ value) { + return Func_void_std__vector_MeasurementInboxEntry__Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___() { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___(std::shared_ptr>> promise) { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__shared_ptr_ArrayBuffer_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__shared_ptr_ArrayBuffer__Wrapper final { + public: + explicit Func_void_std__shared_ptr_ArrayBuffer__Wrapper(std::function& /* result */)>&& func): _function(std::make_unique& /* result */)>>(std::move(func))) {} + inline void call(ArrayBufferHolder result) const { + _function->operator()(result.getArrayBuffer()); + } + private: + std::unique_ptr& /* result */)>> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__shared_ptr_ArrayBuffer_ create_Func_void_std__shared_ptr_ArrayBuffer_(void* _Nonnull swiftClosureWrapper); + inline Func_void_std__shared_ptr_ArrayBuffer__Wrapper wrap_Func_void_std__shared_ptr_ArrayBuffer_(Func_void_std__shared_ptr_ArrayBuffer_ value) { + return Func_void_std__shared_ptr_ArrayBuffer__Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_MeasurementConfigurationStateBridge__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_MeasurementConfigurationStateBridge = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_MeasurementConfigurationStateBridge_Wrapper final { + public: + explicit Func_void_MeasurementConfigurationStateBridge_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(MeasurementConfigurationStateBridge result) const { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_MeasurementConfigurationStateBridge create_Func_void_MeasurementConfigurationStateBridge(void* _Nonnull swiftClosureWrapper); + inline Func_void_MeasurementConfigurationStateBridge_Wrapper wrap_Func_void_MeasurementConfigurationStateBridge(Func_void_MeasurementConfigurationStateBridge value) { + return Func_void_MeasurementConfigurationStateBridge_Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(void* _Nonnull swiftUnsafePointer); + void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ cppType); + + // pragma MARK: std::weak_ptr + using std__weak_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ = std::weak_ptr; + inline std__weak_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(const std::shared_ptr& strong) { return strong; } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_MeasurementStateBridge___ = Result>>; + inline Result_std__shared_ptr_Promise_MeasurementStateBridge___ create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_MeasurementStateBridge___ create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_MeasurementCommandResult___ = Result>>; + inline Result_std__shared_ptr_Promise_MeasurementCommandResult___ create_Result_std__shared_ptr_Promise_MeasurementCommandResult___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_MeasurementCommandResult___ create_Result_std__shared_ptr_Promise_MeasurementCommandResult___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___ = Result>>; + inline Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___ create_Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___ create_Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_std__string___ = Result>>; + inline Result_std__shared_ptr_Promise_std__string___ create_Result_std__shared_ptr_Promise_std__string___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__string___ create_Result_std__shared_ptr_Promise_std__string___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____ create_Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____(const std::shared_ptr>>& value) { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____ create_Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____(const std::exception_ptr& error) { + return Result>>>::withError(error); + } + + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____ create_Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____(const std::shared_ptr>>& value) { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____ create_Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____(const std::exception_ptr& error) { + return Result>>>::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___ = Result>>; + inline Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___ create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___ create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_NativePushToken__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_NativePushToken__() { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_NativePushToken__(std::shared_ptr> promise) { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_NativePushToken = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_NativePushToken_Wrapper final { + public: + explicit Func_void_NativePushToken_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(NativePushToken result) const { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_NativePushToken create_Func_void_NativePushToken(void* _Nonnull swiftClosureWrapper); + inline Func_void_NativePushToken_Wrapper wrap_Func_void_NativePushToken(Func_void_NativePushToken value) { + return Func_void_NativePushToken_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_NativeNotificationEvent = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_NativeNotificationEvent_Wrapper final { + public: + explicit Func_void_NativeNotificationEvent_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(NativeNotificationEvent event) const { + _function->operator()(event); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_NativeNotificationEvent create_Func_void_NativeNotificationEvent(void* _Nonnull swiftClosureWrapper); + inline Func_void_NativeNotificationEvent_Wrapper wrap_Func_void_NativeNotificationEvent(Func_void_NativeNotificationEvent value) { + return Func_void_NativeNotificationEvent_Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(void* _Nonnull swiftUnsafePointer); + void* _Nonnull get_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ cppType); + + // pragma MARK: std::weak_ptr + using std__weak_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ = std::weak_ptr; + inline std__weak_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ weakify_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(const std::shared_ptr& strong) { return strong; } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_NativePushToken___ = Result>>; + inline Result_std__shared_ptr_Promise_NativePushToken___ create_Result_std__shared_ptr_Promise_NativePushToken___(const std::shared_ptr>& value) { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_NativePushToken___ create_Result_std__shared_ptr_Promise_NativePushToken___(const std::exception_ptr& error) { + return Result>>::withError(error); + } + } // namespace margelo::nitro::voidhash::bridge::swift diff --git a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Umbrella.hpp b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Umbrella.hpp index ce8725eba..ab7624f87 100644 --- a/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Umbrella.hpp +++ b/libraries/react-native/nitrogen/generated/ios/NitroVoidhash-Swift-Cxx-Umbrella.hpp @@ -8,6 +8,12 @@ #pragma once // Forward declarations of C++ defined types +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `HybridMeasurementSpec` to properly resolve imports. +namespace margelo::nitro::voidhash { class HybridMeasurementSpec; } +// Forward declaration of `HybridNotificationsSpec` to properly resolve imports. +namespace margelo::nitro::voidhash { class HybridNotificationsSpec; } // Forward declaration of `HybridPaywallPresenterSpec` to properly resolve imports. namespace margelo::nitro::voidhash { class HybridPaywallPresenterSpec; } // Forward declaration of `HybridPaywallWebViewSpec` to properly resolve imports. @@ -28,6 +34,54 @@ namespace margelo::nitro::voidhash { class HybridStorekitSpec; } namespace margelo::nitro::voidhash { class HybridStorekitTransactionSpec; } // Forward declaration of `HybridVoidhashSpec` to properly resolve imports. namespace margelo::nitro::voidhash { class HybridVoidhashSpec; } +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } +// Forward declaration of `MeasurementBridgeEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeEvent; } +// Forward declaration of `MeasurementBridgeSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementBridgeSource; } +// Forward declaration of `MeasurementCommandKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementCommandKind; } +// Forward declaration of `MeasurementCommandResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommandResult; } +// Forward declaration of `MeasurementCommand` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommand; } +// Forward declaration of `MeasurementConfigurationStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConfigurationStateBridge; } +// Forward declaration of `MeasurementConsentSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConsentSnapshot; } +// Forward declaration of `MeasurementFlushBridgeResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementFlushBridgeResult; } +// Forward declaration of `MeasurementIdentitySnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementIdentitySnapshot; } +// Forward declaration of `MeasurementInboxEntry` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInboxEntry; } +// Forward declaration of `MeasurementInitializeConfiguration` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInitializeConfiguration; } +// Forward declaration of `MeasurementProtectedEvidenceInput` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementProtectedEvidenceInput; } +// Forward declaration of `MeasurementProtectedPurpose` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedPurpose; } +// Forward declaration of `MeasurementProtectedRetention` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedRetention; } +// Forward declaration of `MeasurementRecordPriority` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordPriority; } +// Forward declaration of `MeasurementRecordSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordSource; } +// Forward declaration of `MeasurementSessionSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementSessionSnapshot; } +// Forward declaration of `MeasurementStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementStateBridge; } +// Forward declaration of `NativeNotificationEventKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativeNotificationEventKind; } +// Forward declaration of `NativeNotificationEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativeNotificationEvent; } +// Forward declaration of `NativePushEnvironment` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushEnvironment; } +// Forward declaration of `NativePushProvider` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushProvider; } +// Forward declaration of `NativePushToken` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativePushToken; } // Forward declaration of `PaywallWebViewAndroidLayerType` to properly resolve imports. namespace margelo::nitro::voidhash { enum class PaywallWebViewAndroidLayerType; } // Forward declaration of `PaywallWebViewBaseEvent` to properly resolve imports. @@ -72,6 +126,8 @@ namespace margelo::nitro::voidhash { struct StorekitProductPurchaseOffer; } namespace margelo::nitro::voidhash { enum class StorekitProductSubscriptionPeriodUnit; } // Include C++ defined types +#include "HybridMeasurementSpec.hpp" +#include "HybridNotificationsSpec.hpp" #include "HybridPaywallPresenterSpec.hpp" #include "HybridPaywallWebViewSpec.hpp" #include "HybridPurchasedItemSpec.hpp" @@ -82,6 +138,30 @@ namespace margelo::nitro::voidhash { enum class StorekitProductSubscriptionPerio #include "HybridStorekitSpec.hpp" #include "HybridStorekitTransactionSpec.hpp" #include "HybridVoidhashSpec.hpp" +#include "MeasurementBridgeError.hpp" +#include "MeasurementBridgeEvent.hpp" +#include "MeasurementBridgeSource.hpp" +#include "MeasurementCommand.hpp" +#include "MeasurementCommandKind.hpp" +#include "MeasurementCommandResult.hpp" +#include "MeasurementConfigurationStateBridge.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "MeasurementFlushBridgeResult.hpp" +#include "MeasurementIdentitySnapshot.hpp" +#include "MeasurementInboxEntry.hpp" +#include "MeasurementInitializeConfiguration.hpp" +#include "MeasurementProtectedEvidenceInput.hpp" +#include "MeasurementProtectedPurpose.hpp" +#include "MeasurementProtectedRetention.hpp" +#include "MeasurementRecordPriority.hpp" +#include "MeasurementRecordSource.hpp" +#include "MeasurementSessionSnapshot.hpp" +#include "MeasurementStateBridge.hpp" +#include "NativeNotificationEvent.hpp" +#include "NativeNotificationEventKind.hpp" +#include "NativePushEnvironment.hpp" +#include "NativePushProvider.hpp" +#include "NativePushToken.hpp" #include "PaywallWebViewAndroidLayerType.hpp" #include "PaywallWebViewBaseEvent.hpp" #include "PaywallWebViewCacheMode.hpp" @@ -103,6 +183,7 @@ namespace margelo::nitro::voidhash { enum class StorekitProductSubscriptionPerio #include "PurchasedItemType.hpp" #include "StorekitProductPurchaseOffer.hpp" #include "StorekitProductSubscriptionPeriodUnit.hpp" +#include #include #include #include @@ -122,6 +203,10 @@ namespace margelo::nitro::voidhash { enum class StorekitProductSubscriptionPerio #include // Forward declarations of Swift defined types +// Forward declaration of `HybridMeasurementSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridMeasurementSpec_cxx; } +// Forward declaration of `HybridNotificationsSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridNotificationsSpec_cxx; } // Forward declaration of `HybridPaywallPresenterSpec_cxx` to properly resolve imports. namespace NitroVoidhash { class HybridPaywallPresenterSpec_cxx; } // Forward declaration of `HybridPaywallWebViewSpec_cxx` to properly resolve imports. diff --git a/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.mm b/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.mm index de371af35..444a25427 100644 --- a/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.mm +++ b/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.mm @@ -14,6 +14,8 @@ #include "HybridStorekitSpecSwift.hpp" #include "HybridPaywallWebViewSpecSwift.hpp" #include "HybridPaywallPresenterSpecSwift.hpp" +#include "HybridMeasurementSpecSwift.hpp" +#include "HybridNotificationsSpecSwift.hpp" @interface NitroVoidhashAutolinking : NSObject @end @@ -52,6 +54,20 @@ + (void) load { return hybridObject; } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Measurement", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = NitroVoidhash::NitroVoidhashAutolinking::createMeasurement(); + return hybridObject; + } + ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Notifications", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = NitroVoidhash::NitroVoidhashAutolinking::createNotifications(); + return hybridObject; + } + ); } @end diff --git a/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.swift b/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.swift index 59809e17b..156aef8de 100644 --- a/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.swift +++ b/libraries/react-native/nitrogen/generated/ios/NitroVoidhashAutolinking.swift @@ -22,7 +22,7 @@ public final class NitroVoidhashAutolinking { return __cxxWrapped.getCxxPart() }() } - + /** * Creates an instance of a Swift class that implements `HybridStorekitSpec`, * and wraps it in a Swift class that can directly interop with C++ (`HybridStorekitSpec_cxx`) @@ -37,7 +37,7 @@ public final class NitroVoidhashAutolinking { return __cxxWrapped.getCxxPart() }() } - + /** * Creates an instance of a Swift class that implements `HybridPaywallWebViewSpec`, * and wraps it in a Swift class that can directly interop with C++ (`HybridPaywallWebViewSpec_cxx`) @@ -52,7 +52,7 @@ public final class NitroVoidhashAutolinking { return __cxxWrapped.getCxxPart() }() } - + /** * Creates an instance of a Swift class that implements `HybridPaywallPresenterSpec`, * and wraps it in a Swift class that can directly interop with C++ (`HybridPaywallPresenterSpec_cxx`) @@ -67,4 +67,34 @@ public final class NitroVoidhashAutolinking { return __cxxWrapped.getCxxPart() }() } + + /** + * Creates an instance of a Swift class that implements `HybridMeasurementSpec`, + * and wraps it in a Swift class that can directly interop with C++ (`HybridMeasurementSpec_cxx`) + * + * This is generated by Nitrogen and will initialize the class specified + * in the `"autolinking"` property of `nitro.json` (in this case, `HybridMeasurement`). + */ + public static func createMeasurement() -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ { + let hybridObject = HybridMeasurement() + return { () -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ in + let __cxxWrapped = hybridObject.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }() + } + + /** + * Creates an instance of a Swift class that implements `HybridNotificationsSpec`, + * and wraps it in a Swift class that can directly interop with C++ (`HybridNotificationsSpec_cxx`) + * + * This is generated by Nitrogen and will initialize the class specified + * in the `"autolinking"` property of `nitro.json` (in this case, `HybridNotifications`). + */ + public static func createNotifications() -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ { + let hybridObject = HybridNotifications() + return { () -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ in + let __cxxWrapped = hybridObject.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }() + } } diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.cpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.cpp new file mode 100644 index 000000000..1a3b24d93 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridMeasurementSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "HybridMeasurementSpecSwift.hpp" + +namespace margelo::nitro::voidhash { +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.hpp new file mode 100644 index 000000000..a3cdcfe5a --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridMeasurementSpecSwift.hpp @@ -0,0 +1,317 @@ +/// +/// HybridMeasurementSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridMeasurementSpec.hpp" + +// Forward declaration of `HybridMeasurementSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridMeasurementSpec_cxx; } + +// Forward declaration of `MeasurementStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementStateBridge; } +// Forward declaration of `MeasurementInitializeConfiguration` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInitializeConfiguration; } +// Forward declaration of `MeasurementCommandResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommandResult; } +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } +// Forward declaration of `MeasurementBridgeSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementBridgeSource; } +// Forward declaration of `MeasurementCommand` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommand; } +// Forward declaration of `MeasurementCommandKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementCommandKind; } +// Forward declaration of `MeasurementRecordSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordSource; } +// Forward declaration of `MeasurementRecordPriority` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordPriority; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `ArrayBufferHolder` to properly resolve imports. +namespace NitroModules { class ArrayBufferHolder; } +// Forward declaration of `MeasurementIdentitySnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementIdentitySnapshot; } +// Forward declaration of `MeasurementConsentSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConsentSnapshot; } +// Forward declaration of `MeasurementSessionSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementSessionSnapshot; } +// Forward declaration of `MeasurementFlushBridgeResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementFlushBridgeResult; } +// Forward declaration of `MeasurementBridgeEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeEvent; } +// Forward declaration of `MeasurementInboxEntry` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInboxEntry; } +// Forward declaration of `MeasurementProtectedEvidenceInput` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementProtectedEvidenceInput; } +// Forward declaration of `MeasurementProtectedPurpose` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedPurpose; } +// Forward declaration of `MeasurementProtectedRetention` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedRetention; } +// Forward declaration of `MeasurementConfigurationStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConfigurationStateBridge; } + +#include +#include "MeasurementStateBridge.hpp" +#include +#include +#include "MeasurementInitializeConfiguration.hpp" +#include +#include "MeasurementCommandResult.hpp" +#include "MeasurementBridgeError.hpp" +#include "MeasurementBridgeSource.hpp" +#include "MeasurementCommand.hpp" +#include "MeasurementCommandKind.hpp" +#include "MeasurementRecordSource.hpp" +#include "MeasurementRecordPriority.hpp" +#include +#include +#include "MeasurementIdentitySnapshot.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "MeasurementSessionSnapshot.hpp" +#include "MeasurementFlushBridgeResult.hpp" +#include +#include "MeasurementBridgeEvent.hpp" +#include "MeasurementInboxEntry.hpp" +#include "MeasurementProtectedEvidenceInput.hpp" +#include "MeasurementProtectedPurpose.hpp" +#include "MeasurementProtectedRetention.hpp" +#include "MeasurementConfigurationStateBridge.hpp" + +#include "NitroVoidhash-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::voidhash { + + /** + * The C++ part of HybridMeasurementSpec_cxx.swift. + * + * HybridMeasurementSpecSwift (C++) accesses HybridMeasurementSpec_cxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridMeasurementSpec_cxx can directly inherit from the C++ class HybridMeasurementSpec + * to simplify the whole structure and memory management. + */ + class HybridMeasurementSpecSwift: public virtual HybridMeasurementSpec { + public: + // Constructor from a Swift instance + explicit HybridMeasurementSpecSwift(const NitroVoidhash::HybridMeasurementSpec_cxx& swiftPart): + HybridObject(HybridMeasurementSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline NitroVoidhash::HybridMeasurementSpec_cxx& getSwiftPart() noexcept { + return _swiftPart; + } + + public: + // Get memory pressure + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr> initialize(const std::string& publishableKey, const MeasurementInitializeConfiguration& configuration) override { + auto __result = _swiftPart.initialize(publishableKey, configuration); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> enqueue(const MeasurementCommand& command) override { + auto __result = _swiftPart.enqueue(command); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> flush() override { + auto __result = _swiftPart.flush(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getInstallationId() override { + auto __result = _swiftPart.getInstallationId(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getState() override { + auto __result = _swiftPart.getState(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void subscribe(const std::string& subscriptionId, const std::function& listener) override { + auto __result = _swiftPart.subscribe(subscriptionId, listener); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline void unsubscribe(const std::string& subscriptionId) override { + auto __result = _swiftPart.unsubscribe(subscriptionId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline std::shared_ptr>> peekInbox(double limit) override { + auto __result = _swiftPart.peekInbox(std::forward(limit)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> acknowledgeInbox(const std::string& entryId) override { + auto __result = _swiftPart.acknowledgeInbox(entryId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr>> readProtectedEvidence(const std::string& blobId) override { + auto __result = _swiftPart.readProtectedEvidence(blobId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> putProtectedEvidence(const MeasurementProtectedEvidenceInput& input) override { + auto __result = _swiftPart.putProtectedEvidence(input); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> deleteProtectedEvidence(const std::string& blobId) override { + auto __result = _swiftPart.deleteProtectedEvidence(blobId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> deleteProtectedData(const std::string& requestId) override { + auto __result = _swiftPart.deleteProtectedData(requestId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getMeasurementConfigurationState() override { + auto __result = _swiftPart.getMeasurementConfigurationState(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> persistMeasurementConfigurationState(double version, const std::shared_ptr& payload) override { + auto __result = _swiftPart.persistMeasurementConfigurationState(std::forward(version), ArrayBufferHolder(payload)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> applyMeasurementConfiguration(double version, const std::shared_ptr& payload) override { + auto __result = _swiftPart.applyMeasurementConfiguration(std::forward(version), ArrayBufferHolder(payload)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> applyMeasurementStorageLimits(double maxOutboxRecords, double maxOutboxBytes, double maxProtectedBytes) override { + auto __result = _swiftPart.applyMeasurementStorageLimits(std::forward(maxOutboxRecords), std::forward(maxOutboxBytes), std::forward(maxProtectedBytes)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getPushRegistrationState() override { + auto __result = _swiftPart.getPushRegistrationState(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> persistPushRegistrationState(const std::shared_ptr& payload) override { + auto __result = _swiftPart.persistPushRegistrationState(ArrayBufferHolder(payload)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> clearPushRegistrationState() override { + auto __result = _swiftPart.clearPushRegistrationState(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getTestDeviceState() override { + auto __result = _swiftPart.getTestDeviceState(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> persistTestDeviceState(bool enabled) override { + auto __result = _swiftPart.persistTestDeviceState(std::forward(enabled)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> hasDedupe(const std::string& namespace, const std::string& key) override { + auto __result = _swiftPart.hasDedupe(namespace, key); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> checkAndSetDedupe(const std::string& namespace, const std::string& key, double expiresAtMs) override { + auto __result = _swiftPart.checkAndSetDedupe(namespace, key, std::forward(expiresAtMs)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + + private: + NitroVoidhash::HybridMeasurementSpec_cxx _swiftPart; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.cpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.cpp new file mode 100644 index 000000000..a7955645d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridNotificationsSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "HybridNotificationsSpecSwift.hpp" + +namespace margelo::nitro::voidhash { +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.hpp new file mode 100644 index 000000000..1da221be4 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridNotificationsSpecSwift.hpp @@ -0,0 +1,124 @@ +/// +/// HybridNotificationsSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridNotificationsSpec.hpp" + +// Forward declaration of `HybridNotificationsSpec_cxx` to properly resolve imports. +namespace NitroVoidhash { class HybridNotificationsSpec_cxx; } + +// Forward declaration of `NativePushToken` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativePushToken; } +// Forward declaration of `NativePushProvider` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushProvider; } +// Forward declaration of `NativePushEnvironment` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushEnvironment; } +// Forward declaration of `NativeNotificationEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativeNotificationEvent; } +// Forward declaration of `NativeNotificationEventKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativeNotificationEventKind; } + +#include +#include +#include "NativePushToken.hpp" +#include "NativePushProvider.hpp" +#include "NativePushEnvironment.hpp" +#include +#include "NativeNotificationEvent.hpp" +#include "NativeNotificationEventKind.hpp" +#include + +#include "NitroVoidhash-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::voidhash { + + /** + * The C++ part of HybridNotificationsSpec_cxx.swift. + * + * HybridNotificationsSpecSwift (C++) accesses HybridNotificationsSpec_cxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridNotificationsSpec_cxx can directly inherit from the C++ class HybridNotificationsSpec + * to simplify the whole structure and memory management. + */ + class HybridNotificationsSpecSwift: public virtual HybridNotificationsSpec { + public: + // Constructor from a Swift instance + explicit HybridNotificationsSpecSwift(const NitroVoidhash::HybridNotificationsSpec_cxx& swiftPart): + HybridObject(HybridNotificationsSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline NitroVoidhash::HybridNotificationsSpec_cxx& getSwiftPart() noexcept { + return _swiftPart; + } + + public: + // Get memory pressure + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr> getPermissionStatus() override { + auto __result = _swiftPart.getPermissionStatus(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> requestPermission(bool provisional) override { + auto __result = _swiftPart.requestPermission(std::forward(provisional)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getToken() override { + auto __result = _swiftPart.getToken(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> setBadgeCount(double count) override { + auto __result = _swiftPart.setBadgeCount(std::forward(count)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void subscribe(const std::string& subscriptionId, const std::function& listener) override { + auto __result = _swiftPart.subscribe(subscriptionId, listener); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline void unsubscribe(const std::string& subscriptionId) override { + auto __result = _swiftPart.unsubscribe(subscriptionId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + + private: + NitroVoidhash::HybridNotificationsSpec_cxx _swiftPart; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridPaywallPresenterSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridPaywallPresenterSpecSwift.hpp index 45852f8a8..8a963bf38 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridPaywallPresenterSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridPaywallPresenterSpecSwift.hpp @@ -54,7 +54,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridPurchasedItemSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridPurchasedItemSpecSwift.hpp index 1489914f3..a45dffe5e 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridPurchasedItemSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridPurchasedItemSpecSwift.hpp @@ -70,7 +70,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridPurchasedItemSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductOfferSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductOfferSpecSwift.hpp index 0954856a8..8f1b8a537 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductOfferSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductOfferSpecSwift.hpp @@ -84,7 +84,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridStorekitProductOfferSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSpecSwift.hpp index 5848393bd..8703b6ebc 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSpecSwift.hpp @@ -96,7 +96,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridStorekitProductSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionPeriodSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionPeriodSpecSwift.hpp index 3a4c1734f..79561ef0d 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionPeriodSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionPeriodSpecSwift.hpp @@ -62,7 +62,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridStorekitProductSubscriptionPeriodSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionSpecSwift.hpp index 6b67e515e..9dfa63fc4 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitProductSubscriptionSpecSwift.hpp @@ -78,7 +78,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridStorekitProductSubscriptionSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitSpecSwift.hpp index 58b35d3a6..281c92838 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitSpecSwift.hpp @@ -61,7 +61,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitTransactionSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitTransactionSpecSwift.hpp index e0e3355be..e706bcd25 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitTransactionSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridStorekitTransactionSpecSwift.hpp @@ -159,7 +159,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + private: NitroVoidhash::HybridStorekitTransactionSpec_cxx _swiftPart; diff --git a/libraries/react-native/nitrogen/generated/ios/c++/HybridVoidhashSpecSwift.hpp b/libraries/react-native/nitrogen/generated/ios/c++/HybridVoidhashSpecSwift.hpp index bb693d0cc..6f02516e1 100644 --- a/libraries/react-native/nitrogen/generated/ios/c++/HybridVoidhashSpecSwift.hpp +++ b/libraries/react-native/nitrogen/generated/ios/c++/HybridVoidhashSpecSwift.hpp @@ -55,7 +55,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementBridgeEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementBridgeEvent.swift new file mode 100644 index 000000000..92cb06c43 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementBridgeEvent.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_MeasurementBridgeEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ event: MeasurementBridgeEvent) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_MeasurementBridgeEvent { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ event: MeasurementBridgeEvent) -> Void + + public init(_ closure: @escaping (_ event: MeasurementBridgeEvent) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(event: MeasurementBridgeEvent) -> Void { + self.closure(event) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_MeasurementBridgeEvent`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_MeasurementBridgeEvent { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementCommandResult.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementCommandResult.swift new file mode 100644 index 000000000..2e6fd9745 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementCommandResult.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_MeasurementCommandResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: MeasurementCommandResult) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_MeasurementCommandResult { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: MeasurementCommandResult) -> Void + + public init(_ closure: @escaping (_ value: MeasurementCommandResult) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: MeasurementCommandResult) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_MeasurementCommandResult`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_MeasurementCommandResult { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementConfigurationStateBridge.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementConfigurationStateBridge.swift new file mode 100644 index 000000000..60345c276 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementConfigurationStateBridge.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_MeasurementConfigurationStateBridge.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: MeasurementConfigurationStateBridge) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_MeasurementConfigurationStateBridge { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: MeasurementConfigurationStateBridge) -> Void + + public init(_ closure: @escaping (_ value: MeasurementConfigurationStateBridge) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: MeasurementConfigurationStateBridge) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_MeasurementConfigurationStateBridge`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_MeasurementConfigurationStateBridge { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementFlushBridgeResult.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementFlushBridgeResult.swift new file mode 100644 index 000000000..e2c7d7dcf --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementFlushBridgeResult.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_MeasurementFlushBridgeResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: MeasurementFlushBridgeResult) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_MeasurementFlushBridgeResult { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: MeasurementFlushBridgeResult) -> Void + + public init(_ closure: @escaping (_ value: MeasurementFlushBridgeResult) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: MeasurementFlushBridgeResult) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_MeasurementFlushBridgeResult`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_MeasurementFlushBridgeResult { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementStateBridge.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementStateBridge.swift new file mode 100644 index 000000000..6ae47f264 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_MeasurementStateBridge.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_MeasurementStateBridge.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: MeasurementStateBridge) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_MeasurementStateBridge { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: MeasurementStateBridge) -> Void + + public init(_ closure: @escaping (_ value: MeasurementStateBridge) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: MeasurementStateBridge) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_MeasurementStateBridge`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_MeasurementStateBridge { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativeNotificationEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativeNotificationEvent.swift new file mode 100644 index 000000000..11e8a09c6 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativeNotificationEvent.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_NativeNotificationEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ event: NativeNotificationEvent) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_NativeNotificationEvent { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ event: NativeNotificationEvent) -> Void + + public init(_ closure: @escaping (_ event: NativeNotificationEvent) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(event: NativeNotificationEvent) -> Void { + self.closure(event) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_NativeNotificationEvent`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_NativeNotificationEvent { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativePushToken.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativePushToken.swift new file mode 100644 index 000000000..bae2c8752 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_NativePushToken.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_NativePushToken.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: NativePushToken) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_NativePushToken { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: NativePushToken) -> Void + + public init(_ closure: @escaping (_ value: NativePushToken) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: NativePushToken) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_NativePushToken`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_NativePushToken { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift new file mode 100644 index 000000000..f5943b9c0 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__shared_ptr_ArrayBuffer_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: ArrayBuffer) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__shared_ptr_ArrayBuffer_ { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: ArrayBuffer) -> Void + + public init(_ closure: @escaping (_ value: ArrayBuffer) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: ArrayBuffer) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__shared_ptr_ArrayBuffer_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__shared_ptr_ArrayBuffer_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__string.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__string.swift index 42744ddad..5ef8dced2 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__string.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__string.swift @@ -8,21 +8,21 @@ import NitroModules /** - * Wraps a Swift `(_ rawEvent: String) -> Void` as a class. + * Wraps a Swift `(_ value: String) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. */ public final class Func_void_std__string { public typealias bridge = margelo.nitro.voidhash.bridge.swift - private let closure: (_ rawEvent: String) -> Void + private let closure: (_ value: String) -> Void - public init(_ closure: @escaping (_ rawEvent: String) -> Void) { + public init(_ closure: @escaping (_ value: String) -> Void) { self.closure = closure } @inline(__always) - public func call(rawEvent: std.string) -> Void { - self.closure(String(rawEvent)) + public func call(value: std.string) -> Void { + self.closure(String(value)) } /** diff --git a/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__vector_MeasurementInboxEntry_.swift b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__vector_MeasurementInboxEntry_.swift new file mode 100644 index 000000000..8a30063bc --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/Func_void_std__vector_MeasurementInboxEntry_.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__vector_MeasurementInboxEntry_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: [MeasurementInboxEntry]) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__vector_MeasurementInboxEntry_ { + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + private let closure: (_ value: [MeasurementInboxEntry]) -> Void + + public init(_ closure: @escaping (_ value: [MeasurementInboxEntry]) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: bridge.std__vector_MeasurementInboxEntry_) -> Void { + self.closure(value.map({ __item in __item })) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__vector_MeasurementInboxEntry_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__vector_MeasurementInboxEntry_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec.swift new file mode 100644 index 000000000..048f3d864 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec.swift @@ -0,0 +1,71 @@ +/// +/// HybridMeasurementSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/// See ``HybridMeasurementSpec`` +public protocol HybridMeasurementSpec_protocol: HybridObject { + // Properties + + + // Methods + func initialize(publishableKey: String, configuration: MeasurementInitializeConfiguration) throws -> Promise + func enqueue(command: MeasurementCommand) throws -> Promise + func flush() throws -> Promise + func getInstallationId() throws -> Promise + func getState() throws -> Promise + func subscribe(subscriptionId: String, listener: @escaping (_ event: MeasurementBridgeEvent) -> Void) throws -> Void + func unsubscribe(subscriptionId: String) throws -> Void + func peekInbox(limit: Double) throws -> Promise<[MeasurementInboxEntry]> + func acknowledgeInbox(entryId: String) throws -> Promise + func readProtectedEvidence(blobId: String) throws -> Promise + func putProtectedEvidence(input: MeasurementProtectedEvidenceInput) throws -> Promise + func deleteProtectedEvidence(blobId: String) throws -> Promise + func deleteProtectedData(requestId: String) throws -> Promise + func getMeasurementConfigurationState() throws -> Promise + func persistMeasurementConfigurationState(version: Double, payload: ArrayBuffer) throws -> Promise + func applyMeasurementConfiguration(version: Double, payload: ArrayBuffer) throws -> Promise + func applyMeasurementStorageLimits(maxOutboxRecords: Double, maxOutboxBytes: Double, maxProtectedBytes: Double) throws -> Promise + func getPushRegistrationState() throws -> Promise + func persistPushRegistrationState(payload: ArrayBuffer) throws -> Promise + func clearPushRegistrationState() throws -> Promise + func getTestDeviceState() throws -> Promise + func persistTestDeviceState(enabled: Bool) throws -> Promise + func hasDedupe(namespace: String, key: String) throws -> Promise + func checkAndSetDedupe(namespace: String, key: String, expiresAtMs: Double) throws -> Promise +} + +/// See ``HybridMeasurementSpec`` +public class HybridMeasurementSpec_base { + private weak var cxxWrapper: HybridMeasurementSpec_cxx? = nil + public func getCxxWrapper() -> HybridMeasurementSpec_cxx { + #if DEBUG + guard self is HybridMeasurementSpec else { + fatalError("`self` is not a `HybridMeasurementSpec`! Did you accidentally inherit from `HybridMeasurementSpec_base` instead of `HybridMeasurementSpec`?") + } + #endif + if let cxxWrapper = self.cxxWrapper { + return cxxWrapper + } else { + let cxxWrapper = HybridMeasurementSpec_cxx(self as! HybridMeasurementSpec) + self.cxxWrapper = cxxWrapper + return cxxWrapper + } + } +} + +/** + * A Swift base-protocol representing the Measurement HybridObject. + * Implement this protocol to create Swift-based instances of Measurement. + * ```swift + * class HybridMeasurement : HybridMeasurementSpec { + * // ... + * } + * ``` + */ +public typealias HybridMeasurementSpec = HybridMeasurementSpec_protocol & HybridMeasurementSpec_base diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec_cxx.swift new file mode 100644 index 000000000..8106b84b1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridMeasurementSpec_cxx.swift @@ -0,0 +1,553 @@ +/// +/// HybridMeasurementSpec_cxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * A class implementation that bridges HybridMeasurementSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +public class HybridMeasurementSpec_cxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::voidhash::bridge::swift`) + * from `NitroVoidhash-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Holds an instance of the `HybridMeasurementSpec` Swift protocol. + */ + private var __implementation: any HybridMeasurementSpec + + /** + * Holds a weak pointer to the C++ class that wraps the Swift class. + */ + private var __cxxPart: bridge.std__weak_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ + + /** + * Create a new `HybridMeasurementSpec_cxx` that wraps the given `HybridMeasurementSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: any HybridMeasurementSpec) { + self.__implementation = implementation + self.__cxxPart = .init() + /* no base class */ + } + + /** + * Get the actual `HybridMeasurementSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridMeasurementSpec() -> any HybridMeasurementSpec { + return __implementation + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridMeasurementSpec_cxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridMeasurementSpec_cxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } + + /** + * Gets (or creates) the C++ part of this Hybrid Object. + * The C++ part is a `std::shared_ptr`. + */ + public func getCxxPart() -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_ { + let cachedCxxPart = self.__cxxPart.lock() + if cachedCxxPart.__convertToBool() { + return cachedCxxPart + } else { + let newCxxPart = bridge.create_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(self.toUnsafe()) + __cxxPart = bridge.weakify_std__shared_ptr_margelo__nitro__voidhash__HybridMeasurementSpec_(newCxxPart) + return newCxxPart + } + } + + + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize + } + + // Properties + + + // Methods + @inline(__always) + public final func initialize(publishableKey: std.string, configuration: MeasurementInitializeConfiguration) -> bridge.Result_std__shared_ptr_Promise_MeasurementStateBridge___ { + do { + let __result = try self.__implementation.initialize(publishableKey: String(publishableKey), configuration: configuration) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementStateBridge__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementStateBridge__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementStateBridge__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(__exceptionPtr) + } + } + + @inline(__always) + public final func enqueue(command: MeasurementCommand) -> bridge.Result_std__shared_ptr_Promise_MeasurementCommandResult___ { + do { + let __result = try self.__implementation.enqueue(command: command) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementCommandResult__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementCommandResult__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementCommandResult__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementCommandResult___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementCommandResult___(__exceptionPtr) + } + } + + @inline(__always) + public final func flush() -> bridge.Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___ { + do { + let __result = try self.__implementation.flush() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementFlushBridgeResult__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementFlushBridgeResult__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementFlushBridgeResult__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementFlushBridgeResult___(__exceptionPtr) + } + } + + @inline(__always) + public final func getInstallationId() -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.getInstallationId() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func getState() -> bridge.Result_std__shared_ptr_Promise_MeasurementStateBridge___ { + do { + let __result = try self.__implementation.getState() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementStateBridge__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementStateBridge__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementStateBridge__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementStateBridge___(__exceptionPtr) + } + } + + @inline(__always) + public final func subscribe(subscriptionId: std.string, listener: bridge.Func_void_MeasurementBridgeEvent) -> bridge.Result_void_ { + do { + try self.__implementation.subscribe(subscriptionId: String(subscriptionId), listener: { () -> (MeasurementBridgeEvent) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_MeasurementBridgeEvent(listener) + return { (__event: MeasurementBridgeEvent) -> Void in + __wrappedFunction.call(__event) + } + }()) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func unsubscribe(subscriptionId: std.string) -> bridge.Result_void_ { + do { + try self.__implementation.unsubscribe(subscriptionId: String(subscriptionId)) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func peekInbox(limit: Double) -> bridge.Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____ { + do { + let __result = try self.__implementation.peekInbox(limit: limit) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__vector_MeasurementInboxEntry_ in + var __vector = bridge.create_std__vector_MeasurementInboxEntry_(__result.count) + for __item in __result { + __vector.push_back(__item) + } + return __vector + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__vector_MeasurementInboxEntry____(__exceptionPtr) + } + } + + @inline(__always) + public final func acknowledgeInbox(entryId: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.acknowledgeInbox(entryId: String(entryId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func readProtectedEvidence(blobId: std.string) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____ { + do { + let __result = try self.__implementation.readProtectedEvidence(blobId: String(blobId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer___(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result.getArrayBuffer()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_ArrayBuffer____(__exceptionPtr) + } + } + + @inline(__always) + public final func putProtectedEvidence(input: MeasurementProtectedEvidenceInput) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.putProtectedEvidence(input: input) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func deleteProtectedEvidence(blobId: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.deleteProtectedEvidence(blobId: String(blobId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func deleteProtectedData(requestId: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.deleteProtectedData(requestId: String(requestId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func getMeasurementConfigurationState() -> bridge.Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___ { + do { + let __result = try self.__implementation.getMeasurementConfigurationState() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementConfigurationStateBridge__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(__exceptionPtr) + } + } + + @inline(__always) + public final func persistMeasurementConfigurationState(version: Double, payload: ArrayBuffer) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.persistMeasurementConfigurationState(version: version, payload: payload) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func applyMeasurementConfiguration(version: Double, payload: ArrayBuffer) -> bridge.Result_std__shared_ptr_Promise_void___ { + do { + let __result = try self.__implementation.applyMeasurementConfiguration(version: version, payload: payload) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in + let __promise = bridge.create_std__shared_ptr_Promise_void__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise) + __result + .then({ __result in __promiseHolder.resolve() }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) + } + } + + @inline(__always) + public final func applyMeasurementStorageLimits(maxOutboxRecords: Double, maxOutboxBytes: Double, maxProtectedBytes: Double) -> bridge.Result_std__shared_ptr_Promise_void___ { + do { + let __result = try self.__implementation.applyMeasurementStorageLimits(maxOutboxRecords: maxOutboxRecords, maxOutboxBytes: maxOutboxBytes, maxProtectedBytes: maxProtectedBytes) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in + let __promise = bridge.create_std__shared_ptr_Promise_void__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise) + __result + .then({ __result in __promiseHolder.resolve() }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) + } + } + + @inline(__always) + public final func getPushRegistrationState() -> bridge.Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___ { + do { + let __result = try self.__implementation.getPushRegistrationState() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_MeasurementConfigurationStateBridge__ in + let __promise = bridge.create_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_MeasurementConfigurationStateBridge__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_MeasurementConfigurationStateBridge___(__exceptionPtr) + } + } + + @inline(__always) + public final func persistPushRegistrationState(payload: ArrayBuffer) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.persistPushRegistrationState(payload: payload) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func clearPushRegistrationState() -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.clearPushRegistrationState() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func getTestDeviceState() -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.getTestDeviceState() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func persistTestDeviceState(enabled: Bool) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.persistTestDeviceState(enabled: enabled) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func hasDedupe(namespace: std.string, key: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.hasDedupe(namespace: String(namespace), key: String(key)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } + + @inline(__always) + public final func checkAndSetDedupe(namespace: std.string, key: std.string, expiresAtMs: Double) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.checkAndSetDedupe(namespace: String(namespace), key: String(key), expiresAtMs: expiresAtMs) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec.swift new file mode 100644 index 000000000..86e81bfce --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec.swift @@ -0,0 +1,53 @@ +/// +/// HybridNotificationsSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/// See ``HybridNotificationsSpec`` +public protocol HybridNotificationsSpec_protocol: HybridObject { + // Properties + + + // Methods + func getPermissionStatus() throws -> Promise + func requestPermission(provisional: Bool) throws -> Promise + func getToken() throws -> Promise + func setBadgeCount(count: Double) throws -> Promise + func subscribe(subscriptionId: String, listener: @escaping (_ event: NativeNotificationEvent) -> Void) throws -> Void + func unsubscribe(subscriptionId: String) throws -> Void +} + +/// See ``HybridNotificationsSpec`` +public class HybridNotificationsSpec_base { + private weak var cxxWrapper: HybridNotificationsSpec_cxx? = nil + public func getCxxWrapper() -> HybridNotificationsSpec_cxx { + #if DEBUG + guard self is HybridNotificationsSpec else { + fatalError("`self` is not a `HybridNotificationsSpec`! Did you accidentally inherit from `HybridNotificationsSpec_base` instead of `HybridNotificationsSpec`?") + } + #endif + if let cxxWrapper = self.cxxWrapper { + return cxxWrapper + } else { + let cxxWrapper = HybridNotificationsSpec_cxx(self as! HybridNotificationsSpec) + self.cxxWrapper = cxxWrapper + return cxxWrapper + } + } +} + +/** + * A Swift base-protocol representing the Notifications HybridObject. + * Implement this protocol to create Swift-based instances of Notifications. + * ```swift + * class HybridNotifications : HybridNotificationsSpec { + * // ... + * } + * ``` + */ +public typealias HybridNotificationsSpec = HybridNotificationsSpec_protocol & HybridNotificationsSpec_base diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec_cxx.swift new file mode 100644 index 000000000..a401676f4 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridNotificationsSpec_cxx.swift @@ -0,0 +1,205 @@ +/// +/// HybridNotificationsSpec_cxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * A class implementation that bridges HybridNotificationsSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +public class HybridNotificationsSpec_cxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::voidhash::bridge::swift`) + * from `NitroVoidhash-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Holds an instance of the `HybridNotificationsSpec` Swift protocol. + */ + private var __implementation: any HybridNotificationsSpec + + /** + * Holds a weak pointer to the C++ class that wraps the Swift class. + */ + private var __cxxPart: bridge.std__weak_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ + + /** + * Create a new `HybridNotificationsSpec_cxx` that wraps the given `HybridNotificationsSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: any HybridNotificationsSpec) { + self.__implementation = implementation + self.__cxxPart = .init() + /* no base class */ + } + + /** + * Get the actual `HybridNotificationsSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridNotificationsSpec() -> any HybridNotificationsSpec { + return __implementation + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridNotificationsSpec_cxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridNotificationsSpec_cxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } + + /** + * Gets (or creates) the C++ part of this Hybrid Object. + * The C++ part is a `std::shared_ptr`. + */ + public func getCxxPart() -> bridge.std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_ { + let cachedCxxPart = self.__cxxPart.lock() + if cachedCxxPart.__convertToBool() { + return cachedCxxPart + } else { + let newCxxPart = bridge.create_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(self.toUnsafe()) + __cxxPart = bridge.weakify_std__shared_ptr_margelo__nitro__voidhash__HybridNotificationsSpec_(newCxxPart) + return newCxxPart + } + } + + + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize + } + + // Properties + + + // Methods + @inline(__always) + public final func getPermissionStatus() -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.getPermissionStatus() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func requestPermission(provisional: Bool) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.requestPermission(provisional: provisional) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func getToken() -> bridge.Result_std__shared_ptr_Promise_NativePushToken___ { + do { + let __result = try self.__implementation.getToken() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_NativePushToken__ in + let __promise = bridge.create_std__shared_ptr_Promise_NativePushToken__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_NativePushToken__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_NativePushToken___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_NativePushToken___(__exceptionPtr) + } + } + + @inline(__always) + public final func setBadgeCount(count: Double) -> bridge.Result_std__shared_ptr_Promise_void___ { + do { + let __result = try self.__implementation.setBadgeCount(count: count) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in + let __promise = bridge.create_std__shared_ptr_Promise_void__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise) + __result + .then({ __result in __promiseHolder.resolve() }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) + } + } + + @inline(__always) + public final func subscribe(subscriptionId: std.string, listener: bridge.Func_void_NativeNotificationEvent) -> bridge.Result_void_ { + do { + try self.__implementation.subscribe(subscriptionId: String(subscriptionId), listener: { () -> (NativeNotificationEvent) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_NativeNotificationEvent(listener) + return { (__event: NativeNotificationEvent) -> Void in + __wrappedFunction.call(__event) + } + }()) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func unsubscribe(subscriptionId: std.string) -> bridge.Result_void_ { + do { + try self.__implementation.unsubscribe(subscriptionId: String(subscriptionId)) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec.swift index 8fc658b34..bfb6b8a7e 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec.swift @@ -11,7 +11,7 @@ import NitroModules /// See ``HybridPaywallPresenterSpec`` public protocol HybridPaywallPresenterSpec_protocol: HybridObject { // Properties - + // Methods func preload(locationSlug: String, htmlUrl: String) throws -> Promise diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec_cxx.swift index ba31c8467..a426aad1a 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallPresenterSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridPaywallPresenterSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -97,7 +97,7 @@ public class HybridPaywallPresenterSpec_cxx { } // Properties - + // Methods @inline(__always) @@ -118,7 +118,7 @@ public class HybridPaywallPresenterSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } - + @inline(__always) public final func show(locationSlug: std.string, htmlUrl: std.string, onBridgeEvent: bridge.std__optional_std__function_void_const_std__string_____rawEvent______, onDismiss: bridge.std__optional_std__function_void____) -> bridge.Result_std__shared_ptr_Promise_bool___ { do { @@ -159,7 +159,7 @@ public class HybridPaywallPresenterSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } - + @inline(__always) public final func dismiss() -> bridge.Result_std__shared_ptr_Promise_void___ { do { @@ -178,7 +178,7 @@ public class HybridPaywallPresenterSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) } } - + @inline(__always) public final func release(locationSlug: std.string) -> bridge.Result_void_ { do { @@ -189,7 +189,7 @@ public class HybridPaywallPresenterSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func postMessage(locationSlug: std.string, data: std.string) -> bridge.Result_void_ { do { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallWebViewSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallWebViewSpec_cxx.swift index cb9e1dd36..941a1fa9b 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallWebViewSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridPaywallWebViewSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridPaywallWebViewSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -119,7 +119,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var javaScriptEnabled: Bool { @inline(__always) get { @@ -130,7 +130,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.javaScriptEnabled = newValue } } - + public final var cacheEnabled: Bool { @inline(__always) get { @@ -141,7 +141,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.cacheEnabled = newValue } } - + public final var incognito: Bool { @inline(__always) get { @@ -152,7 +152,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.incognito = newValue } } - + public final var userAgent: bridge.std__optional_std__string_ { @inline(__always) get { @@ -175,7 +175,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var applicationNameForUserAgent: bridge.std__optional_std__string_ { @inline(__always) get { @@ -198,7 +198,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var injectedJavaScript: bridge.std__optional_std__string_ { @inline(__always) get { @@ -221,7 +221,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var injectedJavaScriptBeforeContentLoaded: bridge.std__optional_std__string_ { @inline(__always) get { @@ -244,7 +244,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var injectedJavaScriptForMainFrameOnly: Bool { @inline(__always) get { @@ -255,7 +255,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.injectedJavaScriptForMainFrameOnly = newValue } } - + public final var injectedJavaScriptBeforeContentLoadedForMainFrameOnly: Bool { @inline(__always) get { @@ -266,7 +266,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.injectedJavaScriptBeforeContentLoadedForMainFrameOnly = newValue } } - + public final var mediaPlaybackRequiresUserAction: Bool { @inline(__always) get { @@ -277,7 +277,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.mediaPlaybackRequiresUserAction = newValue } } - + public final var allowsInlineMediaPlayback: Bool { @inline(__always) get { @@ -288,7 +288,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowsInlineMediaPlayback = newValue } } - + public final var allowsPictureInPictureMediaPlayback: Bool { @inline(__always) get { @@ -299,7 +299,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowsPictureInPictureMediaPlayback = newValue } } - + public final var allowsAirPlayForMediaPlayback: Bool { @inline(__always) get { @@ -310,7 +310,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowsAirPlayForMediaPlayback = newValue } } - + public final var allowsFullscreenVideo: Bool { @inline(__always) get { @@ -321,7 +321,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowsFullscreenVideo = newValue } } - + public final var setSupportMultipleWindows: Bool { @inline(__always) get { @@ -332,7 +332,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.setSupportMultipleWindows = newValue } } - + public final var setBuiltInZoomControls: Bool { @inline(__always) get { @@ -343,7 +343,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.setBuiltInZoomControls = newValue } } - + public final var setDisplayZoomControls: Bool { @inline(__always) get { @@ -354,7 +354,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.setDisplayZoomControls = newValue } } - + public final var scalesPageToFit: Bool { @inline(__always) get { @@ -365,7 +365,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.scalesPageToFit = newValue } } - + public final var thirdPartyCookiesEnabled: Bool { @inline(__always) get { @@ -376,7 +376,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.thirdPartyCookiesEnabled = newValue } } - + public final var sharedCookiesEnabled: Bool { @inline(__always) get { @@ -387,7 +387,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.sharedCookiesEnabled = newValue } } - + public final var allowFileAccess: Bool { @inline(__always) get { @@ -398,7 +398,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowFileAccess = newValue } } - + public final var allowFileAccessFromFileURLs: Bool { @inline(__always) get { @@ -409,7 +409,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowFileAccessFromFileURLs = newValue } } - + public final var allowUniversalAccessFromFileURLs: Bool { @inline(__always) get { @@ -420,7 +420,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.allowUniversalAccessFromFileURLs = newValue } } - + public final var textZoom: Double { @inline(__always) get { @@ -431,7 +431,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.textZoom = newValue } } - + public final var overScrollMode: Int32 { @inline(__always) get { @@ -442,7 +442,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.overScrollMode = margelo.nitro.voidhash.PaywallWebViewOverScrollModeType(rawValue: newValue)! } } - + public final var cacheMode: Int32 { @inline(__always) get { @@ -453,7 +453,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.cacheMode = margelo.nitro.voidhash.PaywallWebViewCacheMode(rawValue: newValue)! } } - + public final var mixedContentMode: Int32 { @inline(__always) get { @@ -464,7 +464,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.mixedContentMode = margelo.nitro.voidhash.PaywallWebViewMixedContentMode(rawValue: newValue)! } } - + public final var androidLayerType: Int32 { @inline(__always) get { @@ -475,7 +475,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.androidLayerType = margelo.nitro.voidhash.PaywallWebViewAndroidLayerType(rawValue: newValue)! } } - + public final var geolocationEnabled: Bool { @inline(__always) get { @@ -486,7 +486,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.geolocationEnabled = newValue } } - + public final var pullToRefreshEnabled: Bool { @inline(__always) get { @@ -497,7 +497,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.pullToRefreshEnabled = newValue } } - + public final var nestedScrollEnabled: Bool { @inline(__always) get { @@ -508,7 +508,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.nestedScrollEnabled = newValue } } - + public final var bounces: Bool { @inline(__always) get { @@ -519,7 +519,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.bounces = newValue } } - + public final var dataDetectorTypes: bridge.std__vector_PaywallWebViewDataDetectorType_ { @inline(__always) get { @@ -536,7 +536,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.dataDetectorTypes = newValue.map({ __item in __item }) } } - + public final var originWhitelist: bridge.std__vector_std__string_ { @inline(__always) get { @@ -553,7 +553,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.originWhitelist = newValue.map({ __item in String(__item) }) } } - + public final var messagingEnabled: Bool { @inline(__always) get { @@ -564,7 +564,7 @@ public class HybridPaywallWebViewSpec_cxx { self.__implementation.messagingEnabled = newValue } } - + public final var onLoadingStart: bridge.std__optional_std__function_void_const_PaywallWebViewNavigationEvent_____event______ { @inline(__always) get { @@ -595,7 +595,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onLoadingProgress: bridge.std__optional_std__function_void_const_PaywallWebViewProgressEvent_____event______ { @inline(__always) get { @@ -626,7 +626,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onLoadingFinish: bridge.std__optional_std__function_void_const_PaywallWebViewNavigationEvent_____event______ { @inline(__always) get { @@ -657,7 +657,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onLoadingError: bridge.std__optional_std__function_void_const_PaywallWebViewErrorEvent_____event______ { @inline(__always) get { @@ -688,7 +688,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onHttpError: bridge.std__optional_std__function_void_const_PaywallWebViewHttpErrorEvent_____event______ { @inline(__always) get { @@ -719,7 +719,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onMessage: bridge.std__optional_std__function_void_const_PaywallWebViewMessageEvent_____event______ { @inline(__always) get { @@ -750,7 +750,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onOpenWindow: bridge.std__optional_std__function_void_const_PaywallWebViewOpenWindowEvent_____event______ { @inline(__always) get { @@ -781,7 +781,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onFileDownload: bridge.std__optional_std__function_void_const_PaywallWebViewFileDownloadEvent_____event______ { @inline(__always) get { @@ -812,7 +812,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onRenderProcessGone: bridge.std__optional_std__function_void_const_PaywallWebViewRenderProcessGoneEvent_____event______ { @inline(__always) get { @@ -843,7 +843,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onContentProcessDidTerminate: bridge.std__optional_std__function_void_const_PaywallWebViewBaseEvent_____event______ { @inline(__always) get { @@ -874,7 +874,7 @@ public class HybridPaywallWebViewSpec_cxx { }() } } - + public final var onShouldStartLoadWithRequest: bridge.std__optional_std__function_bool_const_PaywallWebViewShouldStartLoadRequest_____event______ { @inline(__always) get { @@ -918,7 +918,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func goForward() -> bridge.Result_void_ { do { @@ -929,7 +929,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func reload() -> bridge.Result_void_ { do { @@ -940,7 +940,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func stopLoading() -> bridge.Result_void_ { do { @@ -951,7 +951,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func requestFocus() -> bridge.Result_void_ { do { @@ -962,7 +962,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func postMessage(data: std.string) -> bridge.Result_void_ { do { @@ -973,7 +973,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func injectJavaScript(javascript: std.string) -> bridge.Result_void_ { do { @@ -984,7 +984,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func loadUrl(url: std.string) -> bridge.Result_void_ { do { @@ -995,7 +995,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func clearFormData() -> bridge.Result_void_ { do { @@ -1006,7 +1006,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func clearHistory() -> bridge.Result_void_ { do { @@ -1017,7 +1017,7 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func clearCache(includeDiskFiles: Bool) -> bridge.Result_void_ { do { @@ -1028,15 +1028,15 @@ public class HybridPaywallWebViewSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + public final func getView() -> UnsafeMutableRawPointer { return Unmanaged.passRetained(__implementation.view).toOpaque() } - + public final func beforeUpdate() { __implementation.beforeUpdate() } - + public final func afterUpdate() { __implementation.afterUpdate() } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec.swift index 0f00366be..4d6d3ec8b 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec.swift @@ -15,7 +15,7 @@ public protocol HybridPurchasedItemSpec_protocol: HybridObject { var sku: String { get set } // Methods - + } /// See ``HybridPurchasedItemSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec_cxx.swift index 5363b4938..2cd2edf13 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridPurchasedItemSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridPurchasedItemSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -107,7 +107,7 @@ public class HybridPurchasedItemSpec_cxx { self.__implementation.type = margelo.nitro.voidhash.PurchasedItemType(rawValue: newValue)! } } - + public final var sku: std.string { @inline(__always) get { @@ -120,5 +120,5 @@ public class HybridPurchasedItemSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec.swift index ad025b3bb..1437c3958 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec.swift @@ -20,7 +20,7 @@ public protocol HybridStorekitProductOfferSpec_protocol: HybridObject { var displayPrice: String { get } // Methods - + } /// See ``HybridStorekitProductOfferSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec_cxx.swift index a151ffe57..80a8481b1 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductOfferSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitProductOfferSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -109,7 +109,7 @@ public class HybridStorekitProductOfferSpec_cxx { }() } } - + public final var period: bridge.std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ { @inline(__always) get { @@ -119,35 +119,35 @@ public class HybridStorekitProductOfferSpec_cxx { }() } } - + public final var periodCount: Double { @inline(__always) get { return self.__implementation.periodCount } } - + public final var paymentMode: std.string { @inline(__always) get { return std.string(self.__implementation.paymentMode) } } - + public final var type: std.string { @inline(__always) get { return std.string(self.__implementation.type) } } - + public final var price: Double { @inline(__always) get { return self.__implementation.price } } - + public final var displayPrice: std.string { @inline(__always) get { @@ -156,5 +156,5 @@ public class HybridStorekitProductOfferSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec.swift index 9fab58c1b..88999f290 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec.swift @@ -23,7 +23,7 @@ public protocol HybridStorekitProductSpec_protocol: HybridObject { var currency: String { get } // Methods - + } /// See ``HybridStorekitProductSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec_cxx.swift index c33e3cc60..8a6c63d4c 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitProductSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -109,49 +109,49 @@ public class HybridStorekitProductSpec_cxx { }() } } - + public final var description: std.string { @inline(__always) get { return std.string(self.__implementation.description) } } - + public final var displayName: std.string { @inline(__always) get { return std.string(self.__implementation.displayName) } } - + public final var displayPrice: std.string { @inline(__always) get { return std.string(self.__implementation.displayPrice) } } - + public final var id: std.string { @inline(__always) get { return std.string(self.__implementation.id) } } - + public final var isFamilyShareable: Bool { @inline(__always) get { return self.__implementation.isFamilyShareable } } - + public final var price: Double { @inline(__always) get { return self.__implementation.price } } - + public final var subscription: bridge.std__optional_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionSpec__ { @inline(__always) get { @@ -167,14 +167,14 @@ public class HybridStorekitProductSpec_cxx { }() } } - + public final var type: std.string { @inline(__always) get { return std.string(self.__implementation.type) } } - + public final var currency: std.string { @inline(__always) get { @@ -183,5 +183,5 @@ public class HybridStorekitProductSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec.swift index 53dce77c9..26f96d99a 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec.swift @@ -15,7 +15,7 @@ public protocol HybridStorekitProductSubscriptionPeriodSpec_protocol: HybridObje var value: Double { get } // Methods - + } /// See ``HybridStorekitProductSubscriptionPeriodSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec_cxx.swift index dba938d60..0836be7f7 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionPeriodSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitProductSubscriptionPeriodSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -103,7 +103,7 @@ public class HybridStorekitProductSubscriptionPeriodSpec_cxx { return self.__implementation.unit.rawValue } } - + public final var value: Double { @inline(__always) get { @@ -112,5 +112,5 @@ public class HybridStorekitProductSubscriptionPeriodSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec.swift index 0358e6e24..b49f1ad72 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec.swift @@ -17,7 +17,7 @@ public protocol HybridStorekitProductSubscriptionSpec_protocol: HybridObject { var subscriptionPeriod: (any HybridStorekitProductSubscriptionPeriodSpec) { get } // Methods - + } /// See ``HybridStorekitProductSubscriptionSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec_cxx.swift index 761b43d68..5e9e6cc9e 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitProductSubscriptionSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitProductSubscriptionSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -112,7 +112,7 @@ public class HybridStorekitProductSubscriptionSpec_cxx { }() } } - + public final var promotionalOffers: bridge.std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductOfferSpec__ { @inline(__always) get { @@ -128,14 +128,14 @@ public class HybridStorekitProductSubscriptionSpec_cxx { }() } } - + public final var subscriptionGroupID: std.string { @inline(__always) get { return std.string(self.__implementation.subscriptionGroupID) } } - + public final var subscriptionPeriod: bridge.std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSubscriptionPeriodSpec_ { @inline(__always) get { @@ -147,5 +147,5 @@ public class HybridStorekitProductSubscriptionSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec.swift index 58d7bd939..7901644e6 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec.swift @@ -11,7 +11,7 @@ import NitroModules /// See ``HybridStorekitSpec`` public protocol HybridStorekitSpec_protocol: HybridObject { // Properties - + // Methods func initConnection(onTransaction: ((_ transaction: (any HybridStorekitTransactionSpec)) -> Void)?) throws -> Promise diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec_cxx.swift index 4cde5e16a..f29976f24 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -97,7 +97,7 @@ public class HybridStorekitSpec_cxx { } // Properties - + // Methods @inline(__always) @@ -132,7 +132,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } - + @inline(__always) public final func endConnection() -> bridge.Result_std__shared_ptr_Promise_bool___ { do { @@ -151,7 +151,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } - + @inline(__always) public final func getPurchasedItems(onlyIncludeActiveItems: Bool) -> bridge.Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____ { do { @@ -179,7 +179,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec_____(__exceptionPtr) } } - + @inline(__always) public final func getItems(skus: bridge.std__vector_std__string_) -> bridge.Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____ { do { @@ -207,7 +207,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitProductSpec_____(__exceptionPtr) } } - + @inline(__always) public final func buyProduct(sku: std.string, appAccountToken: std.string, quantity: Double) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____ { do { @@ -229,7 +229,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec____(__exceptionPtr) } } - + @inline(__always) public final func finishTransaction(transactionId: std.string) -> bridge.Result_std__shared_ptr_Promise_void___ { do { @@ -248,7 +248,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) } } - + @inline(__always) public final func getPendingTransactions() -> bridge.Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___ { do { @@ -269,7 +269,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_std__vector_std__shared_ptr_margelo__nitro__voidhash__HybridStorekitTransactionSpec___(__exceptionPtr) } } - + @inline(__always) public final func presentCodeRedemptionSheet() -> bridge.Result_void_ { do { @@ -280,7 +280,7 @@ public class HybridStorekitSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } - + @inline(__always) public final func showManageSubscriptions() -> bridge.Result_std__shared_ptr_Promise_void___ { do { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec.swift index 3e757a574..1952083ea 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec.swift @@ -39,7 +39,7 @@ public protocol HybridStorekitTransactionSpec_protocol: HybridObject { var currencyIos: String? { get } // Methods - + } /// See ``HybridStorekitTransactionSpec`` diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec_cxx.swift index 5141bf138..2174bbf61 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridStorekitTransactionSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridStorekitTransactionSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -103,7 +103,7 @@ public class HybridStorekitTransactionSpec_cxx { return std.string(self.__implementation.id) } } - + public final var ids: bridge.std__vector_std__string_ { @inline(__always) get { @@ -116,49 +116,49 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var transactionId: std.string { @inline(__always) get { return std.string(self.__implementation.transactionId) } } - + public final var transactionDate: Double { @inline(__always) get { return self.__implementation.transactionDate } } - + public final var transactionReceipt: std.string { @inline(__always) get { return std.string(self.__implementation.transactionReceipt) } } - + public final var quantityIos: Double { @inline(__always) get { return self.__implementation.quantityIos } } - + public final var originalTransactionDateIos: Double { @inline(__always) get { return self.__implementation.originalTransactionDateIos } } - + public final var originalTransactionIdentifierIos: std.string { @inline(__always) get { return std.string(self.__implementation.originalTransactionIdentifierIos) } } - + public final var appAccountToken: bridge.std__optional_std__string_ { @inline(__always) get { @@ -171,21 +171,21 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var appBundleIdIos: std.string { @inline(__always) get { return std.string(self.__implementation.appBundleIdIos) } } - + public final var productTypeIos: std.string { @inline(__always) get { return std.string(self.__implementation.productTypeIos) } } - + public final var subscriptionGroupIdIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -198,7 +198,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var webOrderLineItemIdIos: bridge.std__optional_double_ { @inline(__always) get { @@ -211,7 +211,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var expirationDateIos: bridge.std__optional_double_ { @inline(__always) get { @@ -224,7 +224,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var isUpgradedIos: bridge.std__optional_bool_ { @inline(__always) get { @@ -237,14 +237,14 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var ownershipTypeIos: std.string { @inline(__always) get { return std.string(self.__implementation.ownershipTypeIos) } } - + public final var revocationDateIos: bridge.std__optional_double_ { @inline(__always) get { @@ -257,7 +257,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var revocationReasonIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -270,7 +270,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var transactionReasonIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -283,7 +283,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var jwsRepresentationIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -296,7 +296,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var environmentIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -309,7 +309,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var storefrontCountryCodeIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -322,7 +322,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var reasonIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -335,7 +335,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var offerIos: bridge.std__optional_StorekitProductPurchaseOffer_ { @inline(__always) get { @@ -348,7 +348,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var priceIos: bridge.std__optional_double_ { @inline(__always) get { @@ -361,7 +361,7 @@ public class HybridStorekitTransactionSpec_cxx { }() } } - + public final var currencyIos: bridge.std__optional_std__string_ { @inline(__always) get { @@ -376,5 +376,5 @@ public class HybridStorekitTransactionSpec_cxx { } // Methods - + } diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec.swift index 2eca1e475..a41ea636d 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec.swift @@ -11,7 +11,7 @@ import NitroModules /// See ``HybridVoidhashSpec`` public protocol HybridVoidhashSpec_protocol: HybridObject { // Properties - + // Methods func purchase(sku: String) throws -> Promise<(any HybridPurchasedItemSpec)> diff --git a/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec_cxx.swift b/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec_cxx.swift index f5bce06d1..a939c8b0d 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec_cxx.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/HybridVoidhashSpec_cxx.swift @@ -85,7 +85,7 @@ public class HybridVoidhashSpec_cxx { } } - + /** * Get the memory size of the Swift class (plus size of any other allocations) @@ -97,7 +97,7 @@ public class HybridVoidhashSpec_cxx { } // Properties - + // Methods @inline(__always) diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeError.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeError.swift new file mode 100644 index 000000000..a86ffdd7e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeError.swift @@ -0,0 +1,115 @@ +/// +/// MeasurementBridgeError.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementBridgeError`, backed by a C++ struct. + */ +public typealias MeasurementBridgeError = margelo.nitro.voidhash.MeasurementBridgeError + +public extension MeasurementBridgeError { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementBridgeError`. + */ + init(code: String, message: String, source: MeasurementBridgeSource, capability: String?, reason: String?) { + self.init(std.string(code), std.string(message), source, { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = capability { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = reason { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }()) + } + + var code: String { + @inline(__always) + get { + return String(self.__code) + } + @inline(__always) + set { + self.__code = std.string(newValue) + } + } + + var message: String { + @inline(__always) + get { + return String(self.__message) + } + @inline(__always) + set { + self.__message = std.string(newValue) + } + } + + var source: MeasurementBridgeSource { + @inline(__always) + get { + return self.__source + } + @inline(__always) + set { + self.__source = newValue + } + } + + var capability: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__capability.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__capability = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var reason: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__reason.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__reason = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeEvent.swift new file mode 100644 index 000000000..339534700 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeEvent.swift @@ -0,0 +1,156 @@ +/// +/// MeasurementBridgeEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementBridgeEvent`, backed by a C++ struct. + */ +public typealias MeasurementBridgeEvent = margelo.nitro.voidhash.MeasurementBridgeEvent + +public extension MeasurementBridgeEvent { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementBridgeEvent`. + */ + init(subscriptionId: String, event: String, recordId: String?, requestId: String?, payload: ArrayBuffer?, error: MeasurementBridgeError?) { + self.init(std.string(subscriptionId), std.string(event), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = recordId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = requestId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__shared_ptr_ArrayBuffer__ in + if let __unwrappedValue = payload { + return bridge.create_std__optional_std__shared_ptr_ArrayBuffer__(__unwrappedValue.getArrayBuffer()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_MeasurementBridgeError_ in + if let __unwrappedValue = error { + return bridge.create_std__optional_MeasurementBridgeError_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var subscriptionId: String { + @inline(__always) + get { + return String(self.__subscriptionId) + } + @inline(__always) + set { + self.__subscriptionId = std.string(newValue) + } + } + + var event: String { + @inline(__always) + get { + return String(self.__event) + } + @inline(__always) + set { + self.__event = std.string(newValue) + } + } + + var recordId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__recordId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__recordId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var requestId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__requestId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__requestId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var payload: ArrayBuffer? { + @inline(__always) + get { + return self.__payload.value + } + @inline(__always) + set { + self.__payload = { () -> bridge.std__optional_std__shared_ptr_ArrayBuffer__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__shared_ptr_ArrayBuffer__(__unwrappedValue.getArrayBuffer()) + } else { + return .init() + } + }() + } + } + + var error: MeasurementBridgeError? { + @inline(__always) + get { + return { () -> MeasurementBridgeError? in + if let __unwrapped = self.__error.value { + return __unwrapped + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__error = { () -> bridge.std__optional_MeasurementBridgeError_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_MeasurementBridgeError_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeSource.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeSource.swift new file mode 100644 index 000000000..097e005a6 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementBridgeSource.swift @@ -0,0 +1,44 @@ +/// +/// MeasurementBridgeSource.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementBridgeSource`, backed by a C++ enum. + */ +public typealias MeasurementBridgeSource = margelo.nitro.voidhash.MeasurementBridgeSource + +public extension MeasurementBridgeSource { + /** + * Get a MeasurementBridgeSource for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "ios": + self = .ios + case "android": + self = .android + case "core": + self = .core + default: + return nil + } + } + + /** + * Get the String value this MeasurementBridgeSource represents. + */ + var stringValue: String { + switch self { + case .ios: + return "ios" + case .android: + return "android" + case .core: + return "core" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommand.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommand.swift new file mode 100644 index 000000000..8b5f448a1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommand.swift @@ -0,0 +1,217 @@ +/// +/// MeasurementCommand.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementCommand`, backed by a C++ struct. + */ +public typealias MeasurementCommand = margelo.nitro.voidhash.MeasurementCommand + +public extension MeasurementCommand { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementCommand`. + */ + init(kind: MeasurementCommandKind, commandId: String, recordType: String, occurredAt: String, source: MeasurementRecordSource, priority: MeasurementRecordPriority, publicPayload: ArrayBuffer, protectedEvidenceRef: String?, identity: MeasurementIdentitySnapshot?, consent: MeasurementConsentSnapshot?, session: MeasurementSessionSnapshot?) { + self.init(kind, std.string(commandId), std.string(recordType), std.string(occurredAt), source, priority, publicPayload.getArrayBuffer(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = protectedEvidenceRef { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_MeasurementIdentitySnapshot_ in + if let __unwrappedValue = identity { + return bridge.create_std__optional_MeasurementIdentitySnapshot_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_MeasurementConsentSnapshot_ in + if let __unwrappedValue = consent { + return bridge.create_std__optional_MeasurementConsentSnapshot_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_MeasurementSessionSnapshot_ in + if let __unwrappedValue = session { + return bridge.create_std__optional_MeasurementSessionSnapshot_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var kind: MeasurementCommandKind { + @inline(__always) + get { + return self.__kind + } + @inline(__always) + set { + self.__kind = newValue + } + } + + var commandId: String { + @inline(__always) + get { + return String(self.__commandId) + } + @inline(__always) + set { + self.__commandId = std.string(newValue) + } + } + + var recordType: String { + @inline(__always) + get { + return String(self.__recordType) + } + @inline(__always) + set { + self.__recordType = std.string(newValue) + } + } + + var occurredAt: String { + @inline(__always) + get { + return String(self.__occurredAt) + } + @inline(__always) + set { + self.__occurredAt = std.string(newValue) + } + } + + var source: MeasurementRecordSource { + @inline(__always) + get { + return self.__source + } + @inline(__always) + set { + self.__source = newValue + } + } + + var priority: MeasurementRecordPriority { + @inline(__always) + get { + return self.__priority + } + @inline(__always) + set { + self.__priority = newValue + } + } + + var publicPayload: ArrayBuffer { + @inline(__always) + get { + return ArrayBuffer(self.__publicPayload) + } + @inline(__always) + set { + self.__publicPayload = newValue.getArrayBuffer() + } + } + + var protectedEvidenceRef: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__protectedEvidenceRef.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__protectedEvidenceRef = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var identity: MeasurementIdentitySnapshot? { + @inline(__always) + get { + return { () -> MeasurementIdentitySnapshot? in + if let __unwrapped = self.__identity.value { + return __unwrapped + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__identity = { () -> bridge.std__optional_MeasurementIdentitySnapshot_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_MeasurementIdentitySnapshot_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var consent: MeasurementConsentSnapshot? { + @inline(__always) + get { + return { () -> MeasurementConsentSnapshot? in + if let __unwrapped = self.__consent.value { + return __unwrapped + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__consent = { () -> bridge.std__optional_MeasurementConsentSnapshot_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_MeasurementConsentSnapshot_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var session: MeasurementSessionSnapshot? { + @inline(__always) + get { + return { () -> MeasurementSessionSnapshot? in + if let __unwrapped = self.__session.value { + return __unwrapped + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__session = { () -> bridge.std__optional_MeasurementSessionSnapshot_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_MeasurementSessionSnapshot_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandKind.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandKind.swift new file mode 100644 index 000000000..f4732363e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandKind.swift @@ -0,0 +1,72 @@ +/// +/// MeasurementCommandKind.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementCommandKind`, backed by a C++ enum. + */ +public typealias MeasurementCommandKind = margelo.nitro.voidhash.MeasurementCommandKind + +public extension MeasurementCommandKind { + /** + * Get a MeasurementCommandKind for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "enqueueRecord": + self = .enqueuerecord + case "identityTransition": + self = .identitytransition + case "consentTransition": + self = .consenttransition + case "sessionSignal": + self = .sessionsignal + case "coldLaunchInput": + self = .coldlaunchinput + case "transactionDedup": + self = .transactiondedup + case "linkInput": + self = .linkinput + case "pushInput": + self = .pushinput + case "purchaseInput": + self = .purchaseinput + case "identifierInput": + self = .identifierinput + default: + return nil + } + } + + /** + * Get the String value this MeasurementCommandKind represents. + */ + var stringValue: String { + switch self { + case .enqueuerecord: + return "enqueueRecord" + case .identitytransition: + return "identityTransition" + case .consenttransition: + return "consentTransition" + case .sessionsignal: + return "sessionSignal" + case .coldlaunchinput: + return "coldLaunchInput" + case .transactiondedup: + return "transactionDedup" + case .linkinput: + return "linkInput" + case .pushinput: + return "pushInput" + case .purchaseinput: + return "purchaseInput" + case .identifierinput: + return "identifierInput" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandResult.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandResult.swift new file mode 100644 index 000000000..0b7a5121f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementCommandResult.swift @@ -0,0 +1,116 @@ +/// +/// MeasurementCommandResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementCommandResult`, backed by a C++ struct. + */ +public typealias MeasurementCommandResult = margelo.nitro.voidhash.MeasurementCommandResult + +public extension MeasurementCommandResult { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementCommandResult`. + */ + init(accepted: Bool, recordId: String?, installationSequence: Double?, error: MeasurementBridgeError?) { + self.init(accepted, { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = recordId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = installationSequence { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_MeasurementBridgeError_ in + if let __unwrappedValue = error { + return bridge.create_std__optional_MeasurementBridgeError_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var accepted: Bool { + @inline(__always) + get { + return self.__accepted + } + @inline(__always) + set { + self.__accepted = newValue + } + } + + var recordId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__recordId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__recordId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var installationSequence: Double? { + @inline(__always) + get { + return self.__installationSequence.value + } + @inline(__always) + set { + self.__installationSequence = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var error: MeasurementBridgeError? { + @inline(__always) + get { + return { () -> MeasurementBridgeError? in + if let __unwrapped = self.__error.value { + return __unwrapped + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__error = { () -> bridge.std__optional_MeasurementBridgeError_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_MeasurementBridgeError_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConfigurationStateBridge.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConfigurationStateBridge.swift new file mode 100644 index 000000000..b48b1551f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConfigurationStateBridge.swift @@ -0,0 +1,58 @@ +/// +/// MeasurementConfigurationStateBridge.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementConfigurationStateBridge`, backed by a C++ struct. + */ +public typealias MeasurementConfigurationStateBridge = margelo.nitro.voidhash.MeasurementConfigurationStateBridge + +public extension MeasurementConfigurationStateBridge { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementConfigurationStateBridge`. + */ + init(version: Double, payload: ArrayBuffer?) { + self.init(version, { () -> bridge.std__optional_std__shared_ptr_ArrayBuffer__ in + if let __unwrappedValue = payload { + return bridge.create_std__optional_std__shared_ptr_ArrayBuffer__(__unwrappedValue.getArrayBuffer()) + } else { + return .init() + } + }()) + } + + var version: Double { + @inline(__always) + get { + return self.__version + } + @inline(__always) + set { + self.__version = newValue + } + } + + var payload: ArrayBuffer? { + @inline(__always) + get { + return self.__payload.value + } + @inline(__always) + set { + self.__payload = { () -> bridge.std__optional_std__shared_ptr_ArrayBuffer__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__shared_ptr_ArrayBuffer__(__unwrappedValue.getArrayBuffer()) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConsentSnapshot.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConsentSnapshot.swift new file mode 100644 index 000000000..167dc6455 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementConsentSnapshot.swift @@ -0,0 +1,195 @@ +/// +/// MeasurementConsentSnapshot.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementConsentSnapshot`, backed by a C++ struct. + */ +public typealias MeasurementConsentSnapshot = margelo.nitro.voidhash.MeasurementConsentSnapshot + +public extension MeasurementConsentSnapshot { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementConsentSnapshot`. + */ + init(revision: Double, decidedAt: String, source: String, gdprApplies: Bool?, dataUsage: Bool?, adsPersonalization: Bool?, adStorage: Bool?, collectionOptOut: Bool?, partnerSharingOptOut: Bool?) { + self.init(revision, std.string(decidedAt), std.string(source), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = gdprApplies { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = dataUsage { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = adsPersonalization { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = adStorage { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = collectionOptOut { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = partnerSharingOptOut { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var revision: Double { + @inline(__always) + get { + return self.__revision + } + @inline(__always) + set { + self.__revision = newValue + } + } + + var decidedAt: String { + @inline(__always) + get { + return String(self.__decidedAt) + } + @inline(__always) + set { + self.__decidedAt = std.string(newValue) + } + } + + var source: String { + @inline(__always) + get { + return String(self.__source) + } + @inline(__always) + set { + self.__source = std.string(newValue) + } + } + + var gdprApplies: Bool? { + @inline(__always) + get { + return self.__gdprApplies.value + } + @inline(__always) + set { + self.__gdprApplies = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var dataUsage: Bool? { + @inline(__always) + get { + return self.__dataUsage.value + } + @inline(__always) + set { + self.__dataUsage = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var adsPersonalization: Bool? { + @inline(__always) + get { + return self.__adsPersonalization.value + } + @inline(__always) + set { + self.__adsPersonalization = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var adStorage: Bool? { + @inline(__always) + get { + return self.__adStorage.value + } + @inline(__always) + set { + self.__adStorage = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var collectionOptOut: Bool? { + @inline(__always) + get { + return self.__collectionOptOut.value + } + @inline(__always) + set { + self.__collectionOptOut = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var partnerSharingOptOut: Bool? { + @inline(__always) + get { + return self.__partnerSharingOptOut.value + } + @inline(__always) + set { + self.__partnerSharingOptOut = { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementFlushBridgeResult.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementFlushBridgeResult.swift new file mode 100644 index 000000000..a377f8b9d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementFlushBridgeResult.swift @@ -0,0 +1,68 @@ +/// +/// MeasurementFlushBridgeResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementFlushBridgeResult`, backed by a C++ struct. + */ +public typealias MeasurementFlushBridgeResult = margelo.nitro.voidhash.MeasurementFlushBridgeResult + +public extension MeasurementFlushBridgeResult { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementFlushBridgeResult`. + */ + init(accepted: Double, scheduled: Double, quarantined: Double, policyBlocked: Double) { + self.init(accepted, scheduled, quarantined, policyBlocked) + } + + var accepted: Double { + @inline(__always) + get { + return self.__accepted + } + @inline(__always) + set { + self.__accepted = newValue + } + } + + var scheduled: Double { + @inline(__always) + get { + return self.__scheduled + } + @inline(__always) + set { + self.__scheduled = newValue + } + } + + var quarantined: Double { + @inline(__always) + get { + return self.__quarantined + } + @inline(__always) + set { + self.__quarantined = newValue + } + } + + var policyBlocked: Double { + @inline(__always) + get { + return self.__policyBlocked + } + @inline(__always) + set { + self.__policyBlocked = newValue + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementIdentitySnapshot.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementIdentitySnapshot.swift new file mode 100644 index 000000000..e06f1126d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementIdentitySnapshot.swift @@ -0,0 +1,104 @@ +/// +/// MeasurementIdentitySnapshot.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementIdentitySnapshot`, backed by a C++ struct. + */ +public typealias MeasurementIdentitySnapshot = margelo.nitro.voidhash.MeasurementIdentitySnapshot + +public extension MeasurementIdentitySnapshot { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementIdentitySnapshot`. + */ + init(distinctId: String, anonymousId: String?, personId: String?, revision: Double) { + self.init(std.string(distinctId), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = anonymousId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = personId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), revision) + } + + var distinctId: String { + @inline(__always) + get { + return String(self.__distinctId) + } + @inline(__always) + set { + self.__distinctId = std.string(newValue) + } + } + + var anonymousId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__anonymousId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__anonymousId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var personId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__personId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__personId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var revision: Double { + @inline(__always) + get { + return self.__revision + } + @inline(__always) + set { + self.__revision = newValue + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInboxEntry.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInboxEntry.swift new file mode 100644 index 000000000..9d00e8843 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInboxEntry.swift @@ -0,0 +1,90 @@ +/// +/// MeasurementInboxEntry.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementInboxEntry`, backed by a C++ struct. + */ +public typealias MeasurementInboxEntry = margelo.nitro.voidhash.MeasurementInboxEntry + +public extension MeasurementInboxEntry { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementInboxEntry`. + */ + init(id: String, kind: String, source: String, appState: String, receivedAt: String, protectedEvidenceRef: String) { + self.init(std.string(id), std.string(kind), std.string(source), std.string(appState), std.string(receivedAt), std.string(protectedEvidenceRef)) + } + + var id: String { + @inline(__always) + get { + return String(self.__id) + } + @inline(__always) + set { + self.__id = std.string(newValue) + } + } + + var kind: String { + @inline(__always) + get { + return String(self.__kind) + } + @inline(__always) + set { + self.__kind = std.string(newValue) + } + } + + var source: String { + @inline(__always) + get { + return String(self.__source) + } + @inline(__always) + set { + self.__source = std.string(newValue) + } + } + + var appState: String { + @inline(__always) + get { + return String(self.__appState) + } + @inline(__always) + set { + self.__appState = std.string(newValue) + } + } + + var receivedAt: String { + @inline(__always) + get { + return String(self.__receivedAt) + } + @inline(__always) + set { + self.__receivedAt = std.string(newValue) + } + } + + var protectedEvidenceRef: String { + @inline(__always) + get { + return String(self.__protectedEvidenceRef) + } + @inline(__always) + set { + self.__protectedEvidenceRef = std.string(newValue) + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInitializeConfiguration.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInitializeConfiguration.swift new file mode 100644 index 000000000..011fbf626 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementInitializeConfiguration.swift @@ -0,0 +1,80 @@ +/// +/// MeasurementInitializeConfiguration.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementInitializeConfiguration`, backed by a C++ struct. + */ +public typealias MeasurementInitializeConfiguration = margelo.nitro.voidhash.MeasurementInitializeConfiguration + +public extension MeasurementInitializeConfiguration { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementInitializeConfiguration`. + */ + init(apiUrl: String, ingestUrl: String, linksUrl: String, trustedConfigKeyIds: [String]) { + self.init(std.string(apiUrl), std.string(ingestUrl), std.string(linksUrl), { () -> bridge.std__vector_std__string_ in + var __vector = bridge.create_std__vector_std__string_(trustedConfigKeyIds.count) + for __item in trustedConfigKeyIds { + __vector.push_back(std.string(__item)) + } + return __vector + }()) + } + + var apiUrl: String { + @inline(__always) + get { + return String(self.__apiUrl) + } + @inline(__always) + set { + self.__apiUrl = std.string(newValue) + } + } + + var ingestUrl: String { + @inline(__always) + get { + return String(self.__ingestUrl) + } + @inline(__always) + set { + self.__ingestUrl = std.string(newValue) + } + } + + var linksUrl: String { + @inline(__always) + get { + return String(self.__linksUrl) + } + @inline(__always) + set { + self.__linksUrl = std.string(newValue) + } + } + + var trustedConfigKeyIds: [String] { + @inline(__always) + get { + return self.__trustedConfigKeyIds.map({ __item in String(__item) }) + } + @inline(__always) + set { + self.__trustedConfigKeyIds = { () -> bridge.std__vector_std__string_ in + var __vector = bridge.create_std__vector_std__string_(newValue.count) + for __item in newValue { + __vector.push_back(std.string(__item)) + } + return __vector + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedEvidenceInput.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedEvidenceInput.swift new file mode 100644 index 000000000..a2f613176 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedEvidenceInput.swift @@ -0,0 +1,79 @@ +/// +/// MeasurementProtectedEvidenceInput.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementProtectedEvidenceInput`, backed by a C++ struct. + */ +public typealias MeasurementProtectedEvidenceInput = margelo.nitro.voidhash.MeasurementProtectedEvidenceInput + +public extension MeasurementProtectedEvidenceInput { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementProtectedEvidenceInput`. + */ + init(blobId: String, purpose: MeasurementProtectedPurpose, consentRevision: Double, retentionClass: MeasurementProtectedRetention, value: ArrayBuffer) { + self.init(std.string(blobId), purpose, consentRevision, retentionClass, value.getArrayBuffer()) + } + + var blobId: String { + @inline(__always) + get { + return String(self.__blobId) + } + @inline(__always) + set { + self.__blobId = std.string(newValue) + } + } + + var purpose: MeasurementProtectedPurpose { + @inline(__always) + get { + return self.__purpose + } + @inline(__always) + set { + self.__purpose = newValue + } + } + + var consentRevision: Double { + @inline(__always) + get { + return self.__consentRevision + } + @inline(__always) + set { + self.__consentRevision = newValue + } + } + + var retentionClass: MeasurementProtectedRetention { + @inline(__always) + get { + return self.__retentionClass + } + @inline(__always) + set { + self.__retentionClass = newValue + } + } + + var value: ArrayBuffer { + @inline(__always) + get { + return ArrayBuffer(self.__value) + } + @inline(__always) + set { + self.__value = newValue.getArrayBuffer() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedPurpose.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedPurpose.swift new file mode 100644 index 000000000..41c18bfba --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedPurpose.swift @@ -0,0 +1,68 @@ +/// +/// MeasurementProtectedPurpose.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementProtectedPurpose`, backed by a C++ enum. + */ +public typealias MeasurementProtectedPurpose = margelo.nitro.voidhash.MeasurementProtectedPurpose + +public extension MeasurementProtectedPurpose { + /** + * Get a MeasurementProtectedPurpose for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "advertising-identifier": + self = .advertisingIdentifier + case "diagnostic-authorization": + self = .diagnosticAuthorization + case "email": + self = .email + case "install-referrer": + self = .installReferrer + case "link-capture": + self = .linkCapture + case "partner-context": + self = .partnerContext + case "phone": + self = .phone + case "purchase-receipt": + self = .purchaseReceipt + case "push-token": + self = .pushToken + default: + return nil + } + } + + /** + * Get the String value this MeasurementProtectedPurpose represents. + */ + var stringValue: String { + switch self { + case .advertisingIdentifier: + return "advertising-identifier" + case .diagnosticAuthorization: + return "diagnostic-authorization" + case .email: + return "email" + case .installReferrer: + return "install-referrer" + case .linkCapture: + return "link-capture" + case .partnerContext: + return "partner-context" + case .phone: + return "phone" + case .purchaseReceipt: + return "purchase-receipt" + case .pushToken: + return "push-token" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedRetention.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedRetention.swift new file mode 100644 index 000000000..26e5e78ed --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementProtectedRetention.swift @@ -0,0 +1,48 @@ +/// +/// MeasurementProtectedRetention.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementProtectedRetention`, backed by a C++ enum. + */ +public typealias MeasurementProtectedRetention = margelo.nitro.voidhash.MeasurementProtectedRetention + +public extension MeasurementProtectedRetention { + /** + * Get a MeasurementProtectedRetention for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "ephemeral": + self = .ephemeral + case "installation": + self = .installation + case "legal": + self = .legal + case "transaction": + self = .transaction + default: + return nil + } + } + + /** + * Get the String value this MeasurementProtectedRetention represents. + */ + var stringValue: String { + switch self { + case .ephemeral: + return "ephemeral" + case .installation: + return "installation" + case .legal: + return "legal" + case .transaction: + return "transaction" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordPriority.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordPriority.swift new file mode 100644 index 000000000..a147969de --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordPriority.swift @@ -0,0 +1,48 @@ +/// +/// MeasurementRecordPriority.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementRecordPriority`, backed by a C++ enum. + */ +public typealias MeasurementRecordPriority = margelo.nitro.voidhash.MeasurementRecordPriority + +public extension MeasurementRecordPriority { + /** + * Get a MeasurementRecordPriority for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "critical": + self = .critical + case "high": + self = .high + case "normal": + self = .normal + case "low": + self = .low + default: + return nil + } + } + + /** + * Get the String value this MeasurementRecordPriority represents. + */ + var stringValue: String { + switch self { + case .critical: + return "critical" + case .high: + return "high" + case .normal: + return "normal" + case .low: + return "low" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordSource.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordSource.swift new file mode 100644 index 000000000..eab62e882 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementRecordSource.swift @@ -0,0 +1,52 @@ +/// +/// MeasurementRecordSource.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MeasurementRecordSource`, backed by a C++ enum. + */ +public typealias MeasurementRecordSource = margelo.nitro.voidhash.MeasurementRecordSource + +public extension MeasurementRecordSource { + /** + * Get a MeasurementRecordSource for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "native": + self = .native + case "javascript": + self = .javascript + case "store": + self = .store + case "push": + self = .push + case "server-correlation": + self = .serverCorrelation + default: + return nil + } + } + + /** + * Get the String value this MeasurementRecordSource represents. + */ + var stringValue: String { + switch self { + case .native: + return "native" + case .javascript: + return "javascript" + case .store: + return "store" + case .push: + return "push" + case .serverCorrelation: + return "server-correlation" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementSessionSnapshot.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementSessionSnapshot.swift new file mode 100644 index 000000000..3be231ba0 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementSessionSnapshot.swift @@ -0,0 +1,68 @@ +/// +/// MeasurementSessionSnapshot.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementSessionSnapshot`, backed by a C++ struct. + */ +public typealias MeasurementSessionSnapshot = margelo.nitro.voidhash.MeasurementSessionSnapshot + +public extension MeasurementSessionSnapshot { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementSessionSnapshot`. + */ + init(id: String, sequence: Double, startedAt: String, reason: String) { + self.init(std.string(id), sequence, std.string(startedAt), std.string(reason)) + } + + var id: String { + @inline(__always) + get { + return String(self.__id) + } + @inline(__always) + set { + self.__id = std.string(newValue) + } + } + + var sequence: Double { + @inline(__always) + get { + return self.__sequence + } + @inline(__always) + set { + self.__sequence = newValue + } + } + + var startedAt: String { + @inline(__always) + get { + return String(self.__startedAt) + } + @inline(__always) + set { + self.__startedAt = std.string(newValue) + } + } + + var reason: String { + @inline(__always) + get { + return String(self.__reason) + } + @inline(__always) + set { + self.__reason = std.string(newValue) + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/MeasurementStateBridge.swift b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementStateBridge.swift new file mode 100644 index 000000000..d7bf0b09d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/MeasurementStateBridge.swift @@ -0,0 +1,209 @@ +/// +/// MeasurementStateBridge.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `MeasurementStateBridge`, backed by a C++ struct. + */ +public typealias MeasurementStateBridge = margelo.nitro.voidhash.MeasurementStateBridge + +public extension MeasurementStateBridge { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `MeasurementStateBridge`. + */ + init(installationId: String, firstOpenedAt: String, installationSequence: Double, readiness: String, currentSessionId: String?, currentSessionSequence: Double?, consentRevision: Double, configurationRevision: Double, outboxCritical: Double, outboxHigh: Double, outboxNormal: Double, outboxLow: Double, oldestRecordAgeMs: Double?) { + self.init(std.string(installationId), std.string(firstOpenedAt), installationSequence, std.string(readiness), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = currentSessionId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = currentSessionSequence { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), consentRevision, configurationRevision, outboxCritical, outboxHigh, outboxNormal, outboxLow, { () -> bridge.std__optional_double_ in + if let __unwrappedValue = oldestRecordAgeMs { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var installationId: String { + @inline(__always) + get { + return String(self.__installationId) + } + @inline(__always) + set { + self.__installationId = std.string(newValue) + } + } + + var firstOpenedAt: String { + @inline(__always) + get { + return String(self.__firstOpenedAt) + } + @inline(__always) + set { + self.__firstOpenedAt = std.string(newValue) + } + } + + var installationSequence: Double { + @inline(__always) + get { + return self.__installationSequence + } + @inline(__always) + set { + self.__installationSequence = newValue + } + } + + var readiness: String { + @inline(__always) + get { + return String(self.__readiness) + } + @inline(__always) + set { + self.__readiness = std.string(newValue) + } + } + + var currentSessionId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__currentSessionId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__currentSessionId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var currentSessionSequence: Double? { + @inline(__always) + get { + return self.__currentSessionSequence.value + } + @inline(__always) + set { + self.__currentSessionSequence = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var consentRevision: Double { + @inline(__always) + get { + return self.__consentRevision + } + @inline(__always) + set { + self.__consentRevision = newValue + } + } + + var configurationRevision: Double { + @inline(__always) + get { + return self.__configurationRevision + } + @inline(__always) + set { + self.__configurationRevision = newValue + } + } + + var outboxCritical: Double { + @inline(__always) + get { + return self.__outboxCritical + } + @inline(__always) + set { + self.__outboxCritical = newValue + } + } + + var outboxHigh: Double { + @inline(__always) + get { + return self.__outboxHigh + } + @inline(__always) + set { + self.__outboxHigh = newValue + } + } + + var outboxNormal: Double { + @inline(__always) + get { + return self.__outboxNormal + } + @inline(__always) + set { + self.__outboxNormal = newValue + } + } + + var outboxLow: Double { + @inline(__always) + get { + return self.__outboxLow + } + @inline(__always) + set { + self.__outboxLow = newValue + } + } + + var oldestRecordAgeMs: Double? { + @inline(__always) + get { + return self.__oldestRecordAgeMs.value + } + @inline(__always) + set { + self.__oldestRecordAgeMs = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEvent.swift new file mode 100644 index 000000000..84cf11f47 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEvent.swift @@ -0,0 +1,173 @@ +/// +/// NativeNotificationEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `NativeNotificationEvent`, backed by a C++ struct. + */ +public typealias NativeNotificationEvent = margelo.nitro.voidhash.NativeNotificationEvent + +public extension NativeNotificationEvent { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `NativeNotificationEvent`. + */ + init(id: String, kind: NativeNotificationEventKind, occurredAt: String, protectedPayloadRef: String?, pushNotificationSendId: String?, link: String?, errorCode: String?) { + self.init(std.string(id), kind, std.string(occurredAt), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = protectedPayloadRef { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = pushNotificationSendId { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = link { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = errorCode { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }()) + } + + var id: String { + @inline(__always) + get { + return String(self.__id) + } + @inline(__always) + set { + self.__id = std.string(newValue) + } + } + + var kind: NativeNotificationEventKind { + @inline(__always) + get { + return self.__kind + } + @inline(__always) + set { + self.__kind = newValue + } + } + + var occurredAt: String { + @inline(__always) + get { + return String(self.__occurredAt) + } + @inline(__always) + set { + self.__occurredAt = std.string(newValue) + } + } + + var protectedPayloadRef: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__protectedPayloadRef.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__protectedPayloadRef = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var pushNotificationSendId: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__pushNotificationSendId.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__pushNotificationSendId = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var link: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__link.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__link = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var errorCode: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__errorCode.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__errorCode = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEventKind.swift b/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEventKind.swift new file mode 100644 index 000000000..d18cc0d22 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/NativeNotificationEventKind.swift @@ -0,0 +1,48 @@ +/// +/// NativeNotificationEventKind.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `NativeNotificationEventKind`, backed by a C++ enum. + */ +public typealias NativeNotificationEventKind = margelo.nitro.voidhash.NativeNotificationEventKind + +public extension NativeNotificationEventKind { + /** + * Get a NativeNotificationEventKind for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "received": + self = .received + case "opened": + self = .opened + case "tokenChanged": + self = .tokenchanged + case "registrationError": + self = .registrationerror + default: + return nil + } + } + + /** + * Get the String value this NativeNotificationEventKind represents. + */ + var stringValue: String { + switch self { + case .received: + return "received" + case .opened: + return "opened" + case .tokenchanged: + return "tokenChanged" + case .registrationerror: + return "registrationError" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/NativePushEnvironment.swift b/libraries/react-native/nitrogen/generated/ios/swift/NativePushEnvironment.swift new file mode 100644 index 000000000..9b2f438fe --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/NativePushEnvironment.swift @@ -0,0 +1,40 @@ +/// +/// NativePushEnvironment.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `NativePushEnvironment`, backed by a C++ enum. + */ +public typealias NativePushEnvironment = margelo.nitro.voidhash.NativePushEnvironment + +public extension NativePushEnvironment { + /** + * Get a NativePushEnvironment for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "development": + self = .development + case "production": + self = .production + default: + return nil + } + } + + /** + * Get the String value this NativePushEnvironment represents. + */ + var stringValue: String { + switch self { + case .development: + return "development" + case .production: + return "production" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/NativePushProvider.swift b/libraries/react-native/nitrogen/generated/ios/swift/NativePushProvider.swift new file mode 100644 index 000000000..a9ded247d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/NativePushProvider.swift @@ -0,0 +1,40 @@ +/// +/// NativePushProvider.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `NativePushProvider`, backed by a C++ enum. + */ +public typealias NativePushProvider = margelo.nitro.voidhash.NativePushProvider + +public extension NativePushProvider { + /** + * Get a NativePushProvider for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "apns": + self = .apns + case "fcm": + self = .fcm + default: + return nil + } + } + + /** + * Get the String value this NativePushProvider represents. + */ + var stringValue: String { + switch self { + case .apns: + return "apns" + case .fcm: + return "fcm" + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/NativePushToken.swift b/libraries/react-native/nitrogen/generated/ios/swift/NativePushToken.swift new file mode 100644 index 000000000..3824bf6c1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/ios/swift/NativePushToken.swift @@ -0,0 +1,57 @@ +/// +/// NativePushToken.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `NativePushToken`, backed by a C++ struct. + */ +public typealias NativePushToken = margelo.nitro.voidhash.NativePushToken + +public extension NativePushToken { + private typealias bridge = margelo.nitro.voidhash.bridge.swift + + /** + * Create a new instance of `NativePushToken`. + */ + init(token: String, provider: NativePushProvider, environment: NativePushEnvironment) { + self.init(std.string(token), provider, environment) + } + + var token: String { + @inline(__always) + get { + return String(self.__token) + } + @inline(__always) + set { + self.__token = std.string(newValue) + } + } + + var provider: NativePushProvider { + @inline(__always) + get { + return self.__provider + } + @inline(__always) + set { + self.__provider = newValue + } + } + + var environment: NativePushEnvironment { + @inline(__always) + get { + return self.__environment + } + @inline(__always) + set { + self.__environment = newValue + } + } +} diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewBaseEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewBaseEvent.swift index a4719b295..89152cfe6 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewBaseEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewBaseEvent.swift @@ -32,7 +32,7 @@ public extension PaywallWebViewBaseEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -43,7 +43,7 @@ public extension PaywallWebViewBaseEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -54,7 +54,7 @@ public extension PaywallWebViewBaseEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -65,7 +65,7 @@ public extension PaywallWebViewBaseEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -76,7 +76,7 @@ public extension PaywallWebViewBaseEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewErrorEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewErrorEvent.swift index 06aa6fc06..d4a5c992f 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewErrorEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewErrorEvent.swift @@ -50,7 +50,7 @@ public extension PaywallWebViewErrorEvent { }() } } - + var code: Double { @inline(__always) get { @@ -61,7 +61,7 @@ public extension PaywallWebViewErrorEvent { self.__code = newValue } } - + var description: String { @inline(__always) get { @@ -72,7 +72,7 @@ public extension PaywallWebViewErrorEvent { self.__description = std.string(newValue) } } - + var url: String { @inline(__always) get { @@ -83,7 +83,7 @@ public extension PaywallWebViewErrorEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -94,7 +94,7 @@ public extension PaywallWebViewErrorEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -105,7 +105,7 @@ public extension PaywallWebViewErrorEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -116,7 +116,7 @@ public extension PaywallWebViewErrorEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -127,7 +127,7 @@ public extension PaywallWebViewErrorEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHeader.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHeader.swift index 6b8f40742..5a50c9efa 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHeader.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHeader.swift @@ -32,7 +32,7 @@ public extension PaywallWebViewHeader { self.__name = std.string(newValue) } } - + var value: String { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHttpErrorEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHttpErrorEvent.swift index bf481b7a5..25f36e769 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHttpErrorEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewHttpErrorEvent.swift @@ -32,7 +32,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__description = std.string(newValue) } } - + var statusCode: Double { @inline(__always) get { @@ -43,7 +43,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__statusCode = newValue } } - + var url: String { @inline(__always) get { @@ -54,7 +54,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -65,7 +65,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -76,7 +76,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -87,7 +87,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -98,7 +98,7 @@ public extension PaywallWebViewHttpErrorEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewMessageEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewMessageEvent.swift index 175863b60..a74cd4dd6 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewMessageEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewMessageEvent.swift @@ -32,7 +32,7 @@ public extension PaywallWebViewMessageEvent { self.__data = std.string(newValue) } } - + var url: String { @inline(__always) get { @@ -43,7 +43,7 @@ public extension PaywallWebViewMessageEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -54,7 +54,7 @@ public extension PaywallWebViewMessageEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -65,7 +65,7 @@ public extension PaywallWebViewMessageEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -76,7 +76,7 @@ public extension PaywallWebViewMessageEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -87,7 +87,7 @@ public extension PaywallWebViewMessageEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewNavigationEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewNavigationEvent.swift index 931b3ee25..5cd9c5d21 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewNavigationEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewNavigationEvent.swift @@ -38,7 +38,7 @@ public extension PaywallWebViewNavigationEvent { self.__navigationType = newValue } } - + var mainDocumentURL: String? { @inline(__always) get { @@ -61,7 +61,7 @@ public extension PaywallWebViewNavigationEvent { }() } } - + var url: String { @inline(__always) get { @@ -72,7 +72,7 @@ public extension PaywallWebViewNavigationEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -83,7 +83,7 @@ public extension PaywallWebViewNavigationEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -94,7 +94,7 @@ public extension PaywallWebViewNavigationEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -105,7 +105,7 @@ public extension PaywallWebViewNavigationEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -116,7 +116,7 @@ public extension PaywallWebViewNavigationEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewProgressEvent.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewProgressEvent.swift index d6231d231..5d9d6efff 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewProgressEvent.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewProgressEvent.swift @@ -32,7 +32,7 @@ public extension PaywallWebViewProgressEvent { self.__progress = newValue } } - + var url: String { @inline(__always) get { @@ -43,7 +43,7 @@ public extension PaywallWebViewProgressEvent { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -54,7 +54,7 @@ public extension PaywallWebViewProgressEvent { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -65,7 +65,7 @@ public extension PaywallWebViewProgressEvent { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -76,7 +76,7 @@ public extension PaywallWebViewProgressEvent { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -87,7 +87,7 @@ public extension PaywallWebViewProgressEvent { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewShouldStartLoadRequest.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewShouldStartLoadRequest.swift index bc8be5d9b..2b49fbe1d 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewShouldStartLoadRequest.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewShouldStartLoadRequest.swift @@ -38,7 +38,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__isTopFrame = newValue } } - + var navigationType: PaywallWebViewNavigationType { @inline(__always) get { @@ -49,7 +49,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__navigationType = newValue } } - + var mainDocumentURL: String? { @inline(__always) get { @@ -72,7 +72,7 @@ public extension PaywallWebViewShouldStartLoadRequest { }() } } - + var url: String { @inline(__always) get { @@ -83,7 +83,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__url = std.string(newValue) } } - + var loading: Bool { @inline(__always) get { @@ -94,7 +94,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__loading = newValue } } - + var title: String { @inline(__always) get { @@ -105,7 +105,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__title = std.string(newValue) } } - + var canGoBack: Bool { @inline(__always) get { @@ -116,7 +116,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__canGoBack = newValue } } - + var canGoForward: Bool { @inline(__always) get { @@ -127,7 +127,7 @@ public extension PaywallWebViewShouldStartLoadRequest { self.__canGoForward = newValue } } - + var lockIdentifier: Double { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewSource.swift b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewSource.swift index 0249e4190..8af13b62f 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewSource.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/PaywallWebViewSource.swift @@ -86,7 +86,7 @@ public extension PaywallWebViewSource { }() } } - + var method: String? { @inline(__always) get { @@ -109,7 +109,7 @@ public extension PaywallWebViewSource { }() } } - + var body: String? { @inline(__always) get { @@ -132,7 +132,7 @@ public extension PaywallWebViewSource { }() } } - + var headers: [PaywallWebViewHeader]? { @inline(__always) get { @@ -161,7 +161,7 @@ public extension PaywallWebViewSource { }() } } - + var html: String? { @inline(__always) get { @@ -184,7 +184,7 @@ public extension PaywallWebViewSource { }() } } - + var baseUrl: String? { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/ios/swift/StorekitProductPurchaseOffer.swift b/libraries/react-native/nitrogen/generated/ios/swift/StorekitProductPurchaseOffer.swift index a0f8aa702..bad0a29eb 100644 --- a/libraries/react-native/nitrogen/generated/ios/swift/StorekitProductPurchaseOffer.swift +++ b/libraries/react-native/nitrogen/generated/ios/swift/StorekitProductPurchaseOffer.swift @@ -32,7 +32,7 @@ public extension StorekitProductPurchaseOffer { self.__id = std.string(newValue) } } - + var type: Double { @inline(__always) get { @@ -43,7 +43,7 @@ public extension StorekitProductPurchaseOffer { self.__type = newValue } } - + var paymentMode: String { @inline(__always) get { diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingAcknowledgeResultSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingAcknowledgeResultSpec.hpp index 221acc056..5ca53e6cc 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingAcknowledgeResultSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingAcknowledgeResultSpec.hpp @@ -52,7 +52,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingConsumeResultSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingConsumeResultSpec.hpp index 8f1270c7b..c84084ed1 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingConsumeResultSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingConsumeResultSpec.hpp @@ -53,7 +53,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp index 3ca6c92c4..2df920d20 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingOneTimePurchaseOfferDetailsSpec.hpp @@ -50,7 +50,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhaseSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhaseSpec.hpp index 78d66e085..2c47a150f 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhaseSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhaseSpec.hpp @@ -53,7 +53,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhasesSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhasesSpec.hpp index dfb0b2a69..d42fd03fa 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhasesSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPricingPhasesSpec.hpp @@ -51,7 +51,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingProductDetailSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingProductDetailSpec.hpp index 9f4c6a674..419d1267f 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingProductDetailSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingProductDetailSpec.hpp @@ -65,7 +65,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPurchaseSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPurchaseSpec.hpp index 62a3d3318..838a34612 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPurchaseSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingPurchaseSpec.hpp @@ -63,7 +63,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSpec.hpp index d2e4dc365..ea4cf28eb 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSpec.hpp @@ -63,7 +63,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSubscriptionOfferDetailsSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSubscriptionOfferDetailsSpec.hpp index 456826685..91ecc07e5 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSubscriptionOfferDetailsSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridGoogleBillingSubscriptionOfferDetailsSpec.hpp @@ -57,7 +57,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.cpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.cpp new file mode 100644 index 000000000..25a1a6252 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.cpp @@ -0,0 +1,44 @@ +/// +/// HybridMeasurementSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "HybridMeasurementSpec.hpp" + +namespace margelo::nitro::voidhash { + + void HybridMeasurementSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("initialize", &HybridMeasurementSpec::initialize); + prototype.registerHybridMethod("enqueue", &HybridMeasurementSpec::enqueue); + prototype.registerHybridMethod("flush", &HybridMeasurementSpec::flush); + prototype.registerHybridMethod("getInstallationId", &HybridMeasurementSpec::getInstallationId); + prototype.registerHybridMethod("getState", &HybridMeasurementSpec::getState); + prototype.registerHybridMethod("subscribe", &HybridMeasurementSpec::subscribe); + prototype.registerHybridMethod("unsubscribe", &HybridMeasurementSpec::unsubscribe); + prototype.registerHybridMethod("peekInbox", &HybridMeasurementSpec::peekInbox); + prototype.registerHybridMethod("acknowledgeInbox", &HybridMeasurementSpec::acknowledgeInbox); + prototype.registerHybridMethod("readProtectedEvidence", &HybridMeasurementSpec::readProtectedEvidence); + prototype.registerHybridMethod("putProtectedEvidence", &HybridMeasurementSpec::putProtectedEvidence); + prototype.registerHybridMethod("deleteProtectedEvidence", &HybridMeasurementSpec::deleteProtectedEvidence); + prototype.registerHybridMethod("deleteProtectedData", &HybridMeasurementSpec::deleteProtectedData); + prototype.registerHybridMethod("getMeasurementConfigurationState", &HybridMeasurementSpec::getMeasurementConfigurationState); + prototype.registerHybridMethod("persistMeasurementConfigurationState", &HybridMeasurementSpec::persistMeasurementConfigurationState); + prototype.registerHybridMethod("applyMeasurementConfiguration", &HybridMeasurementSpec::applyMeasurementConfiguration); + prototype.registerHybridMethod("applyMeasurementStorageLimits", &HybridMeasurementSpec::applyMeasurementStorageLimits); + prototype.registerHybridMethod("getPushRegistrationState", &HybridMeasurementSpec::getPushRegistrationState); + prototype.registerHybridMethod("persistPushRegistrationState", &HybridMeasurementSpec::persistPushRegistrationState); + prototype.registerHybridMethod("clearPushRegistrationState", &HybridMeasurementSpec::clearPushRegistrationState); + prototype.registerHybridMethod("getTestDeviceState", &HybridMeasurementSpec::getTestDeviceState); + prototype.registerHybridMethod("persistTestDeviceState", &HybridMeasurementSpec::persistTestDeviceState); + prototype.registerHybridMethod("hasDedupe", &HybridMeasurementSpec::hasDedupe); + prototype.registerHybridMethod("checkAndSetDedupe", &HybridMeasurementSpec::checkAndSetDedupe); + }); + } + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.hpp new file mode 100644 index 000000000..52e833536 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridMeasurementSpec.hpp @@ -0,0 +1,117 @@ +/// +/// HybridMeasurementSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `MeasurementStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementStateBridge; } +// Forward declaration of `MeasurementInitializeConfiguration` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInitializeConfiguration; } +// Forward declaration of `MeasurementCommandResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommandResult; } +// Forward declaration of `MeasurementCommand` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementCommand; } +// Forward declaration of `MeasurementFlushBridgeResult` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementFlushBridgeResult; } +// Forward declaration of `MeasurementBridgeEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeEvent; } +// Forward declaration of `MeasurementInboxEntry` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementInboxEntry; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `MeasurementProtectedEvidenceInput` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementProtectedEvidenceInput; } +// Forward declaration of `MeasurementConfigurationStateBridge` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConfigurationStateBridge; } + +#include +#include "MeasurementStateBridge.hpp" +#include +#include "MeasurementInitializeConfiguration.hpp" +#include "MeasurementCommandResult.hpp" +#include "MeasurementCommand.hpp" +#include "MeasurementFlushBridgeResult.hpp" +#include +#include "MeasurementBridgeEvent.hpp" +#include +#include "MeasurementInboxEntry.hpp" +#include +#include "MeasurementProtectedEvidenceInput.hpp" +#include "MeasurementConfigurationStateBridge.hpp" + +namespace margelo::nitro::voidhash { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Measurement` + * Inherit this class to create instances of `HybridMeasurementSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridMeasurement: public HybridMeasurementSpec { + * public: + * HybridMeasurement(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridMeasurementSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridMeasurementSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridMeasurementSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> initialize(const std::string& publishableKey, const MeasurementInitializeConfiguration& configuration) = 0; + virtual std::shared_ptr> enqueue(const MeasurementCommand& command) = 0; + virtual std::shared_ptr> flush() = 0; + virtual std::shared_ptr> getInstallationId() = 0; + virtual std::shared_ptr> getState() = 0; + virtual void subscribe(const std::string& subscriptionId, const std::function& listener) = 0; + virtual void unsubscribe(const std::string& subscriptionId) = 0; + virtual std::shared_ptr>> peekInbox(double limit) = 0; + virtual std::shared_ptr> acknowledgeInbox(const std::string& entryId) = 0; + virtual std::shared_ptr>> readProtectedEvidence(const std::string& blobId) = 0; + virtual std::shared_ptr> putProtectedEvidence(const MeasurementProtectedEvidenceInput& input) = 0; + virtual std::shared_ptr> deleteProtectedEvidence(const std::string& blobId) = 0; + virtual std::shared_ptr> deleteProtectedData(const std::string& requestId) = 0; + virtual std::shared_ptr> getMeasurementConfigurationState() = 0; + virtual std::shared_ptr> persistMeasurementConfigurationState(double version, const std::shared_ptr& payload) = 0; + virtual std::shared_ptr> applyMeasurementConfiguration(double version, const std::shared_ptr& payload) = 0; + virtual std::shared_ptr> applyMeasurementStorageLimits(double maxOutboxRecords, double maxOutboxBytes, double maxProtectedBytes) = 0; + virtual std::shared_ptr> getPushRegistrationState() = 0; + virtual std::shared_ptr> persistPushRegistrationState(const std::shared_ptr& payload) = 0; + virtual std::shared_ptr> clearPushRegistrationState() = 0; + virtual std::shared_ptr> getTestDeviceState() = 0; + virtual std::shared_ptr> persistTestDeviceState(bool enabled) = 0; + virtual std::shared_ptr> hasDedupe(const std::string& namespace, const std::string& key) = 0; + virtual std::shared_ptr> checkAndSetDedupe(const std::string& namespace, const std::string& key, double expiresAtMs) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Measurement"; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.cpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.cpp new file mode 100644 index 000000000..c4fa0a744 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.cpp @@ -0,0 +1,26 @@ +/// +/// HybridNotificationsSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#include "HybridNotificationsSpec.hpp" + +namespace margelo::nitro::voidhash { + + void HybridNotificationsSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("getPermissionStatus", &HybridNotificationsSpec::getPermissionStatus); + prototype.registerHybridMethod("requestPermission", &HybridNotificationsSpec::requestPermission); + prototype.registerHybridMethod("getToken", &HybridNotificationsSpec::getToken); + prototype.registerHybridMethod("setBadgeCount", &HybridNotificationsSpec::setBadgeCount); + prototype.registerHybridMethod("subscribe", &HybridNotificationsSpec::subscribe); + prototype.registerHybridMethod("unsubscribe", &HybridNotificationsSpec::unsubscribe); + }); + } + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.hpp new file mode 100644 index 000000000..aa2f2c8a5 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridNotificationsSpec.hpp @@ -0,0 +1,74 @@ +/// +/// HybridNotificationsSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `NativePushToken` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativePushToken; } +// Forward declaration of `NativeNotificationEvent` to properly resolve imports. +namespace margelo::nitro::voidhash { struct NativeNotificationEvent; } + +#include +#include +#include "NativePushToken.hpp" +#include +#include "NativeNotificationEvent.hpp" + +namespace margelo::nitro::voidhash { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Notifications` + * Inherit this class to create instances of `HybridNotificationsSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridNotifications: public HybridNotificationsSpec { + * public: + * HybridNotifications(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridNotificationsSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridNotificationsSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridNotificationsSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> getPermissionStatus() = 0; + virtual std::shared_ptr> requestPermission(bool provisional) = 0; + virtual std::shared_ptr> getToken() = 0; + virtual std::shared_ptr> setBadgeCount(double count) = 0; + virtual void subscribe(const std::string& subscriptionId, const std::function& listener) = 0; + virtual void unsubscribe(const std::string& subscriptionId) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Notifications"; + }; + +} // namespace margelo::nitro::voidhash diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridPaywallPresenterSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridPaywallPresenterSpec.hpp index 0f0d0807b..0b4fb4492 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridPaywallPresenterSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridPaywallPresenterSpec.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridPurchasedItemSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridPurchasedItemSpec.hpp index e91ac8347..a11557c33 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridPurchasedItemSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridPurchasedItemSpec.hpp @@ -53,7 +53,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductOfferSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductOfferSpec.hpp index 4a8aee184..330df6c65 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductOfferSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductOfferSpec.hpp @@ -58,7 +58,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSpec.hpp index d6bed441d..8d8ceaa9b 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSpec.hpp @@ -61,7 +61,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionPeriodSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionPeriodSpec.hpp index 640ba2017..fd5b3580d 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionPeriodSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionPeriodSpec.hpp @@ -50,7 +50,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionSpec.hpp index cbfa8d210..77f2ed384 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitProductSubscriptionSpec.hpp @@ -59,7 +59,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitSpec.hpp index 4f3cd1337..aa5d8db7d 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitSpec.hpp @@ -54,7 +54,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitTransactionSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitTransactionSpec.hpp index 1162a7a15..b87f16033 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitTransactionSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridStorekitTransactionSpec.hpp @@ -77,7 +77,7 @@ namespace margelo::nitro::voidhash { public: // Methods - + protected: // Hybrid Setup diff --git a/libraries/react-native/nitrogen/generated/shared/c++/HybridVoidhashSpec.hpp b/libraries/react-native/nitrogen/generated/shared/c++/HybridVoidhashSpec.hpp index 615b82d38..4a551de5f 100644 --- a/libraries/react-native/nitrogen/generated/shared/c++/HybridVoidhashSpec.hpp +++ b/libraries/react-native/nitrogen/generated/shared/c++/HybridVoidhashSpec.hpp @@ -48,7 +48,7 @@ namespace margelo::nitro::voidhash { public: // Properties - + public: // Methods diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeError.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeError.hpp new file mode 100644 index 000000000..bf5c1f345 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeError.hpp @@ -0,0 +1,88 @@ +/// +/// MeasurementBridgeError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `MeasurementBridgeSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementBridgeSource; } + +#include +#include "MeasurementBridgeSource.hpp" +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementBridgeError). + */ + struct MeasurementBridgeError { + public: + std::string code SWIFT_PRIVATE; + std::string message SWIFT_PRIVATE; + MeasurementBridgeSource source SWIFT_PRIVATE; + std::optional capability SWIFT_PRIVATE; + std::optional reason SWIFT_PRIVATE; + + public: + MeasurementBridgeError() = default; + explicit MeasurementBridgeError(std::string code, std::string message, MeasurementBridgeSource source, std::optional capability, std::optional reason): code(code), message(message), source(source), capability(capability), reason(reason) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementBridgeError <> JS MeasurementBridgeError (object) + template <> + struct JSIConverter final { + static inline MeasurementBridgeError fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementBridgeError( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "code")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "message")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "source")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "capability")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "reason")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementBridgeError& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "code", JSIConverter::toJSI(runtime, arg.code)); + obj.setProperty(runtime, "message", JSIConverter::toJSI(runtime, arg.message)); + obj.setProperty(runtime, "source", JSIConverter::toJSI(runtime, arg.source)); + obj.setProperty(runtime, "capability", JSIConverter>::toJSI(runtime, arg.capability)); + obj.setProperty(runtime, "reason", JSIConverter>::toJSI(runtime, arg.reason)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "code"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "message"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "source"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "capability"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "reason"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeEvent.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeEvent.hpp new file mode 100644 index 000000000..a3d3cbce2 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeEvent.hpp @@ -0,0 +1,95 @@ +/// +/// MeasurementBridgeEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } + +#include +#include +#include +#include "MeasurementBridgeError.hpp" + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementBridgeEvent). + */ + struct MeasurementBridgeEvent { + public: + std::string subscriptionId SWIFT_PRIVATE; + std::string event SWIFT_PRIVATE; + std::optional recordId SWIFT_PRIVATE; + std::optional requestId SWIFT_PRIVATE; + std::optional> payload SWIFT_PRIVATE; + std::optional error SWIFT_PRIVATE; + + public: + MeasurementBridgeEvent() = default; + explicit MeasurementBridgeEvent(std::string subscriptionId, std::string event, std::optional recordId, std::optional requestId, std::optional> payload, std::optional error): subscriptionId(subscriptionId), event(event), recordId(recordId), requestId(requestId), payload(payload), error(error) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementBridgeEvent <> JS MeasurementBridgeEvent (object) + template <> + struct JSIConverter final { + static inline MeasurementBridgeEvent fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementBridgeEvent( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "subscriptionId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "event")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "recordId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "requestId")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "payload")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "error")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementBridgeEvent& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "subscriptionId", JSIConverter::toJSI(runtime, arg.subscriptionId)); + obj.setProperty(runtime, "event", JSIConverter::toJSI(runtime, arg.event)); + obj.setProperty(runtime, "recordId", JSIConverter>::toJSI(runtime, arg.recordId)); + obj.setProperty(runtime, "requestId", JSIConverter>::toJSI(runtime, arg.requestId)); + obj.setProperty(runtime, "payload", JSIConverter>>::toJSI(runtime, arg.payload)); + obj.setProperty(runtime, "error", JSIConverter>::toJSI(runtime, arg.error)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "subscriptionId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "event"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "recordId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "requestId"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "payload"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "error"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeSource.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeSource.hpp new file mode 100644 index 000000000..6bf949a08 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementBridgeSource.hpp @@ -0,0 +1,82 @@ +/// +/// MeasurementBridgeSource.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementBridgeSource). + */ + enum class MeasurementBridgeSource { + IOS SWIFT_NAME(ios) = 0, + ANDROID SWIFT_NAME(android) = 1, + CORE SWIFT_NAME(core) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementBridgeSource <> JS MeasurementBridgeSource (union) + template <> + struct JSIConverter final { + static inline MeasurementBridgeSource fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("ios"): return MeasurementBridgeSource::IOS; + case hashString("android"): return MeasurementBridgeSource::ANDROID; + case hashString("core"): return MeasurementBridgeSource::CORE; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementBridgeSource - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementBridgeSource arg) { + switch (arg) { + case MeasurementBridgeSource::IOS: return JSIConverter::toJSI(runtime, "ios"); + case MeasurementBridgeSource::ANDROID: return JSIConverter::toJSI(runtime, "android"); + case MeasurementBridgeSource::CORE: return JSIConverter::toJSI(runtime, "core"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementBridgeSource to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("ios"): + case hashString("android"): + case hashString("core"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommand.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommand.hpp new file mode 100644 index 000000000..9c53ee7ed --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommand.hpp @@ -0,0 +1,130 @@ +/// +/// MeasurementCommand.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `MeasurementCommandKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementCommandKind; } +// Forward declaration of `MeasurementRecordSource` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordSource; } +// Forward declaration of `MeasurementRecordPriority` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementRecordPriority; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `MeasurementIdentitySnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementIdentitySnapshot; } +// Forward declaration of `MeasurementConsentSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementConsentSnapshot; } +// Forward declaration of `MeasurementSessionSnapshot` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementSessionSnapshot; } + +#include "MeasurementCommandKind.hpp" +#include +#include "MeasurementRecordSource.hpp" +#include "MeasurementRecordPriority.hpp" +#include +#include +#include "MeasurementIdentitySnapshot.hpp" +#include "MeasurementConsentSnapshot.hpp" +#include "MeasurementSessionSnapshot.hpp" + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementCommand). + */ + struct MeasurementCommand { + public: + MeasurementCommandKind kind SWIFT_PRIVATE; + std::string commandId SWIFT_PRIVATE; + std::string recordType SWIFT_PRIVATE; + std::string occurredAt SWIFT_PRIVATE; + MeasurementRecordSource source SWIFT_PRIVATE; + MeasurementRecordPriority priority SWIFT_PRIVATE; + std::shared_ptr publicPayload SWIFT_PRIVATE; + std::optional protectedEvidenceRef SWIFT_PRIVATE; + std::optional identity SWIFT_PRIVATE; + std::optional consent SWIFT_PRIVATE; + std::optional session SWIFT_PRIVATE; + + public: + MeasurementCommand() = default; + explicit MeasurementCommand(MeasurementCommandKind kind, std::string commandId, std::string recordType, std::string occurredAt, MeasurementRecordSource source, MeasurementRecordPriority priority, std::shared_ptr publicPayload, std::optional protectedEvidenceRef, std::optional identity, std::optional consent, std::optional session): kind(kind), commandId(commandId), recordType(recordType), occurredAt(occurredAt), source(source), priority(priority), publicPayload(publicPayload), protectedEvidenceRef(protectedEvidenceRef), identity(identity), consent(consent), session(session) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementCommand <> JS MeasurementCommand (object) + template <> + struct JSIConverter final { + static inline MeasurementCommand fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementCommand( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "kind")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "commandId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "recordType")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "occurredAt")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "source")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "priority")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "publicPayload")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "protectedEvidenceRef")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "identity")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "consent")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "session")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementCommand& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "kind", JSIConverter::toJSI(runtime, arg.kind)); + obj.setProperty(runtime, "commandId", JSIConverter::toJSI(runtime, arg.commandId)); + obj.setProperty(runtime, "recordType", JSIConverter::toJSI(runtime, arg.recordType)); + obj.setProperty(runtime, "occurredAt", JSIConverter::toJSI(runtime, arg.occurredAt)); + obj.setProperty(runtime, "source", JSIConverter::toJSI(runtime, arg.source)); + obj.setProperty(runtime, "priority", JSIConverter::toJSI(runtime, arg.priority)); + obj.setProperty(runtime, "publicPayload", JSIConverter>::toJSI(runtime, arg.publicPayload)); + obj.setProperty(runtime, "protectedEvidenceRef", JSIConverter>::toJSI(runtime, arg.protectedEvidenceRef)); + obj.setProperty(runtime, "identity", JSIConverter>::toJSI(runtime, arg.identity)); + obj.setProperty(runtime, "consent", JSIConverter>::toJSI(runtime, arg.consent)); + obj.setProperty(runtime, "session", JSIConverter>::toJSI(runtime, arg.session)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "kind"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "commandId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "recordType"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "occurredAt"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "source"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "priority"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "publicPayload"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "protectedEvidenceRef"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "identity"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "consent"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "session"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandKind.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandKind.hpp new file mode 100644 index 000000000..6056d4f20 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandKind.hpp @@ -0,0 +1,110 @@ +/// +/// MeasurementCommandKind.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementCommandKind). + */ + enum class MeasurementCommandKind { + ENQUEUERECORD SWIFT_NAME(enqueuerecord) = 0, + IDENTITYTRANSITION SWIFT_NAME(identitytransition) = 1, + CONSENTTRANSITION SWIFT_NAME(consenttransition) = 2, + SESSIONSIGNAL SWIFT_NAME(sessionsignal) = 3, + COLDLAUNCHINPUT SWIFT_NAME(coldlaunchinput) = 4, + TRANSACTIONDEDUP SWIFT_NAME(transactiondedup) = 5, + LINKINPUT SWIFT_NAME(linkinput) = 6, + PUSHINPUT SWIFT_NAME(pushinput) = 7, + PURCHASEINPUT SWIFT_NAME(purchaseinput) = 8, + IDENTIFIERINPUT SWIFT_NAME(identifierinput) = 9, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementCommandKind <> JS MeasurementCommandKind (union) + template <> + struct JSIConverter final { + static inline MeasurementCommandKind fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("enqueueRecord"): return MeasurementCommandKind::ENQUEUERECORD; + case hashString("identityTransition"): return MeasurementCommandKind::IDENTITYTRANSITION; + case hashString("consentTransition"): return MeasurementCommandKind::CONSENTTRANSITION; + case hashString("sessionSignal"): return MeasurementCommandKind::SESSIONSIGNAL; + case hashString("coldLaunchInput"): return MeasurementCommandKind::COLDLAUNCHINPUT; + case hashString("transactionDedup"): return MeasurementCommandKind::TRANSACTIONDEDUP; + case hashString("linkInput"): return MeasurementCommandKind::LINKINPUT; + case hashString("pushInput"): return MeasurementCommandKind::PUSHINPUT; + case hashString("purchaseInput"): return MeasurementCommandKind::PURCHASEINPUT; + case hashString("identifierInput"): return MeasurementCommandKind::IDENTIFIERINPUT; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementCommandKind - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementCommandKind arg) { + switch (arg) { + case MeasurementCommandKind::ENQUEUERECORD: return JSIConverter::toJSI(runtime, "enqueueRecord"); + case MeasurementCommandKind::IDENTITYTRANSITION: return JSIConverter::toJSI(runtime, "identityTransition"); + case MeasurementCommandKind::CONSENTTRANSITION: return JSIConverter::toJSI(runtime, "consentTransition"); + case MeasurementCommandKind::SESSIONSIGNAL: return JSIConverter::toJSI(runtime, "sessionSignal"); + case MeasurementCommandKind::COLDLAUNCHINPUT: return JSIConverter::toJSI(runtime, "coldLaunchInput"); + case MeasurementCommandKind::TRANSACTIONDEDUP: return JSIConverter::toJSI(runtime, "transactionDedup"); + case MeasurementCommandKind::LINKINPUT: return JSIConverter::toJSI(runtime, "linkInput"); + case MeasurementCommandKind::PUSHINPUT: return JSIConverter::toJSI(runtime, "pushInput"); + case MeasurementCommandKind::PURCHASEINPUT: return JSIConverter::toJSI(runtime, "purchaseInput"); + case MeasurementCommandKind::IDENTIFIERINPUT: return JSIConverter::toJSI(runtime, "identifierInput"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementCommandKind to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("enqueueRecord"): + case hashString("identityTransition"): + case hashString("consentTransition"): + case hashString("sessionSignal"): + case hashString("coldLaunchInput"): + case hashString("transactionDedup"): + case hashString("linkInput"): + case hashString("pushInput"): + case hashString("purchaseInput"): + case hashString("identifierInput"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandResult.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandResult.hpp new file mode 100644 index 000000000..118a6d41e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementCommandResult.hpp @@ -0,0 +1,84 @@ +/// +/// MeasurementCommandResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `MeasurementBridgeError` to properly resolve imports. +namespace margelo::nitro::voidhash { struct MeasurementBridgeError; } + +#include +#include +#include "MeasurementBridgeError.hpp" + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementCommandResult). + */ + struct MeasurementCommandResult { + public: + bool accepted SWIFT_PRIVATE; + std::optional recordId SWIFT_PRIVATE; + std::optional installationSequence SWIFT_PRIVATE; + std::optional error SWIFT_PRIVATE; + + public: + MeasurementCommandResult() = default; + explicit MeasurementCommandResult(bool accepted, std::optional recordId, std::optional installationSequence, std::optional error): accepted(accepted), recordId(recordId), installationSequence(installationSequence), error(error) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementCommandResult <> JS MeasurementCommandResult (object) + template <> + struct JSIConverter final { + static inline MeasurementCommandResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementCommandResult( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "accepted")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "recordId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "installationSequence")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "error")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementCommandResult& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "accepted", JSIConverter::toJSI(runtime, arg.accepted)); + obj.setProperty(runtime, "recordId", JSIConverter>::toJSI(runtime, arg.recordId)); + obj.setProperty(runtime, "installationSequence", JSIConverter>::toJSI(runtime, arg.installationSequence)); + obj.setProperty(runtime, "error", JSIConverter>::toJSI(runtime, arg.error)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "accepted"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "recordId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "installationSequence"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "error"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConfigurationStateBridge.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConfigurationStateBridge.hpp new file mode 100644 index 000000000..2c493c14c --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConfigurationStateBridge.hpp @@ -0,0 +1,75 @@ +/// +/// MeasurementConfigurationStateBridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } + +#include +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementConfigurationStateBridge). + */ + struct MeasurementConfigurationStateBridge { + public: + double version SWIFT_PRIVATE; + std::optional> payload SWIFT_PRIVATE; + + public: + MeasurementConfigurationStateBridge() = default; + explicit MeasurementConfigurationStateBridge(double version, std::optional> payload): version(version), payload(payload) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementConfigurationStateBridge <> JS MeasurementConfigurationStateBridge (object) + template <> + struct JSIConverter final { + static inline MeasurementConfigurationStateBridge fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementConfigurationStateBridge( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "version")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "payload")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementConfigurationStateBridge& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "version", JSIConverter::toJSI(runtime, arg.version)); + obj.setProperty(runtime, "payload", JSIConverter>>::toJSI(runtime, arg.payload)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "version"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "payload"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConsentSnapshot.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConsentSnapshot.hpp new file mode 100644 index 000000000..7e54fa13f --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementConsentSnapshot.hpp @@ -0,0 +1,102 @@ +/// +/// MeasurementConsentSnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementConsentSnapshot). + */ + struct MeasurementConsentSnapshot { + public: + double revision SWIFT_PRIVATE; + std::string decidedAt SWIFT_PRIVATE; + std::string source SWIFT_PRIVATE; + std::optional gdprApplies SWIFT_PRIVATE; + std::optional dataUsage SWIFT_PRIVATE; + std::optional adsPersonalization SWIFT_PRIVATE; + std::optional adStorage SWIFT_PRIVATE; + std::optional collectionOptOut SWIFT_PRIVATE; + std::optional partnerSharingOptOut SWIFT_PRIVATE; + + public: + MeasurementConsentSnapshot() = default; + explicit MeasurementConsentSnapshot(double revision, std::string decidedAt, std::string source, std::optional gdprApplies, std::optional dataUsage, std::optional adsPersonalization, std::optional adStorage, std::optional collectionOptOut, std::optional partnerSharingOptOut): revision(revision), decidedAt(decidedAt), source(source), gdprApplies(gdprApplies), dataUsage(dataUsage), adsPersonalization(adsPersonalization), adStorage(adStorage), collectionOptOut(collectionOptOut), partnerSharingOptOut(partnerSharingOptOut) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementConsentSnapshot <> JS MeasurementConsentSnapshot (object) + template <> + struct JSIConverter final { + static inline MeasurementConsentSnapshot fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementConsentSnapshot( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "revision")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "decidedAt")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "source")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "gdprApplies")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "dataUsage")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "adsPersonalization")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "adStorage")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "collectionOptOut")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "partnerSharingOptOut")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementConsentSnapshot& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "revision", JSIConverter::toJSI(runtime, arg.revision)); + obj.setProperty(runtime, "decidedAt", JSIConverter::toJSI(runtime, arg.decidedAt)); + obj.setProperty(runtime, "source", JSIConverter::toJSI(runtime, arg.source)); + obj.setProperty(runtime, "gdprApplies", JSIConverter>::toJSI(runtime, arg.gdprApplies)); + obj.setProperty(runtime, "dataUsage", JSIConverter>::toJSI(runtime, arg.dataUsage)); + obj.setProperty(runtime, "adsPersonalization", JSIConverter>::toJSI(runtime, arg.adsPersonalization)); + obj.setProperty(runtime, "adStorage", JSIConverter>::toJSI(runtime, arg.adStorage)); + obj.setProperty(runtime, "collectionOptOut", JSIConverter>::toJSI(runtime, arg.collectionOptOut)); + obj.setProperty(runtime, "partnerSharingOptOut", JSIConverter>::toJSI(runtime, arg.partnerSharingOptOut)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "revision"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "decidedAt"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "source"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "gdprApplies"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "dataUsage"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "adsPersonalization"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "adStorage"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "collectionOptOut"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "partnerSharingOptOut"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementFlushBridgeResult.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementFlushBridgeResult.hpp new file mode 100644 index 000000000..95e7d613b --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementFlushBridgeResult.hpp @@ -0,0 +1,81 @@ +/// +/// MeasurementFlushBridgeResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + + + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementFlushBridgeResult). + */ + struct MeasurementFlushBridgeResult { + public: + double accepted SWIFT_PRIVATE; + double scheduled SWIFT_PRIVATE; + double quarantined SWIFT_PRIVATE; + double policyBlocked SWIFT_PRIVATE; + + public: + MeasurementFlushBridgeResult() = default; + explicit MeasurementFlushBridgeResult(double accepted, double scheduled, double quarantined, double policyBlocked): accepted(accepted), scheduled(scheduled), quarantined(quarantined), policyBlocked(policyBlocked) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementFlushBridgeResult <> JS MeasurementFlushBridgeResult (object) + template <> + struct JSIConverter final { + static inline MeasurementFlushBridgeResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementFlushBridgeResult( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "accepted")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "scheduled")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "quarantined")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "policyBlocked")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementFlushBridgeResult& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "accepted", JSIConverter::toJSI(runtime, arg.accepted)); + obj.setProperty(runtime, "scheduled", JSIConverter::toJSI(runtime, arg.scheduled)); + obj.setProperty(runtime, "quarantined", JSIConverter::toJSI(runtime, arg.quarantined)); + obj.setProperty(runtime, "policyBlocked", JSIConverter::toJSI(runtime, arg.policyBlocked)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "accepted"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "scheduled"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "quarantined"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "policyBlocked"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementIdentitySnapshot.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementIdentitySnapshot.hpp new file mode 100644 index 000000000..d9488cc1b --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementIdentitySnapshot.hpp @@ -0,0 +1,82 @@ +/// +/// MeasurementIdentitySnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementIdentitySnapshot). + */ + struct MeasurementIdentitySnapshot { + public: + std::string distinctId SWIFT_PRIVATE; + std::optional anonymousId SWIFT_PRIVATE; + std::optional personId SWIFT_PRIVATE; + double revision SWIFT_PRIVATE; + + public: + MeasurementIdentitySnapshot() = default; + explicit MeasurementIdentitySnapshot(std::string distinctId, std::optional anonymousId, std::optional personId, double revision): distinctId(distinctId), anonymousId(anonymousId), personId(personId), revision(revision) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementIdentitySnapshot <> JS MeasurementIdentitySnapshot (object) + template <> + struct JSIConverter final { + static inline MeasurementIdentitySnapshot fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementIdentitySnapshot( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "distinctId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "anonymousId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "personId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "revision")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementIdentitySnapshot& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "distinctId", JSIConverter::toJSI(runtime, arg.distinctId)); + obj.setProperty(runtime, "anonymousId", JSIConverter>::toJSI(runtime, arg.anonymousId)); + obj.setProperty(runtime, "personId", JSIConverter>::toJSI(runtime, arg.personId)); + obj.setProperty(runtime, "revision", JSIConverter::toJSI(runtime, arg.revision)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "distinctId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "anonymousId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "personId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "revision"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInboxEntry.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInboxEntry.hpp new file mode 100644 index 000000000..9a985f5c1 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInboxEntry.hpp @@ -0,0 +1,89 @@ +/// +/// MeasurementInboxEntry.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementInboxEntry). + */ + struct MeasurementInboxEntry { + public: + std::string id SWIFT_PRIVATE; + std::string kind SWIFT_PRIVATE; + std::string source SWIFT_PRIVATE; + std::string appState SWIFT_PRIVATE; + std::string receivedAt SWIFT_PRIVATE; + std::string protectedEvidenceRef SWIFT_PRIVATE; + + public: + MeasurementInboxEntry() = default; + explicit MeasurementInboxEntry(std::string id, std::string kind, std::string source, std::string appState, std::string receivedAt, std::string protectedEvidenceRef): id(id), kind(kind), source(source), appState(appState), receivedAt(receivedAt), protectedEvidenceRef(protectedEvidenceRef) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementInboxEntry <> JS MeasurementInboxEntry (object) + template <> + struct JSIConverter final { + static inline MeasurementInboxEntry fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementInboxEntry( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "id")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "kind")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "source")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "appState")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "receivedAt")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "protectedEvidenceRef")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementInboxEntry& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "id", JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, "kind", JSIConverter::toJSI(runtime, arg.kind)); + obj.setProperty(runtime, "source", JSIConverter::toJSI(runtime, arg.source)); + obj.setProperty(runtime, "appState", JSIConverter::toJSI(runtime, arg.appState)); + obj.setProperty(runtime, "receivedAt", JSIConverter::toJSI(runtime, arg.receivedAt)); + obj.setProperty(runtime, "protectedEvidenceRef", JSIConverter::toJSI(runtime, arg.protectedEvidenceRef)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "id"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "kind"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "source"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "appState"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "receivedAt"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "protectedEvidenceRef"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInitializeConfiguration.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInitializeConfiguration.hpp new file mode 100644 index 000000000..faf15c6fd --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementInitializeConfiguration.hpp @@ -0,0 +1,82 @@ +/// +/// MeasurementInitializeConfiguration.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementInitializeConfiguration). + */ + struct MeasurementInitializeConfiguration { + public: + std::string apiUrl SWIFT_PRIVATE; + std::string ingestUrl SWIFT_PRIVATE; + std::string linksUrl SWIFT_PRIVATE; + std::vector trustedConfigKeyIds SWIFT_PRIVATE; + + public: + MeasurementInitializeConfiguration() = default; + explicit MeasurementInitializeConfiguration(std::string apiUrl, std::string ingestUrl, std::string linksUrl, std::vector trustedConfigKeyIds): apiUrl(apiUrl), ingestUrl(ingestUrl), linksUrl(linksUrl), trustedConfigKeyIds(trustedConfigKeyIds) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementInitializeConfiguration <> JS MeasurementInitializeConfiguration (object) + template <> + struct JSIConverter final { + static inline MeasurementInitializeConfiguration fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementInitializeConfiguration( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "apiUrl")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "ingestUrl")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "linksUrl")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "trustedConfigKeyIds")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementInitializeConfiguration& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "apiUrl", JSIConverter::toJSI(runtime, arg.apiUrl)); + obj.setProperty(runtime, "ingestUrl", JSIConverter::toJSI(runtime, arg.ingestUrl)); + obj.setProperty(runtime, "linksUrl", JSIConverter::toJSI(runtime, arg.linksUrl)); + obj.setProperty(runtime, "trustedConfigKeyIds", JSIConverter>::toJSI(runtime, arg.trustedConfigKeyIds)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "apiUrl"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "ingestUrl"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "linksUrl"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "trustedConfigKeyIds"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedEvidenceInput.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedEvidenceInput.hpp new file mode 100644 index 000000000..e3dae4915 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedEvidenceInput.hpp @@ -0,0 +1,93 @@ +/// +/// MeasurementProtectedEvidenceInput.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `MeasurementProtectedPurpose` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedPurpose; } +// Forward declaration of `MeasurementProtectedRetention` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class MeasurementProtectedRetention; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } + +#include +#include "MeasurementProtectedPurpose.hpp" +#include "MeasurementProtectedRetention.hpp" +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementProtectedEvidenceInput). + */ + struct MeasurementProtectedEvidenceInput { + public: + std::string blobId SWIFT_PRIVATE; + MeasurementProtectedPurpose purpose SWIFT_PRIVATE; + double consentRevision SWIFT_PRIVATE; + MeasurementProtectedRetention retentionClass SWIFT_PRIVATE; + std::shared_ptr value SWIFT_PRIVATE; + + public: + MeasurementProtectedEvidenceInput() = default; + explicit MeasurementProtectedEvidenceInput(std::string blobId, MeasurementProtectedPurpose purpose, double consentRevision, MeasurementProtectedRetention retentionClass, std::shared_ptr value): blobId(blobId), purpose(purpose), consentRevision(consentRevision), retentionClass(retentionClass), value(value) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementProtectedEvidenceInput <> JS MeasurementProtectedEvidenceInput (object) + template <> + struct JSIConverter final { + static inline MeasurementProtectedEvidenceInput fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementProtectedEvidenceInput( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "blobId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "purpose")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "consentRevision")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "retentionClass")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "value")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementProtectedEvidenceInput& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "blobId", JSIConverter::toJSI(runtime, arg.blobId)); + obj.setProperty(runtime, "purpose", JSIConverter::toJSI(runtime, arg.purpose)); + obj.setProperty(runtime, "consentRevision", JSIConverter::toJSI(runtime, arg.consentRevision)); + obj.setProperty(runtime, "retentionClass", JSIConverter::toJSI(runtime, arg.retentionClass)); + obj.setProperty(runtime, "value", JSIConverter>::toJSI(runtime, arg.value)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "blobId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "purpose"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "consentRevision"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "retentionClass"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "value"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedPurpose.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedPurpose.hpp new file mode 100644 index 000000000..516447550 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedPurpose.hpp @@ -0,0 +1,106 @@ +/// +/// MeasurementProtectedPurpose.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementProtectedPurpose). + */ + enum class MeasurementProtectedPurpose { + ADVERTISING_IDENTIFIER SWIFT_NAME(advertisingIdentifier) = 0, + DIAGNOSTIC_AUTHORIZATION SWIFT_NAME(diagnosticAuthorization) = 1, + EMAIL SWIFT_NAME(email) = 2, + INSTALL_REFERRER SWIFT_NAME(installReferrer) = 3, + LINK_CAPTURE SWIFT_NAME(linkCapture) = 4, + PARTNER_CONTEXT SWIFT_NAME(partnerContext) = 5, + PHONE SWIFT_NAME(phone) = 6, + PURCHASE_RECEIPT SWIFT_NAME(purchaseReceipt) = 7, + PUSH_TOKEN SWIFT_NAME(pushToken) = 8, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementProtectedPurpose <> JS MeasurementProtectedPurpose (union) + template <> + struct JSIConverter final { + static inline MeasurementProtectedPurpose fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("advertising-identifier"): return MeasurementProtectedPurpose::ADVERTISING_IDENTIFIER; + case hashString("diagnostic-authorization"): return MeasurementProtectedPurpose::DIAGNOSTIC_AUTHORIZATION; + case hashString("email"): return MeasurementProtectedPurpose::EMAIL; + case hashString("install-referrer"): return MeasurementProtectedPurpose::INSTALL_REFERRER; + case hashString("link-capture"): return MeasurementProtectedPurpose::LINK_CAPTURE; + case hashString("partner-context"): return MeasurementProtectedPurpose::PARTNER_CONTEXT; + case hashString("phone"): return MeasurementProtectedPurpose::PHONE; + case hashString("purchase-receipt"): return MeasurementProtectedPurpose::PURCHASE_RECEIPT; + case hashString("push-token"): return MeasurementProtectedPurpose::PUSH_TOKEN; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementProtectedPurpose - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementProtectedPurpose arg) { + switch (arg) { + case MeasurementProtectedPurpose::ADVERTISING_IDENTIFIER: return JSIConverter::toJSI(runtime, "advertising-identifier"); + case MeasurementProtectedPurpose::DIAGNOSTIC_AUTHORIZATION: return JSIConverter::toJSI(runtime, "diagnostic-authorization"); + case MeasurementProtectedPurpose::EMAIL: return JSIConverter::toJSI(runtime, "email"); + case MeasurementProtectedPurpose::INSTALL_REFERRER: return JSIConverter::toJSI(runtime, "install-referrer"); + case MeasurementProtectedPurpose::LINK_CAPTURE: return JSIConverter::toJSI(runtime, "link-capture"); + case MeasurementProtectedPurpose::PARTNER_CONTEXT: return JSIConverter::toJSI(runtime, "partner-context"); + case MeasurementProtectedPurpose::PHONE: return JSIConverter::toJSI(runtime, "phone"); + case MeasurementProtectedPurpose::PURCHASE_RECEIPT: return JSIConverter::toJSI(runtime, "purchase-receipt"); + case MeasurementProtectedPurpose::PUSH_TOKEN: return JSIConverter::toJSI(runtime, "push-token"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementProtectedPurpose to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("advertising-identifier"): + case hashString("diagnostic-authorization"): + case hashString("email"): + case hashString("install-referrer"): + case hashString("link-capture"): + case hashString("partner-context"): + case hashString("phone"): + case hashString("purchase-receipt"): + case hashString("push-token"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedRetention.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedRetention.hpp new file mode 100644 index 000000000..9cc7857a5 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementProtectedRetention.hpp @@ -0,0 +1,86 @@ +/// +/// MeasurementProtectedRetention.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementProtectedRetention). + */ + enum class MeasurementProtectedRetention { + EPHEMERAL SWIFT_NAME(ephemeral) = 0, + INSTALLATION SWIFT_NAME(installation) = 1, + LEGAL SWIFT_NAME(legal) = 2, + TRANSACTION SWIFT_NAME(transaction) = 3, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementProtectedRetention <> JS MeasurementProtectedRetention (union) + template <> + struct JSIConverter final { + static inline MeasurementProtectedRetention fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("ephemeral"): return MeasurementProtectedRetention::EPHEMERAL; + case hashString("installation"): return MeasurementProtectedRetention::INSTALLATION; + case hashString("legal"): return MeasurementProtectedRetention::LEGAL; + case hashString("transaction"): return MeasurementProtectedRetention::TRANSACTION; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementProtectedRetention - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementProtectedRetention arg) { + switch (arg) { + case MeasurementProtectedRetention::EPHEMERAL: return JSIConverter::toJSI(runtime, "ephemeral"); + case MeasurementProtectedRetention::INSTALLATION: return JSIConverter::toJSI(runtime, "installation"); + case MeasurementProtectedRetention::LEGAL: return JSIConverter::toJSI(runtime, "legal"); + case MeasurementProtectedRetention::TRANSACTION: return JSIConverter::toJSI(runtime, "transaction"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementProtectedRetention to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("ephemeral"): + case hashString("installation"): + case hashString("legal"): + case hashString("transaction"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordPriority.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordPriority.hpp new file mode 100644 index 000000000..3d3c565f3 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordPriority.hpp @@ -0,0 +1,86 @@ +/// +/// MeasurementRecordPriority.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementRecordPriority). + */ + enum class MeasurementRecordPriority { + CRITICAL SWIFT_NAME(critical) = 0, + HIGH SWIFT_NAME(high) = 1, + NORMAL SWIFT_NAME(normal) = 2, + LOW SWIFT_NAME(low) = 3, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementRecordPriority <> JS MeasurementRecordPriority (union) + template <> + struct JSIConverter final { + static inline MeasurementRecordPriority fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("critical"): return MeasurementRecordPriority::CRITICAL; + case hashString("high"): return MeasurementRecordPriority::HIGH; + case hashString("normal"): return MeasurementRecordPriority::NORMAL; + case hashString("low"): return MeasurementRecordPriority::LOW; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementRecordPriority - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementRecordPriority arg) { + switch (arg) { + case MeasurementRecordPriority::CRITICAL: return JSIConverter::toJSI(runtime, "critical"); + case MeasurementRecordPriority::HIGH: return JSIConverter::toJSI(runtime, "high"); + case MeasurementRecordPriority::NORMAL: return JSIConverter::toJSI(runtime, "normal"); + case MeasurementRecordPriority::LOW: return JSIConverter::toJSI(runtime, "low"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementRecordPriority to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("critical"): + case hashString("high"): + case hashString("normal"): + case hashString("low"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordSource.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordSource.hpp new file mode 100644 index 000000000..d2c81498d --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementRecordSource.hpp @@ -0,0 +1,90 @@ +/// +/// MeasurementRecordSource.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (MeasurementRecordSource). + */ + enum class MeasurementRecordSource { + NATIVE SWIFT_NAME(native) = 0, + JAVASCRIPT SWIFT_NAME(javascript) = 1, + STORE SWIFT_NAME(store) = 2, + PUSH SWIFT_NAME(push) = 3, + SERVER_CORRELATION SWIFT_NAME(serverCorrelation) = 4, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementRecordSource <> JS MeasurementRecordSource (union) + template <> + struct JSIConverter final { + static inline MeasurementRecordSource fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("native"): return MeasurementRecordSource::NATIVE; + case hashString("javascript"): return MeasurementRecordSource::JAVASCRIPT; + case hashString("store"): return MeasurementRecordSource::STORE; + case hashString("push"): return MeasurementRecordSource::PUSH; + case hashString("server-correlation"): return MeasurementRecordSource::SERVER_CORRELATION; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MeasurementRecordSource - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MeasurementRecordSource arg) { + switch (arg) { + case MeasurementRecordSource::NATIVE: return JSIConverter::toJSI(runtime, "native"); + case MeasurementRecordSource::JAVASCRIPT: return JSIConverter::toJSI(runtime, "javascript"); + case MeasurementRecordSource::STORE: return JSIConverter::toJSI(runtime, "store"); + case MeasurementRecordSource::PUSH: return JSIConverter::toJSI(runtime, "push"); + case MeasurementRecordSource::SERVER_CORRELATION: return JSIConverter::toJSI(runtime, "server-correlation"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MeasurementRecordSource to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("native"): + case hashString("javascript"): + case hashString("store"): + case hashString("push"): + case hashString("server-correlation"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementSessionSnapshot.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementSessionSnapshot.hpp new file mode 100644 index 000000000..37ac0a319 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementSessionSnapshot.hpp @@ -0,0 +1,81 @@ +/// +/// MeasurementSessionSnapshot.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementSessionSnapshot). + */ + struct MeasurementSessionSnapshot { + public: + std::string id SWIFT_PRIVATE; + double sequence SWIFT_PRIVATE; + std::string startedAt SWIFT_PRIVATE; + std::string reason SWIFT_PRIVATE; + + public: + MeasurementSessionSnapshot() = default; + explicit MeasurementSessionSnapshot(std::string id, double sequence, std::string startedAt, std::string reason): id(id), sequence(sequence), startedAt(startedAt), reason(reason) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementSessionSnapshot <> JS MeasurementSessionSnapshot (object) + template <> + struct JSIConverter final { + static inline MeasurementSessionSnapshot fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementSessionSnapshot( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "id")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "sequence")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "startedAt")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "reason")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementSessionSnapshot& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "id", JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, "sequence", JSIConverter::toJSI(runtime, arg.sequence)); + obj.setProperty(runtime, "startedAt", JSIConverter::toJSI(runtime, arg.startedAt)); + obj.setProperty(runtime, "reason", JSIConverter::toJSI(runtime, arg.reason)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "id"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "sequence"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "startedAt"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "reason"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/MeasurementStateBridge.hpp b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementStateBridge.hpp new file mode 100644 index 000000000..ee3b8ba71 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/MeasurementStateBridge.hpp @@ -0,0 +1,118 @@ +/// +/// MeasurementStateBridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (MeasurementStateBridge). + */ + struct MeasurementStateBridge { + public: + std::string installationId SWIFT_PRIVATE; + std::string firstOpenedAt SWIFT_PRIVATE; + double installationSequence SWIFT_PRIVATE; + std::string readiness SWIFT_PRIVATE; + std::optional currentSessionId SWIFT_PRIVATE; + std::optional currentSessionSequence SWIFT_PRIVATE; + double consentRevision SWIFT_PRIVATE; + double configurationRevision SWIFT_PRIVATE; + double outboxCritical SWIFT_PRIVATE; + double outboxHigh SWIFT_PRIVATE; + double outboxNormal SWIFT_PRIVATE; + double outboxLow SWIFT_PRIVATE; + std::optional oldestRecordAgeMs SWIFT_PRIVATE; + + public: + MeasurementStateBridge() = default; + explicit MeasurementStateBridge(std::string installationId, std::string firstOpenedAt, double installationSequence, std::string readiness, std::optional currentSessionId, std::optional currentSessionSequence, double consentRevision, double configurationRevision, double outboxCritical, double outboxHigh, double outboxNormal, double outboxLow, std::optional oldestRecordAgeMs): installationId(installationId), firstOpenedAt(firstOpenedAt), installationSequence(installationSequence), readiness(readiness), currentSessionId(currentSessionId), currentSessionSequence(currentSessionSequence), consentRevision(consentRevision), configurationRevision(configurationRevision), outboxCritical(outboxCritical), outboxHigh(outboxHigh), outboxNormal(outboxNormal), outboxLow(outboxLow), oldestRecordAgeMs(oldestRecordAgeMs) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ MeasurementStateBridge <> JS MeasurementStateBridge (object) + template <> + struct JSIConverter final { + static inline MeasurementStateBridge fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return MeasurementStateBridge( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "installationId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "firstOpenedAt")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "installationSequence")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "readiness")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "currentSessionId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "currentSessionSequence")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "consentRevision")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "configurationRevision")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "outboxCritical")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "outboxHigh")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "outboxNormal")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "outboxLow")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "oldestRecordAgeMs")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const MeasurementStateBridge& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "installationId", JSIConverter::toJSI(runtime, arg.installationId)); + obj.setProperty(runtime, "firstOpenedAt", JSIConverter::toJSI(runtime, arg.firstOpenedAt)); + obj.setProperty(runtime, "installationSequence", JSIConverter::toJSI(runtime, arg.installationSequence)); + obj.setProperty(runtime, "readiness", JSIConverter::toJSI(runtime, arg.readiness)); + obj.setProperty(runtime, "currentSessionId", JSIConverter>::toJSI(runtime, arg.currentSessionId)); + obj.setProperty(runtime, "currentSessionSequence", JSIConverter>::toJSI(runtime, arg.currentSessionSequence)); + obj.setProperty(runtime, "consentRevision", JSIConverter::toJSI(runtime, arg.consentRevision)); + obj.setProperty(runtime, "configurationRevision", JSIConverter::toJSI(runtime, arg.configurationRevision)); + obj.setProperty(runtime, "outboxCritical", JSIConverter::toJSI(runtime, arg.outboxCritical)); + obj.setProperty(runtime, "outboxHigh", JSIConverter::toJSI(runtime, arg.outboxHigh)); + obj.setProperty(runtime, "outboxNormal", JSIConverter::toJSI(runtime, arg.outboxNormal)); + obj.setProperty(runtime, "outboxLow", JSIConverter::toJSI(runtime, arg.outboxLow)); + obj.setProperty(runtime, "oldestRecordAgeMs", JSIConverter>::toJSI(runtime, arg.oldestRecordAgeMs)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "installationId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "firstOpenedAt"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "installationSequence"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "readiness"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "currentSessionId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "currentSessionSequence"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "consentRevision"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "configurationRevision"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "outboxCritical"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "outboxHigh"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "outboxNormal"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "outboxLow"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "oldestRecordAgeMs"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEvent.hpp b/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEvent.hpp new file mode 100644 index 000000000..1912098f2 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEvent.hpp @@ -0,0 +1,96 @@ +/// +/// NativeNotificationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `NativeNotificationEventKind` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativeNotificationEventKind; } + +#include +#include "NativeNotificationEventKind.hpp" +#include + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (NativeNotificationEvent). + */ + struct NativeNotificationEvent { + public: + std::string id SWIFT_PRIVATE; + NativeNotificationEventKind kind SWIFT_PRIVATE; + std::string occurredAt SWIFT_PRIVATE; + std::optional protectedPayloadRef SWIFT_PRIVATE; + std::optional pushNotificationSendId SWIFT_PRIVATE; + std::optional link SWIFT_PRIVATE; + std::optional errorCode SWIFT_PRIVATE; + + public: + NativeNotificationEvent() = default; + explicit NativeNotificationEvent(std::string id, NativeNotificationEventKind kind, std::string occurredAt, std::optional protectedPayloadRef, std::optional pushNotificationSendId, std::optional link, std::optional errorCode): id(id), kind(kind), occurredAt(occurredAt), protectedPayloadRef(protectedPayloadRef), pushNotificationSendId(pushNotificationSendId), link(link), errorCode(errorCode) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ NativeNotificationEvent <> JS NativeNotificationEvent (object) + template <> + struct JSIConverter final { + static inline NativeNotificationEvent fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return NativeNotificationEvent( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "id")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "kind")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "occurredAt")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "protectedPayloadRef")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "pushNotificationSendId")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "link")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "errorCode")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const NativeNotificationEvent& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "id", JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, "kind", JSIConverter::toJSI(runtime, arg.kind)); + obj.setProperty(runtime, "occurredAt", JSIConverter::toJSI(runtime, arg.occurredAt)); + obj.setProperty(runtime, "protectedPayloadRef", JSIConverter>::toJSI(runtime, arg.protectedPayloadRef)); + obj.setProperty(runtime, "pushNotificationSendId", JSIConverter>::toJSI(runtime, arg.pushNotificationSendId)); + obj.setProperty(runtime, "link", JSIConverter>::toJSI(runtime, arg.link)); + obj.setProperty(runtime, "errorCode", JSIConverter>::toJSI(runtime, arg.errorCode)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "id"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "kind"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "occurredAt"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "protectedPayloadRef"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "pushNotificationSendId"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "link"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "errorCode"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEventKind.hpp b/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEventKind.hpp new file mode 100644 index 000000000..50506b4eb --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/NativeNotificationEventKind.hpp @@ -0,0 +1,86 @@ +/// +/// NativeNotificationEventKind.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (NativeNotificationEventKind). + */ + enum class NativeNotificationEventKind { + RECEIVED SWIFT_NAME(received) = 0, + OPENED SWIFT_NAME(opened) = 1, + TOKENCHANGED SWIFT_NAME(tokenchanged) = 2, + REGISTRATIONERROR SWIFT_NAME(registrationerror) = 3, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ NativeNotificationEventKind <> JS NativeNotificationEventKind (union) + template <> + struct JSIConverter final { + static inline NativeNotificationEventKind fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("received"): return NativeNotificationEventKind::RECEIVED; + case hashString("opened"): return NativeNotificationEventKind::OPENED; + case hashString("tokenChanged"): return NativeNotificationEventKind::TOKENCHANGED; + case hashString("registrationError"): return NativeNotificationEventKind::REGISTRATIONERROR; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum NativeNotificationEventKind - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, NativeNotificationEventKind arg) { + switch (arg) { + case NativeNotificationEventKind::RECEIVED: return JSIConverter::toJSI(runtime, "received"); + case NativeNotificationEventKind::OPENED: return JSIConverter::toJSI(runtime, "opened"); + case NativeNotificationEventKind::TOKENCHANGED: return JSIConverter::toJSI(runtime, "tokenChanged"); + case NativeNotificationEventKind::REGISTRATIONERROR: return JSIConverter::toJSI(runtime, "registrationError"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert NativeNotificationEventKind to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("received"): + case hashString("opened"): + case hashString("tokenChanged"): + case hashString("registrationError"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/NativePushEnvironment.hpp b/libraries/react-native/nitrogen/generated/shared/c++/NativePushEnvironment.hpp new file mode 100644 index 000000000..d02870e61 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/NativePushEnvironment.hpp @@ -0,0 +1,78 @@ +/// +/// NativePushEnvironment.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (NativePushEnvironment). + */ + enum class NativePushEnvironment { + DEVELOPMENT SWIFT_NAME(development) = 0, + PRODUCTION SWIFT_NAME(production) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ NativePushEnvironment <> JS NativePushEnvironment (union) + template <> + struct JSIConverter final { + static inline NativePushEnvironment fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("development"): return NativePushEnvironment::DEVELOPMENT; + case hashString("production"): return NativePushEnvironment::PRODUCTION; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum NativePushEnvironment - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, NativePushEnvironment arg) { + switch (arg) { + case NativePushEnvironment::DEVELOPMENT: return JSIConverter::toJSI(runtime, "development"); + case NativePushEnvironment::PRODUCTION: return JSIConverter::toJSI(runtime, "production"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert NativePushEnvironment to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("development"): + case hashString("production"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/NativePushProvider.hpp b/libraries/react-native/nitrogen/generated/shared/c++/NativePushProvider.hpp new file mode 100644 index 000000000..2d450ca26 --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/NativePushProvider.hpp @@ -0,0 +1,78 @@ +/// +/// NativePushProvider.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::voidhash { + + /** + * An enum which can be represented as a JavaScript union (NativePushProvider). + */ + enum class NativePushProvider { + APNS SWIFT_NAME(apns) = 0, + FCM SWIFT_NAME(fcm) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ NativePushProvider <> JS NativePushProvider (union) + template <> + struct JSIConverter final { + static inline NativePushProvider fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("apns"): return NativePushProvider::APNS; + case hashString("fcm"): return NativePushProvider::FCM; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum NativePushProvider - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, NativePushProvider arg) { + switch (arg) { + case NativePushProvider::APNS: return JSIConverter::toJSI(runtime, "apns"); + case NativePushProvider::FCM: return JSIConverter::toJSI(runtime, "fcm"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert NativePushProvider to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("apns"): + case hashString("fcm"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/nitrogen/generated/shared/c++/NativePushToken.hpp b/libraries/react-native/nitrogen/generated/shared/c++/NativePushToken.hpp new file mode 100644 index 000000000..d9c87f51e --- /dev/null +++ b/libraries/react-native/nitrogen/generated/shared/c++/NativePushToken.hpp @@ -0,0 +1,82 @@ +/// +/// NativePushToken.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2026 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `NativePushProvider` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushProvider; } +// Forward declaration of `NativePushEnvironment` to properly resolve imports. +namespace margelo::nitro::voidhash { enum class NativePushEnvironment; } + +#include +#include "NativePushProvider.hpp" +#include "NativePushEnvironment.hpp" + +namespace margelo::nitro::voidhash { + + /** + * A struct which can be represented as a JavaScript object (NativePushToken). + */ + struct NativePushToken { + public: + std::string token SWIFT_PRIVATE; + NativePushProvider provider SWIFT_PRIVATE; + NativePushEnvironment environment SWIFT_PRIVATE; + + public: + NativePushToken() = default; + explicit NativePushToken(std::string token, NativePushProvider provider, NativePushEnvironment environment): token(token), provider(provider), environment(environment) {} + }; + +} // namespace margelo::nitro::voidhash + +namespace margelo::nitro { + + using namespace margelo::nitro::voidhash; + + // C++ NativePushToken <> JS NativePushToken (object) + template <> + struct JSIConverter final { + static inline NativePushToken fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return NativePushToken( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "token")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "provider")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "environment")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const NativePushToken& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "token", JSIConverter::toJSI(runtime, arg.token)); + obj.setProperty(runtime, "provider", JSIConverter::toJSI(runtime, arg.provider)); + obj.setProperty(runtime, "environment", JSIConverter::toJSI(runtime, arg.environment)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "token"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "provider"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "environment"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/libraries/react-native/package.json b/libraries/react-native/package.json index 51713c7e2..24d9ee442 100644 --- a/libraries/react-native/package.json +++ b/libraries/react-native/package.json @@ -27,6 +27,7 @@ "*.podspec", "app.plugin.js", "build", + "plugin/build", "nitrogen", "react-native.config.js", "README.md", @@ -41,6 +42,9 @@ "ios/**/*.mm", "ios/**/*.swift" ], + "bin": { + "voidhash-doctor": "plugin/build/doctor-cli.js" + }, "main": "build/index", "module": "build/index", "types": "build/index.d.ts", diff --git a/libraries/react-native/plugin/build/doctor-cli.d.ts b/libraries/react-native/plugin/build/doctor-cli.d.ts new file mode 100644 index 000000000..b7988016d --- /dev/null +++ b/libraries/react-native/plugin/build/doctor-cli.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/libraries/react-native/plugin/build/doctor-cli.js b/libraries/react-native/plugin/build/doctor-cli.js new file mode 100644 index 000000000..e5b1100da --- /dev/null +++ b/libraries/react-native/plugin/build/doctor-cli.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); +const doctor_1 = require("./doctor"); +const root = (0, node_path_1.resolve)(process.cwd()); +const read = (...candidates) => { + const path = candidates.map((candidate) => (0, node_path_1.resolve)(root, candidate)).find(node_fs_1.existsSync); + return path ? (0, node_fs_1.readFileSync)(path, "utf8") : undefined; +}; +const configured = read("voidhash.config.json"); +const options = configured ? JSON.parse(configured) : {}; +const report = (0, doctor_1.diagnoseVoidhashIntegration)({ + options, + androidApplicationSource: read("android/app/src/main/java/MainApplication.kt", "android/app/src/main/java/MainApplication.java"), + androidManifest: read("android/app/src/main/AndroidManifest.xml"), + googleServicesPresent: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, "android/app/google-services.json")), + iosEntitlements: read("ios/Voidhash.entitlements", "ios/App.entitlements"), + iosInfoPlist: read("ios/Info.plist"), +}); +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +process.exitCode = report.ok ? 0 : 1; diff --git a/libraries/react-native/plugin/build/doctor.d.ts b/libraries/react-native/plugin/build/doctor.d.ts new file mode 100644 index 000000000..74f81d774 --- /dev/null +++ b/libraries/react-native/plugin/build/doctor.d.ts @@ -0,0 +1,21 @@ +import type { VoidhashExpoPluginOptions } from "./withVoidhashReactNative"; +export interface DoctorFinding { + readonly code: string; + readonly level: "error" | "warning"; + readonly message: string; +} +export interface DoctorProjectSnapshot { + readonly options: VoidhashExpoPluginOptions; + readonly iosEntitlements?: string; + readonly iosInfoPlist?: string; + readonly androidManifest?: string; + readonly androidApplicationSource?: string; + readonly googleServicesPresent?: boolean; +} +export interface DoctorReport { + readonly ok: boolean; + readonly findings: ReadonlyArray; + readonly capabilities: Readonly>; +} +/** Evaluates native project integration using the same option validator as the Expo plugin. */ +export declare const diagnoseVoidhashIntegration: (snapshot: DoctorProjectSnapshot) => DoctorReport; diff --git a/libraries/react-native/plugin/build/doctor.js b/libraries/react-native/plugin/build/doctor.js new file mode 100644 index 000000000..f97a74fe9 --- /dev/null +++ b/libraries/react-native/plugin/build/doctor.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.diagnoseVoidhashIntegration = void 0; +const withVoidhashReactNative_1 = require("./withVoidhashReactNative"); +/** Evaluates native project integration using the same option validator as the Expo plugin. */ +const diagnoseVoidhashIntegration = (snapshot) => { + const findings = []; + try { + (0, withVoidhashReactNative_1.validateVoidhashExpoPluginOptions)(snapshot.options); + } + catch (error) { + findings.push({ code: "VH_CFG_CONTRADICTION", level: "error", message: error instanceof Error ? error.message : "Invalid configuration" }); + } + const notifications = snapshot.options.notifications; + if (notifications?.enabled && notifications.ios?.apsEnvironment && !snapshot.iosEntitlements?.includes("aps-environment")) { + findings.push({ code: "VH_IOS_APS_ENTITLEMENT_MISSING", level: "error", message: "Add the aps-environment entitlement for push notifications." }); + } + if (notifications?.enabled && !snapshot.googleServicesPresent) { + findings.push({ code: "VH_ANDROID_GOOGLE_SERVICES_MISSING", level: "error", message: "Add google-services.json to the Android application." }); + } + if (notifications?.enabled && !/FirebaseMessagingService|VoidhashPush/i.test(snapshot.androidApplicationSource ?? snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_FCM_HOOK_MISSING", level: "error", message: "Register the FCM service or Voidhash push subscriber." }); + } + if (snapshot.options.measurement?.android?.backupPolicy === "voidhash-no-backup" && !/fullBackupContent|dataExtractionRules|noBackupFilesDir/.test(snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_NO_BACKUP_UNVERIFIED", level: "error", message: "Configure or verify no-backup storage for measurement install state." }); + } + const ios = snapshot.options.measurement?.ios; + if ((ios?.associatedDomains?.length ?? 0) > 0 && !snapshot.iosEntitlements?.includes("com.apple.developer.associated-domains")) { + findings.push({ code: "VH_IOS_ASSOCIATED_DOMAINS_MISSING", level: "error", message: "Add the associated-domains entitlement." }); + } + if (ios?.disableSKAD !== true && !ios?.skAdNetworkPostbackEndpoint) { + findings.push({ code: "VH_IOS_SKAN_ENDPOINT_MISSING", level: "error", message: "Configure the HTTPS SKAdNetwork postback endpoint or explicitly disable SKAdNetwork." }); + } + if (ios?.skAdNetworkPostbackEndpoint && + !snapshot.iosInfoPlist?.includes("NSAdvertisingAttributionReportEndpoint")) { + findings.push({ code: "VH_IOS_SKAN_PLIST_MISSING", level: "error", message: "Regenerate Info.plist with NSAdvertisingAttributionReportEndpoint." }); + } + if (ios?.adAttributionKitPostbackEndpoint && + !snapshot.iosInfoPlist?.includes("AttributionCopyEndpoint")) { + findings.push({ code: "VH_IOS_ADATTRIBUTIONKIT_PLIST_MISSING", level: "error", message: "Regenerate Info.plist with AttributionCopyEndpoint." }); + } + for (const link of snapshot.options.measurement?.android?.appLinks ?? []) { + if (!new RegExp(`android:host=["']${link.host.replace(/\./g, "\\.")}["']`).test(snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_APP_LINK_MISSING", level: "error", message: `Add the verified App Link host ${link.host}.` }); + } + if (link.autoVerify === false) { + findings.push({ code: "VH_ANDROID_APP_LINK_UNVERIFIED", level: "error", message: `Enable Android App Link verification for ${link.host}.` }); + } + } + const capabilities = { + links: { + androidAppLinks: snapshot.options.measurement?.android?.appLinks?.length ?? 0, + iosAssociatedDomains: snapshot.options.measurement?.ios?.associatedDomains?.length ?? 0, + }, + notifications: { enabled: notifications?.enabled ?? false }, + purchases: { + android: snapshot.options.measurement?.android?.purchaseObservation ?? "disabled", + ios: snapshot.options.measurement?.ios?.purchaseObservation ?? "disabled", + }, + privacy: { + androidAdvertisingId: snapshot.options.measurement?.android?.advertisingIdPermission ?? "remove", + ios: snapshot.options.measurement?.ios?.privacyMode ?? "standard", + }, + }; + return { ok: findings.every((finding) => finding.level !== "error"), findings, capabilities }; +}; +exports.diagnoseVoidhashIntegration = diagnoseVoidhashIntegration; diff --git a/libraries/react-native/plugin/build/storeDisclosures.d.ts b/libraries/react-native/plugin/build/storeDisclosures.d.ts new file mode 100644 index 000000000..04ed02dbb --- /dev/null +++ b/libraries/react-native/plugin/build/storeDisclosures.d.ts @@ -0,0 +1,15 @@ +import type { VoidhashExpoPluginOptions } from "./withVoidhashReactNative"; +export interface StoreDisclosureInputs { + readonly apple: { + readonly collectedData: ReadonlyArray; + readonly tracking: boolean; + }; + readonly googlePlay: { + readonly collectedData: ReadonlyArray; + readonly advertisingId: boolean; + readonly deletionSupported: true; + readonly encryptedInTransit: true; + }; +} +/** Derives mobile-store disclosure inputs from the same enabled capability options as the plugin. */ +export declare const generateStoreDisclosureInputs: (options: VoidhashExpoPluginOptions) => StoreDisclosureInputs; diff --git a/libraries/react-native/plugin/build/storeDisclosures.js b/libraries/react-native/plugin/build/storeDisclosures.js new file mode 100644 index 000000000..d52c64334 --- /dev/null +++ b/libraries/react-native/plugin/build/storeDisclosures.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateStoreDisclosureInputs = void 0; +/** Derives mobile-store disclosure inputs from the same enabled capability options as the plugin. */ +const generateStoreDisclosureInputs = (options) => { + const notifications = options.notifications?.enabled === true; + const iosAdvertising = options.measurement?.ios?.privacyMode !== "strict-no-idfa" && + options.measurement?.ios?.requireAdvertisingId === true; + const androidAdvertising = options.measurement?.android?.advertisingIdPermission === "include"; + const common = ["product-interaction", "device-or-other-identifiers"]; + return { + apple: { + collectedData: [...common, ...(notifications ? ["push-token"] : []), ...(iosAdvertising ? ["advertising-identifier"] : [])].sort(), + tracking: iosAdvertising, + }, + googlePlay: { + collectedData: [...common, ...(notifications ? ["push-token"] : []), ...(androidAdvertising ? ["advertising-identifier"] : [])].sort(), + advertisingId: androidAdvertising, + deletionSupported: true, + encryptedInTransit: true, + }, + }; +}; +exports.generateStoreDisclosureInputs = generateStoreDisclosureInputs; diff --git a/libraries/react-native/plugin/build/withVoidhashReactNative.d.ts b/libraries/react-native/plugin/build/withVoidhashReactNative.d.ts index e82456a45..a14c15df4 100644 --- a/libraries/react-native/plugin/build/withVoidhashReactNative.d.ts +++ b/libraries/react-native/plugin/build/withVoidhashReactNative.d.ts @@ -1,3 +1,64 @@ import type { ConfigPlugin } from "expo/config-plugins"; -declare const _default: ConfigPlugin; +export interface VoidhashExpoPluginOptions { + readonly measurement?: { + readonly buildMode?: "development" | "production"; + readonly purchaseValidationEnvironment?: "production" | "sandbox"; + readonly ios?: { + readonly associatedDomains?: ReadonlyArray; + readonly urlSchemes?: ReadonlyArray; + readonly skAdNetworkPostbackEndpoint?: string; + readonly adAttributionKitPostbackEndpoint?: string; + readonly disableSKAD?: boolean; + readonly privacyMode?: "standard" | "strict-no-idfa"; + readonly requireAdvertisingId?: boolean; + readonly purchaseObservation?: "storekit2" | "storekit1" | "disabled"; + }; + readonly android?: { + readonly appLinks?: ReadonlyArray<{ + readonly host: string; + readonly pathPrefix?: string; + readonly autoVerify?: boolean; + }>; + readonly urlSchemes?: ReadonlyArray; + readonly backupPolicy?: "voidhash-no-backup" | "preserve-app-rules"; + readonly advertisingIdPermission?: "include" | "remove"; + readonly requireAdvertisingId?: boolean; + readonly purchaseObservation?: "billing8" | "disabled"; + readonly installReferrers?: ReadonlyArray<"google-play" | "meta" | "samsung" | "huawei" | "xiaomi">; + readonly identifierProviders?: ReadonlyArray<"app-set-id" | "gaid" | "oaid" | "amazon-aaid" | "meta">; + readonly outOfStore?: string; + }; + }; + readonly notifications?: { + readonly enabled?: boolean; + readonly ios?: { + readonly apsEnvironment?: "development" | "production"; + readonly backgroundRemoteNotifications?: boolean; + }; + readonly android?: { + readonly googleServicesFile?: string; + readonly postNotifications?: "include" | "remove"; + readonly defaultChannel?: { + readonly id: string; + readonly name: string; + readonly importance?: "default" | "high" | "low" | "min" | "none"; + }; + }; + }; +} +/** Validates build-time link and notification configuration before mutating a project. */ +export declare const validateVoidhashExpoPluginOptions: (options: VoidhashExpoPluginOptions) => void; +/** Applies deterministic iOS plist values and returns the same plist object. */ +export declare const applyVoidhashIosInfoPlist: >(infoPlist: T, options: VoidhashExpoPluginOptions) => T; +/** Applies the compilation condition used to remove IDFA-linked code from strict builds. */ +export declare const applyVoidhashIosBuildSettings: (buildSettings: Record, options: VoidhashExpoPluginOptions) => Record; +type AndroidManifestShape = { + $?: Record; + "uses-permission"?: Array<{ + $: Record; + }>; +}; +/** Applies explicit notification and advertising-identifier permission policy. */ +export declare const applyVoidhashAndroidPermissions: (manifest: T, options: VoidhashExpoPluginOptions) => T & AndroidManifestShape; +declare const _default: ConfigPlugin; export default _default; diff --git a/libraries/react-native/plugin/build/withVoidhashReactNative.js b/libraries/react-native/plugin/build/withVoidhashReactNative.js index 23747e5da..a2dea7404 100644 --- a/libraries/react-native/plugin/build/withVoidhashReactNative.js +++ b/libraries/react-native/plugin/build/withVoidhashReactNative.js @@ -1,15 +1,310 @@ "use strict"; -const __importDefault = - (this && this.__importDefault) || - function __importDefault(mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); +exports.applyVoidhashAndroidPermissions = exports.applyVoidhashIosBuildSettings = exports.applyVoidhashIosInfoPlist = exports.validateVoidhashExpoPluginOptions = void 0; const config_plugins_1 = require("expo/config-plugins"); +// biome-ignore lint/nursery/useJsonImportAttribute: plugin code is emitted as CommonJS. const package_json_1 = __importDefault(require("../../package.json")); -const withVoidhashReactNative = (config) => config; -exports.default = (0, config_plugins_1.createRunOncePlugin)( - withVoidhashReactNative, - package_json_1.default.name, - package_json_1.default.version, -); +const DOMAIN = /^(?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?\.)+[a-z]{2,63}$/i; +const SCHEME = /^[a-z][a-z\d+.-]*$/i; +const validateApplePostbackEndpoint = (value, field) => { + let url; + try { + url = new URL(value); + } + catch { + throw new Error(`Voidhash: ${field} must be an absolute HTTPS origin`); + } + if (url.protocol !== "https:" || + url.username || + url.password || + url.pathname !== "/" || + url.search || + url.hash) { + throw new Error(`Voidhash: ${field} must be an absolute HTTPS origin`); + } +}; +/** Validates build-time link and notification configuration before mutating a project. */ +const validateVoidhashExpoPluginOptions = (options) => { + if (options.measurement?.buildMode === "production" && + options.measurement.purchaseValidationEnvironment === "sandbox") { + throw new Error("Voidhash: sandbox purchase validation cannot be used in a production build"); + } + for (const domain of options.measurement?.ios?.associatedDomains ?? []) { + const host = domain.replace(/^applinks:/, ""); + if (!DOMAIN.test(host)) + throw new Error(`Voidhash: invalid associated domain '${domain}'`); + } + for (const appLink of options.measurement?.android?.appLinks ?? []) { + if (!DOMAIN.test(appLink.host)) + throw new Error(`Voidhash: invalid App Link host '${appLink.host}'`); + if (appLink.pathPrefix && !appLink.pathPrefix.startsWith("/")) { + throw new Error(`Voidhash: App Link pathPrefix must start with '/': '${appLink.pathPrefix}'`); + } + } + const schemes = [ + ...(options.measurement?.ios?.urlSchemes ?? []), + ...(options.measurement?.android?.urlSchemes ?? []), + ]; + for (const scheme of schemes) { + if (!SCHEME.test(scheme)) + throw new Error(`Voidhash: invalid URL scheme '${scheme}'`); + } + const iosMeasurement = options.measurement?.ios; + if (iosMeasurement?.privacyMode === "strict-no-idfa" && iosMeasurement.requireAdvertisingId) { + throw new Error("Voidhash: strict-no-idfa cannot be combined with requireAdvertisingId"); + } + const androidMeasurement = options.measurement?.android; + if (androidMeasurement?.advertisingIdPermission === "remove" && + androidMeasurement.requireAdvertisingId) { + throw new Error("Voidhash: advertisingIdPermission remove cannot be combined with requireAdvertisingId"); + } + if (iosMeasurement?.skAdNetworkPostbackEndpoint) { + validateApplePostbackEndpoint(iosMeasurement.skAdNetworkPostbackEndpoint, "measurement.ios.skAdNetworkPostbackEndpoint"); + } + if (iosMeasurement?.adAttributionKitPostbackEndpoint) { + validateApplePostbackEndpoint(iosMeasurement.adAttributionKitPostbackEndpoint, "measurement.ios.adAttributionKitPostbackEndpoint"); + } + if (iosMeasurement?.disableSKAD === true && + (iosMeasurement.skAdNetworkPostbackEndpoint || iosMeasurement.adAttributionKitPostbackEndpoint)) { + throw new Error("Voidhash: measurement.ios.disableSKAD cannot be combined with Apple postback endpoints"); + } + if (options.notifications?.enabled && + !options.notifications.android?.googleServicesFile) { + throw new Error("Voidhash: notifications.android.googleServicesFile is required when Android notifications are enabled"); + } + const channel = options.notifications?.android?.defaultChannel; + if (channel && (!channel.id.trim() || !channel.name.trim())) { + throw new Error("Voidhash: the default Android notification channel requires id and name"); + } + const apsEnvironment = options.notifications?.ios?.apsEnvironment; + if (apsEnvironment !== undefined && + apsEnvironment !== "development" && + apsEnvironment !== "production") { + throw new Error("Voidhash: notifications.ios.apsEnvironment must be development or production"); + } +}; +exports.validateVoidhashExpoPluginOptions = validateVoidhashExpoPluginOptions; +const unique = (values) => [...new Set(values)]; +/** Applies deterministic iOS plist values and returns the same plist object. */ +const applyVoidhashIosInfoPlist = (infoPlist, options) => { + const iosMeasurement = options.measurement?.ios; + const plist = infoPlist; + if (iosMeasurement?.disableSKAD === true) { + delete plist.NSAdvertisingAttributionReportEndpoint; + delete plist.AttributionCopyEndpoint; + } + else { + if (iosMeasurement?.skAdNetworkPostbackEndpoint) { + plist.NSAdvertisingAttributionReportEndpoint = + iosMeasurement.skAdNetworkPostbackEndpoint; + } + if (iosMeasurement?.adAttributionKitPostbackEndpoint) { + plist.AttributionCopyEndpoint = iosMeasurement.adAttributionKitPostbackEndpoint; + } + } + return infoPlist; +}; +exports.applyVoidhashIosInfoPlist = applyVoidhashIosInfoPlist; +/** Applies the compilation condition used to remove IDFA-linked code from strict builds. */ +const applyVoidhashIosBuildSettings = (buildSettings, options) => { + const current = String(buildSettings.SWIFT_ACTIVE_COMPILATION_CONDITIONS ?? "$(inherited)") + .split(/\s+/) + .filter((value) => value && value !== "VOIDHASH_STRICT_NO_IDFA"); + if (options.measurement?.ios?.privacyMode === "strict-no-idfa") { + current.push("VOIDHASH_STRICT_NO_IDFA"); + } + buildSettings.SWIFT_ACTIVE_COMPILATION_CONDITIONS = unique(current).join(" "); + return buildSettings; +}; +exports.applyVoidhashIosBuildSettings = applyVoidhashIosBuildSettings; +const withIosMeasurement = (config, options) => { + config = (0, config_plugins_1.withXcodeProject)(config, (current) => { + const configurations = current.modResults.pbxXCBuildConfigurationSection(); + for (const [key, value] of Object.entries(configurations)) { + if (key.endsWith("_comment") || !value || typeof value !== "object") + continue; + const configuration = value; + configuration.buildSettings ??= {}; + (0, exports.applyVoidhashIosBuildSettings)(configuration.buildSettings, options); + } + return current; + }); + config = (0, config_plugins_1.withEntitlementsPlist)(config, (current) => { + const configured = (options.measurement?.ios?.associatedDomains ?? []).map((domain) => domain.startsWith("applinks:") ? domain : `applinks:${domain}`); + current.modResults["com.apple.developer.associated-domains"] = unique([ + ...(current.modResults["com.apple.developer.associated-domains"] ?? []), + ...configured, + ]).sort(); + const environment = options.notifications?.ios?.apsEnvironment; + if (options.notifications?.enabled && environment) { + current.modResults["aps-environment"] = environment; + } + return current; + }); + return (0, config_plugins_1.withInfoPlist)(config, (current) => { + (0, exports.applyVoidhashIosInfoPlist)(current.modResults, options); + const schemes = unique(options.measurement?.ios?.urlSchemes ?? []).sort(); + if (schemes.length > 0) { + const existing = current.modResults.CFBundleURLTypes ?? []; + const retained = existing.filter((item) => item.CFBundleURLName !== "com.voidhash.measurement"); + current.modResults.CFBundleURLTypes = [ + ...retained, + { CFBundleURLName: "com.voidhash.measurement", CFBundleURLSchemes: schemes }, + ]; + } + if (options.notifications?.ios?.backgroundRemoteNotifications) { + current.modResults.UIBackgroundModes = unique([ + ...(current.modResults.UIBackgroundModes ?? []), + "remote-notification", + ]).sort(); + } + current.modResults.VoidhashCapabilityManifest = JSON.stringify({ + links: { + associatedDomains: options.measurement?.ios?.associatedDomains ?? [], + urlSchemes: schemes, + }, + appleAttribution: { + adAttributionKitPostbackEndpoint: options.measurement?.ios?.adAttributionKitPostbackEndpoint, + disableSKAD: options.measurement?.ios?.disableSKAD ?? false, + skAdNetworkPostbackEndpoint: options.measurement?.ios?.skAdNetworkPostbackEndpoint, + }, + privacy: { + mode: options.measurement?.ios?.privacyMode ?? "standard", + advertisingIdAvailable: options.measurement?.ios?.privacyMode !== "strict-no-idfa", + }, + purchases: { + observation: options.measurement?.ios?.purchaseObservation ?? "disabled", + validationEnvironment: options.measurement?.purchaseValidationEnvironment ?? "production", + }, + notifications: { + enabled: options.notifications?.enabled ?? false, + environment: options.notifications?.ios?.apsEnvironment, + background: options.notifications?.ios?.backgroundRemoteNotifications ?? false, + }, + }); + return current; + }); +}; +/** Applies explicit notification and advertising-identifier permission policy. */ +const applyVoidhashAndroidPermissions = (manifest, options) => { + manifest["uses-permission"] ??= []; + const permissions = manifest["uses-permission"]; + const pushPermission = "android.permission.POST_NOTIFICATIONS"; + const advertisingPermission = "com.google.android.gms.permission.AD_ID"; + const retained = permissions.filter((item) => ![pushPermission, advertisingPermission].includes(item.$["android:name"] ?? "")); + if (options.notifications?.enabled && + options.notifications.android?.postNotifications !== "remove") { + retained.push({ $: { "android:name": pushPermission } }); + } + if (options.measurement?.android?.advertisingIdPermission === "include") { + retained.push({ $: { "android:name": advertisingPermission } }); + } + else if (options.measurement?.android?.advertisingIdPermission === "remove") { + manifest.$ ??= {}; + manifest.$["xmlns:tools"] = "http://schemas.android.com/tools"; + retained.push({ + $: { "android:name": advertisingPermission, "tools:node": "remove" }, + }); + } + manifest["uses-permission"] = retained; + return manifest; +}; +exports.applyVoidhashAndroidPermissions = applyVoidhashAndroidPermissions; +const withAndroidMeasurement = (config, options) => { + if (options.notifications?.android?.googleServicesFile) { + config.android = { + ...config.android, + googleServicesFile: options.notifications.android.googleServicesFile, + }; + } + return (0, config_plugins_1.withAndroidManifest)(config, (current) => { + const manifest = current.modResults.manifest; + (0, exports.applyVoidhashAndroidPermissions)(manifest, options); + const application = manifest.application?.[0]; + if (!application) + throw new Error("Voidhash: AndroidManifest is missing an application element"); + application["meta-data"] ??= []; + const metadata = application["meta-data"].filter((item) => !item.$["android:name"].startsWith("com.voidhash.measurement.")); + const channel = options.notifications?.android?.defaultChannel; + const manifestValue = JSON.stringify({ + links: { + appLinks: options.measurement?.android?.appLinks ?? [], + urlSchemes: options.measurement?.android?.urlSchemes ?? [], + }, + notifications: { + enabled: options.notifications?.enabled ?? false, + defaultChannel: channel, + }, + backupPolicy: options.measurement?.android?.backupPolicy ?? "preserve-app-rules", + privacy: { + advertisingIdPermission: options.measurement?.android?.advertisingIdPermission ?? "remove", + }, + purchases: { + observation: options.measurement?.android?.purchaseObservation ?? "disabled", + validationEnvironment: options.measurement?.purchaseValidationEnvironment ?? "production", + }, + installReferrers: Object.fromEntries(["google-play", "meta", "samsung", "huawei", "xiaomi"].map((provider) => [ + provider, + options.measurement?.android?.installReferrers?.includes(provider) + ? "available" + : "notConfigured", + ])), + identifierProviders: Object.fromEntries(["app-set-id", "gaid", "oaid", "amazon-aaid", "meta"].map((provider) => [ + provider, + options.measurement?.android?.identifierProviders?.includes(provider) + ? "available" + : "notConfigured", + ])), + outOfStore: options.measurement?.android?.outOfStore, + }); + metadata.push({ + $: { + "android:name": "com.voidhash.measurement.CAPABILITIES", + "android:value": manifestValue, + }, + }); + if (channel) { + metadata.push({ + $: { + "android:name": "com.google.firebase.messaging.default_notification_channel_id", + "android:value": channel.id, + }, + }); + } + application["meta-data"] = metadata; + const activity = application.activity?.find((item) => item["intent-filter"]?.some((filter) => filter.action?.some((action) => action.$["android:name"] === "android.intent.action.MAIN"))); + if (!activity) + throw new Error("Voidhash: AndroidManifest is missing a launcher activity"); + const preserved = (activity["intent-filter"] ?? []).filter((filter) => !filter.category?.some((category) => category.$["android:name"] === "com.voidhash.MEASUREMENT_LINK")); + const linkFilters = (options.measurement?.android?.appLinks ?? []).map((link) => ({ + $: { "android:autoVerify": String(link.autoVerify ?? true) }, + action: [{ $: { "android:name": "android.intent.action.VIEW" } }], + category: [ + { $: { "android:name": "android.intent.category.DEFAULT" } }, + { $: { "android:name": "android.intent.category.BROWSABLE" } }, + { $: { "android:name": "com.voidhash.MEASUREMENT_LINK" } }, + ], + data: [{ $: { "android:host": link.host, "android:pathPrefix": link.pathPrefix ?? "/", "android:scheme": "https" } }], + })); + const schemeFilters = (options.measurement?.android?.urlSchemes ?? []).map((scheme) => ({ + action: [{ $: { "android:name": "android.intent.action.VIEW" } }], + category: [ + { $: { "android:name": "android.intent.category.DEFAULT" } }, + { $: { "android:name": "android.intent.category.BROWSABLE" } }, + { $: { "android:name": "com.voidhash.MEASUREMENT_LINK" } }, + ], + data: [{ $: { "android:scheme": scheme } }], + })); + activity["intent-filter"] = [...preserved, ...linkFilters, ...schemeFilters]; + return current; + }); +}; +const withVoidhashReactNative = (config, options = {}) => { + (0, exports.validateVoidhashExpoPluginOptions)(options); + const withIos = withIosMeasurement(config, options); + return withAndroidMeasurement(withIos, options); +}; +exports.default = (0, config_plugins_1.createRunOncePlugin)(withVoidhashReactNative, package_json_1.default.name, package_json_1.default.version); diff --git a/libraries/react-native/plugin/src/doctor-cli.ts b/libraries/react-native/plugin/src/doctor-cli.ts new file mode 100644 index 000000000..872c0dae0 --- /dev/null +++ b/libraries/react-native/plugin/src/doctor-cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { diagnoseVoidhashIntegration } from "./doctor"; +import type { VoidhashExpoPluginOptions } from "./withVoidhashReactNative"; + +const root = resolve(process.cwd()); +const read = (...candidates: ReadonlyArray): string | undefined => { + const path = candidates.map((candidate) => resolve(root, candidate)).find(existsSync); + return path ? readFileSync(path, "utf8") : undefined; +}; +const configured = read("voidhash.config.json"); +const options = configured ? JSON.parse(configured) as VoidhashExpoPluginOptions : {}; +const report = diagnoseVoidhashIntegration({ + options, + androidApplicationSource: read("android/app/src/main/java/MainApplication.kt", "android/app/src/main/java/MainApplication.java"), + androidManifest: read("android/app/src/main/AndroidManifest.xml"), + googleServicesPresent: existsSync(resolve(root, "android/app/google-services.json")), + iosEntitlements: read("ios/Voidhash.entitlements", "ios/App.entitlements"), + iosInfoPlist: read("ios/Info.plist"), +}); +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +process.exitCode = report.ok ? 0 : 1; diff --git a/libraries/react-native/plugin/src/doctor.ts b/libraries/react-native/plugin/src/doctor.ts new file mode 100644 index 000000000..d2612b5a8 --- /dev/null +++ b/libraries/react-native/plugin/src/doctor.ts @@ -0,0 +1,89 @@ +import type { VoidhashExpoPluginOptions } from "./withVoidhashReactNative"; +import { validateVoidhashExpoPluginOptions } from "./withVoidhashReactNative"; + +export interface DoctorFinding { + readonly code: string; + readonly level: "error" | "warning"; + readonly message: string; +} + +export interface DoctorProjectSnapshot { + readonly options: VoidhashExpoPluginOptions; + readonly iosEntitlements?: string; + readonly iosInfoPlist?: string; + readonly androidManifest?: string; + readonly androidApplicationSource?: string; + readonly googleServicesPresent?: boolean; +} + +export interface DoctorReport { + readonly ok: boolean; + readonly findings: ReadonlyArray; + readonly capabilities: Readonly>; +} + +/** Evaluates native project integration using the same option validator as the Expo plugin. */ +export const diagnoseVoidhashIntegration = (snapshot: DoctorProjectSnapshot): DoctorReport => { + const findings: DoctorFinding[] = []; + try { + validateVoidhashExpoPluginOptions(snapshot.options); + } catch (error) { + findings.push({ code: "VH_CFG_CONTRADICTION", level: "error", message: error instanceof Error ? error.message : "Invalid configuration" }); + } + const notifications = snapshot.options.notifications; + if (notifications?.enabled && notifications.ios?.apsEnvironment && !snapshot.iosEntitlements?.includes("aps-environment")) { + findings.push({ code: "VH_IOS_APS_ENTITLEMENT_MISSING", level: "error", message: "Add the aps-environment entitlement for push notifications." }); + } + if (notifications?.enabled && !snapshot.googleServicesPresent) { + findings.push({ code: "VH_ANDROID_GOOGLE_SERVICES_MISSING", level: "error", message: "Add google-services.json to the Android application." }); + } + if (notifications?.enabled && !/FirebaseMessagingService|VoidhashPush/i.test(snapshot.androidApplicationSource ?? snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_FCM_HOOK_MISSING", level: "error", message: "Register the FCM service or Voidhash push subscriber." }); + } + if (snapshot.options.measurement?.android?.backupPolicy === "voidhash-no-backup" && !/fullBackupContent|dataExtractionRules|noBackupFilesDir/.test(snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_NO_BACKUP_UNVERIFIED", level: "error", message: "Configure or verify no-backup storage for measurement install state." }); + } + const ios = snapshot.options.measurement?.ios; + if ((ios?.associatedDomains?.length ?? 0) > 0 && !snapshot.iosEntitlements?.includes("com.apple.developer.associated-domains")) { + findings.push({ code: "VH_IOS_ASSOCIATED_DOMAINS_MISSING", level: "error", message: "Add the associated-domains entitlement." }); + } + if (ios?.disableSKAD !== true && !ios?.skAdNetworkPostbackEndpoint) { + findings.push({ code: "VH_IOS_SKAN_ENDPOINT_MISSING", level: "error", message: "Configure the HTTPS SKAdNetwork postback endpoint or explicitly disable SKAdNetwork." }); + } + if ( + ios?.skAdNetworkPostbackEndpoint && + !snapshot.iosInfoPlist?.includes("NSAdvertisingAttributionReportEndpoint") + ) { + findings.push({ code: "VH_IOS_SKAN_PLIST_MISSING", level: "error", message: "Regenerate Info.plist with NSAdvertisingAttributionReportEndpoint." }); + } + if ( + ios?.adAttributionKitPostbackEndpoint && + !snapshot.iosInfoPlist?.includes("AttributionCopyEndpoint") + ) { + findings.push({ code: "VH_IOS_ADATTRIBUTIONKIT_PLIST_MISSING", level: "error", message: "Regenerate Info.plist with AttributionCopyEndpoint." }); + } + for (const link of snapshot.options.measurement?.android?.appLinks ?? []) { + if (!new RegExp(`android:host=["']${link.host.replace(/\./g, "\\.")}["']`).test(snapshot.androidManifest ?? "")) { + findings.push({ code: "VH_ANDROID_APP_LINK_MISSING", level: "error", message: `Add the verified App Link host ${link.host}.` }); + } + if (link.autoVerify === false) { + findings.push({ code: "VH_ANDROID_APP_LINK_UNVERIFIED", level: "error", message: `Enable Android App Link verification for ${link.host}.` }); + } + } + const capabilities = { + links: { + androidAppLinks: snapshot.options.measurement?.android?.appLinks?.length ?? 0, + iosAssociatedDomains: snapshot.options.measurement?.ios?.associatedDomains?.length ?? 0, + }, + notifications: { enabled: notifications?.enabled ?? false }, + purchases: { + android: snapshot.options.measurement?.android?.purchaseObservation ?? "disabled", + ios: snapshot.options.measurement?.ios?.purchaseObservation ?? "disabled", + }, + privacy: { + androidAdvertisingId: snapshot.options.measurement?.android?.advertisingIdPermission ?? "remove", + ios: snapshot.options.measurement?.ios?.privacyMode ?? "standard", + }, + }; + return { ok: findings.every((finding) => finding.level !== "error"), findings, capabilities }; +}; diff --git a/libraries/react-native/plugin/src/storeDisclosures.ts b/libraries/react-native/plugin/src/storeDisclosures.ts new file mode 100644 index 000000000..d2cc4bcb7 --- /dev/null +++ b/libraries/react-native/plugin/src/storeDisclosures.ts @@ -0,0 +1,37 @@ +import type { VoidhashExpoPluginOptions } from "./withVoidhashReactNative"; + +export interface StoreDisclosureInputs { + readonly apple: { + readonly collectedData: ReadonlyArray; + readonly tracking: boolean; + }; + readonly googlePlay: { + readonly collectedData: ReadonlyArray; + readonly advertisingId: boolean; + readonly deletionSupported: true; + readonly encryptedInTransit: true; + }; +} + +/** Derives mobile-store disclosure inputs from the same enabled capability options as the plugin. */ +export const generateStoreDisclosureInputs = ( + options: VoidhashExpoPluginOptions, +): StoreDisclosureInputs => { + const notifications = options.notifications?.enabled === true; + const iosAdvertising = options.measurement?.ios?.privacyMode !== "strict-no-idfa" && + options.measurement?.ios?.requireAdvertisingId === true; + const androidAdvertising = options.measurement?.android?.advertisingIdPermission === "include"; + const common = ["product-interaction", "device-or-other-identifiers"]; + return { + apple: { + collectedData: [...common, ...(notifications ? ["push-token"] : []), ...(iosAdvertising ? ["advertising-identifier"] : [])].sort(), + tracking: iosAdvertising, + }, + googlePlay: { + collectedData: [...common, ...(notifications ? ["push-token"] : []), ...(androidAdvertising ? ["advertising-identifier"] : [])].sort(), + advertisingId: androidAdvertising, + deletionSupported: true, + encryptedInTransit: true, + }, + }; +}; diff --git a/libraries/react-native/plugin/src/withVoidhashReactNative.ts b/libraries/react-native/plugin/src/withVoidhashReactNative.ts index 3d1cf8674..c817915ae 100644 --- a/libraries/react-native/plugin/src/withVoidhashReactNative.ts +++ b/libraries/react-native/plugin/src/withVoidhashReactNative.ts @@ -1,9 +1,427 @@ import type { ConfigPlugin } from "expo/config-plugins"; -import { createRunOncePlugin } from "expo/config-plugins"; +import { + createRunOncePlugin, + withAndroidManifest, + withEntitlementsPlist, + withInfoPlist, + withXcodeProject, +} from "expo/config-plugins"; -// biome-ignore lint/nursery/useJsonImportAttribute: skip +// biome-ignore lint/nursery/useJsonImportAttribute: plugin code is emitted as CommonJS. import pkg from "../../package.json"; -const withVoidhashReactNative: ConfigPlugin = (config) => config; +export interface VoidhashExpoPluginOptions { + readonly measurement?: { + readonly buildMode?: "development" | "production"; + readonly purchaseValidationEnvironment?: "production" | "sandbox"; + readonly ios?: { + readonly associatedDomains?: ReadonlyArray; + readonly urlSchemes?: ReadonlyArray; + readonly skAdNetworkPostbackEndpoint?: string; + readonly adAttributionKitPostbackEndpoint?: string; + readonly disableSKAD?: boolean; + readonly privacyMode?: "standard" | "strict-no-idfa"; + readonly requireAdvertisingId?: boolean; + readonly purchaseObservation?: "storekit2" | "storekit1" | "disabled"; + }; + readonly android?: { + readonly appLinks?: ReadonlyArray<{ + readonly host: string; + readonly pathPrefix?: string; + readonly autoVerify?: boolean; + }>; + readonly urlSchemes?: ReadonlyArray; + readonly backupPolicy?: "voidhash-no-backup" | "preserve-app-rules"; + readonly advertisingIdPermission?: "include" | "remove"; + readonly requireAdvertisingId?: boolean; + readonly purchaseObservation?: "billing8" | "disabled"; + readonly installReferrers?: ReadonlyArray<"google-play" | "meta" | "samsung" | "huawei" | "xiaomi">; + readonly identifierProviders?: ReadonlyArray<"app-set-id" | "gaid" | "oaid" | "amazon-aaid" | "meta">; + readonly outOfStore?: string; + }; + }; + readonly notifications?: { + readonly enabled?: boolean; + readonly ios?: { + readonly apsEnvironment?: "development" | "production"; + readonly backgroundRemoteNotifications?: boolean; + }; + readonly android?: { + readonly googleServicesFile?: string; + readonly postNotifications?: "include" | "remove"; + readonly defaultChannel?: { + readonly id: string; + readonly name: string; + readonly importance?: "default" | "high" | "low" | "min" | "none"; + }; + }; + }; +} + +const DOMAIN = /^(?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?\.)+[a-z]{2,63}$/i; +const SCHEME = /^[a-z][a-z\d+.-]*$/i; + +const validateApplePostbackEndpoint = (value: string, field: string): void => { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error(`Voidhash: ${field} must be an absolute HTTPS origin`); + } + if ( + url.protocol !== "https:" || + url.username || + url.password || + url.pathname !== "/" || + url.search || + url.hash + ) { + throw new Error(`Voidhash: ${field} must be an absolute HTTPS origin`); + } +}; + +/** Validates build-time link and notification configuration before mutating a project. */ +export const validateVoidhashExpoPluginOptions = ( + options: VoidhashExpoPluginOptions, +): void => { + if ( + options.measurement?.buildMode === "production" && + options.measurement.purchaseValidationEnvironment === "sandbox" + ) { + throw new Error("Voidhash: sandbox purchase validation cannot be used in a production build"); + } + for (const domain of options.measurement?.ios?.associatedDomains ?? []) { + const host = domain.replace(/^applinks:/, ""); + if (!DOMAIN.test(host)) throw new Error(`Voidhash: invalid associated domain '${domain}'`); + } + for (const appLink of options.measurement?.android?.appLinks ?? []) { + if (!DOMAIN.test(appLink.host)) throw new Error(`Voidhash: invalid App Link host '${appLink.host}'`); + if (appLink.pathPrefix && !appLink.pathPrefix.startsWith("/")) { + throw new Error(`Voidhash: App Link pathPrefix must start with '/': '${appLink.pathPrefix}'`); + } + } + const schemes = [ + ...(options.measurement?.ios?.urlSchemes ?? []), + ...(options.measurement?.android?.urlSchemes ?? []), + ]; + for (const scheme of schemes) { + if (!SCHEME.test(scheme)) throw new Error(`Voidhash: invalid URL scheme '${scheme}'`); + } + const iosMeasurement = options.measurement?.ios; + if (iosMeasurement?.privacyMode === "strict-no-idfa" && iosMeasurement.requireAdvertisingId) { + throw new Error( + "Voidhash: strict-no-idfa cannot be combined with requireAdvertisingId", + ); + } + const androidMeasurement = options.measurement?.android; + if ( + androidMeasurement?.advertisingIdPermission === "remove" && + androidMeasurement.requireAdvertisingId + ) { + throw new Error( + "Voidhash: advertisingIdPermission remove cannot be combined with requireAdvertisingId", + ); + } + if (iosMeasurement?.skAdNetworkPostbackEndpoint) { + validateApplePostbackEndpoint( + iosMeasurement.skAdNetworkPostbackEndpoint, + "measurement.ios.skAdNetworkPostbackEndpoint", + ); + } + if (iosMeasurement?.adAttributionKitPostbackEndpoint) { + validateApplePostbackEndpoint( + iosMeasurement.adAttributionKitPostbackEndpoint, + "measurement.ios.adAttributionKitPostbackEndpoint", + ); + } + if ( + iosMeasurement?.disableSKAD === true && + (iosMeasurement.skAdNetworkPostbackEndpoint || iosMeasurement.adAttributionKitPostbackEndpoint) + ) { + throw new Error( + "Voidhash: measurement.ios.disableSKAD cannot be combined with Apple postback endpoints", + ); + } + if ( + options.notifications?.enabled && + !options.notifications.android?.googleServicesFile + ) { + throw new Error( + "Voidhash: notifications.android.googleServicesFile is required when Android notifications are enabled", + ); + } + const channel = options.notifications?.android?.defaultChannel; + if (channel && (!channel.id.trim() || !channel.name.trim())) { + throw new Error("Voidhash: the default Android notification channel requires id and name"); + } + const apsEnvironment = options.notifications?.ios?.apsEnvironment as string | undefined; + if ( + apsEnvironment !== undefined && + apsEnvironment !== "development" && + apsEnvironment !== "production" + ) { + throw new Error("Voidhash: notifications.ios.apsEnvironment must be development or production"); + } +}; + +const unique = (values: ReadonlyArray): T[] => [...new Set(values)]; + +/** Applies deterministic iOS plist values and returns the same plist object. */ +export const applyVoidhashIosInfoPlist = >( + infoPlist: T, + options: VoidhashExpoPluginOptions, +): T => { + const iosMeasurement = options.measurement?.ios; + const plist = infoPlist as Record; + if (iosMeasurement?.disableSKAD === true) { + delete plist.NSAdvertisingAttributionReportEndpoint; + delete plist.AttributionCopyEndpoint; + } else { + if (iosMeasurement?.skAdNetworkPostbackEndpoint) { + plist.NSAdvertisingAttributionReportEndpoint = + iosMeasurement.skAdNetworkPostbackEndpoint; + } + if (iosMeasurement?.adAttributionKitPostbackEndpoint) { + plist.AttributionCopyEndpoint = iosMeasurement.adAttributionKitPostbackEndpoint; + } + } + return infoPlist; +}; + +/** Applies the compilation condition used to remove IDFA-linked code from strict builds. */ +export const applyVoidhashIosBuildSettings = ( + buildSettings: Record, + options: VoidhashExpoPluginOptions, +): Record => { + const current = String(buildSettings.SWIFT_ACTIVE_COMPILATION_CONDITIONS ?? "$(inherited)") + .split(/\s+/) + .filter((value) => value && value !== "VOIDHASH_STRICT_NO_IDFA"); + if (options.measurement?.ios?.privacyMode === "strict-no-idfa") { + current.push("VOIDHASH_STRICT_NO_IDFA"); + } + buildSettings.SWIFT_ACTIVE_COMPILATION_CONDITIONS = unique(current).join(" "); + return buildSettings; +}; + +const withIosMeasurement: ConfigPlugin = (config, options) => { + config = withXcodeProject(config, (current) => { + const configurations = current.modResults.pbxXCBuildConfigurationSection(); + for (const [key, value] of Object.entries(configurations)) { + if (key.endsWith("_comment") || !value || typeof value !== "object") continue; + const configuration = value as { buildSettings?: Record }; + configuration.buildSettings ??= {}; + applyVoidhashIosBuildSettings(configuration.buildSettings, options); + } + return current; + }); + config = withEntitlementsPlist(config, (current) => { + const configured = (options.measurement?.ios?.associatedDomains ?? []).map((domain) => + domain.startsWith("applinks:") ? domain : `applinks:${domain}`, + ); + current.modResults["com.apple.developer.associated-domains"] = unique([ + ...((current.modResults["com.apple.developer.associated-domains"] as string[] | undefined) ?? []), + ...configured, + ]).sort(); + const environment = options.notifications?.ios?.apsEnvironment; + if (options.notifications?.enabled && environment) { + current.modResults["aps-environment"] = environment; + } + return current; + }); + + return withInfoPlist(config, (current) => { + applyVoidhashIosInfoPlist(current.modResults, options); + const schemes = unique(options.measurement?.ios?.urlSchemes ?? []).sort(); + if (schemes.length > 0) { + const existing = current.modResults.CFBundleURLTypes ?? []; + const retained = existing.filter((item) => item.CFBundleURLName !== "com.voidhash.measurement"); + current.modResults.CFBundleURLTypes = [ + ...retained, + { CFBundleURLName: "com.voidhash.measurement", CFBundleURLSchemes: schemes }, + ]; + } + if (options.notifications?.ios?.backgroundRemoteNotifications) { + current.modResults.UIBackgroundModes = unique([ + ...((current.modResults.UIBackgroundModes as string[] | undefined) ?? []), + "remote-notification", + ]).sort(); + } + current.modResults.VoidhashCapabilityManifest = JSON.stringify({ + links: { + associatedDomains: options.measurement?.ios?.associatedDomains ?? [], + urlSchemes: schemes, + }, + appleAttribution: { + adAttributionKitPostbackEndpoint: + options.measurement?.ios?.adAttributionKitPostbackEndpoint, + disableSKAD: options.measurement?.ios?.disableSKAD ?? false, + skAdNetworkPostbackEndpoint: + options.measurement?.ios?.skAdNetworkPostbackEndpoint, + }, + privacy: { + mode: options.measurement?.ios?.privacyMode ?? "standard", + advertisingIdAvailable: options.measurement?.ios?.privacyMode !== "strict-no-idfa", + }, + purchases: { + observation: options.measurement?.ios?.purchaseObservation ?? "disabled", + validationEnvironment: options.measurement?.purchaseValidationEnvironment ?? "production", + }, + notifications: { + enabled: options.notifications?.enabled ?? false, + environment: options.notifications?.ios?.apsEnvironment, + background: options.notifications?.ios?.backgroundRemoteNotifications ?? false, + }, + }); + return current; + }); +}; + +type AndroidManifestShape = { + $?: Record; + "uses-permission"?: Array<{ $: Record }>; +}; + +/** Applies explicit notification and advertising-identifier permission policy. */ +export const applyVoidhashAndroidPermissions = ( + manifest: T, + options: VoidhashExpoPluginOptions, +): T & AndroidManifestShape => { + manifest["uses-permission"] ??= []; + const permissions = manifest["uses-permission"]; + const pushPermission = "android.permission.POST_NOTIFICATIONS"; + const advertisingPermission = "com.google.android.gms.permission.AD_ID"; + const retained = permissions.filter( + (item) => ![pushPermission, advertisingPermission].includes(item.$["android:name"] ?? ""), + ); + if ( + options.notifications?.enabled && + options.notifications.android?.postNotifications !== "remove" + ) { + retained.push({ $: { "android:name": pushPermission } }); + } + if (options.measurement?.android?.advertisingIdPermission === "include") { + retained.push({ $: { "android:name": advertisingPermission } }); + } else if (options.measurement?.android?.advertisingIdPermission === "remove") { + manifest.$ ??= {}; + manifest.$["xmlns:tools"] = "http://schemas.android.com/tools"; + retained.push({ + $: { "android:name": advertisingPermission, "tools:node": "remove" }, + }); + } + manifest["uses-permission"] = retained; + return manifest as T & AndroidManifestShape; +}; + +const withAndroidMeasurement: ConfigPlugin = (config, options) => { + if (options.notifications?.android?.googleServicesFile) { + config.android = { + ...config.android, + googleServicesFile: options.notifications.android.googleServicesFile, + }; + } + return withAndroidManifest(config, (current) => { + const manifest = current.modResults.manifest; + applyVoidhashAndroidPermissions(manifest, options); + + const application = manifest.application?.[0]; + if (!application) throw new Error("Voidhash: AndroidManifest is missing an application element"); + application["meta-data"] ??= []; + const metadata = application["meta-data"].filter( + (item) => !item.$["android:name"].startsWith("com.voidhash.measurement."), + ); + const channel = options.notifications?.android?.defaultChannel; + const manifestValue = JSON.stringify({ + links: { + appLinks: options.measurement?.android?.appLinks ?? [], + urlSchemes: options.measurement?.android?.urlSchemes ?? [], + }, + notifications: { + enabled: options.notifications?.enabled ?? false, + defaultChannel: channel, + }, + backupPolicy: options.measurement?.android?.backupPolicy ?? "preserve-app-rules", + privacy: { + advertisingIdPermission: + options.measurement?.android?.advertisingIdPermission ?? "remove", + }, + purchases: { + observation: options.measurement?.android?.purchaseObservation ?? "disabled", + validationEnvironment: options.measurement?.purchaseValidationEnvironment ?? "production", + }, + installReferrers: Object.fromEntries( + ["google-play", "meta", "samsung", "huawei", "xiaomi"].map((provider) => [ + provider, + options.measurement?.android?.installReferrers?.includes(provider as "google-play") + ? "available" + : "notConfigured", + ]), + ), + identifierProviders: Object.fromEntries( + ["app-set-id", "gaid", "oaid", "amazon-aaid", "meta"].map((provider) => [ + provider, + options.measurement?.android?.identifierProviders?.includes(provider as "gaid") + ? "available" + : "notConfigured", + ]), + ), + outOfStore: options.measurement?.android?.outOfStore, + }); + metadata.push({ + $: { + "android:name": "com.voidhash.measurement.CAPABILITIES", + "android:value": manifestValue, + }, + }); + if (channel) { + metadata.push({ + $: { + "android:name": "com.google.firebase.messaging.default_notification_channel_id", + "android:value": channel.id, + }, + }); + } + application["meta-data"] = metadata; + + const activity = application.activity?.find((item) => + item["intent-filter"]?.some((filter) => + filter.action?.some((action) => action.$["android:name"] === "android.intent.action.MAIN"), + ), + ); + if (!activity) throw new Error("Voidhash: AndroidManifest is missing a launcher activity"); + const preserved = (activity["intent-filter"] ?? []).filter( + (filter) => !filter.category?.some((category) => category.$["android:name"] === "com.voidhash.MEASUREMENT_LINK"), + ); + const linkFilters = (options.measurement?.android?.appLinks ?? []).map((link) => ({ + $: { "android:autoVerify": String(link.autoVerify ?? true) }, + action: [{ $: { "android:name": "android.intent.action.VIEW" } }], + category: [ + { $: { "android:name": "android.intent.category.DEFAULT" } }, + { $: { "android:name": "android.intent.category.BROWSABLE" } }, + { $: { "android:name": "com.voidhash.MEASUREMENT_LINK" } }, + ], + data: [{ $: { "android:host": link.host, "android:pathPrefix": link.pathPrefix ?? "/", "android:scheme": "https" } }], + })); + const schemeFilters = (options.measurement?.android?.urlSchemes ?? []).map((scheme) => ({ + action: [{ $: { "android:name": "android.intent.action.VIEW" } }], + category: [ + { $: { "android:name": "android.intent.category.DEFAULT" } }, + { $: { "android:name": "android.intent.category.BROWSABLE" } }, + { $: { "android:name": "com.voidhash.MEASUREMENT_LINK" } }, + ], + data: [{ $: { "android:scheme": scheme } }], + })); + activity["intent-filter"] = [...preserved, ...linkFilters, ...schemeFilters]; + return current; + }); +}; + +const withVoidhashReactNative: ConfigPlugin = ( + config, + options = {}, +) => { + validateVoidhashExpoPluginOptions(options); + const withIos = withIosMeasurement(config, options); + return withAndroidMeasurement(withIos, options); +}; export default createRunOncePlugin(withVoidhashReactNative, pkg.name, pkg.version); diff --git a/libraries/react-native/src/client-react-native.ts b/libraries/react-native/src/client-react-native.ts index 702488a96..274d4bfc9 100644 --- a/libraries/react-native/src/client-react-native.ts +++ b/libraries/react-native/src/client-react-native.ts @@ -3,6 +3,8 @@ import { AtomRegistry } from "effect/unstable/reactivity"; import { Platform as RNPlatform } from "react-native"; import { VoidhashClient, type VoidhashClientOptions } from "./client"; +import { createNativeMeasurementRuntimeAdapter } from "./core/measurement/native-adapter"; +import { resolveMeasurementEndpoints } from "./core/measurement/endpoints"; import { SchemeNotSetError } from "./errors"; import { voidhashProviderFactory } from "./react/components/provider"; import { useRetrieveAppStoreProduct } from "./react/hooks/app-store/use-retrieve-app-store-product"; @@ -25,10 +27,17 @@ import { purchaseHookFactory } from "./react/hooks/use-purchase"; * `voidhash.gen.d.ts` (run `voidhash-cli types generate`). */ export function createVoidhashClient(publishableKey: string, options: VoidhashClientOptions = {}) { - const baseUrl = options.baseUrl || "https://api.voidhash.com"; const debug = options.debug ?? false; + const resolvedEndpoints = resolveMeasurementEndpoints( + options.endpoints ?? { + api: options.baseUrl, + ingest: options.ingestUrl, + }, + debug, + ); + const baseUrl = resolvedEndpoints.api; const distinctId = options.distinctId ?? null; - const ingestUrl = options.ingestUrl; + const ingestUrl = resolvedEndpoints.ingest; const readOnly = options.readOnly ?? false; const unstableSwallowErrors = options.unstable_swallowErrors ?? false; const scheme = @@ -56,6 +65,15 @@ export function createVoidhashClient(publishableKey: string, options: VoidhashCl platform, debug, options.unstable_internalSchema, + { + consent: options.consent, + links: options.links, + measurement: options.measurement, + notifications: options.notifications, + endpoints: options.endpoints, + nativeAdapter: createNativeMeasurementRuntimeAdapter(), + }, + resolvedEndpoints, ); const { provider, context, useVoidhash } = voidhashProviderFactory(client); diff --git a/libraries/react-native/src/client.tsx b/libraries/react-native/src/client.tsx index 5484e6fc7..88d81fd55 100644 --- a/libraries/react-native/src/client.tsx +++ b/libraries/react-native/src/client.tsx @@ -28,6 +28,22 @@ import type { RuntimeSchema } from "./core/schema/runtime"; import { SchemaManager } from "./core/schema/schema-manager"; import { SdkConfiguration } from "./core/sdk-configuration"; import { TransactionService } from "./core/transactions/transaction-service"; +import { + type ConsentClient, + type ConsentSnapshot, + type LinkConfiguration, + type LinksClient, + type MeasurementClient, + type MeasurementConfiguration, + type NotificationsClient, + type NotificationsConfiguration, + UnifiedMeasurementRuntime, + type MeasurementRuntimeAdapter, + type MeasurementEndpointOverrides, + type ResolvedMeasurementEndpoints, + type ProtectedIdentityTraits, + type ProtectedIdentityUpdateResult, +} from "./core/measurement"; import { ReadOnlyModePurchaseNotAllowedError, VoidhashError } from "./errors"; export interface VoidhashClientOptions { @@ -35,6 +51,16 @@ export interface VoidhashClientOptions { debug?: boolean; distinctId?: string; ingestUrl?: string; + /** Self-host endpoint and signed-configuration trust overrides. */ + endpoints?: MeasurementEndpointOverrides; + /** Initial revisioned consent state loaded before optional collectors run. */ + consent?: ConsentSnapshot; + /** Unified measurement and collection configuration. */ + measurement?: MeasurementConfiguration; + /** Deep-link normalization, allowlisting, and payload configuration. */ + links?: LinkConfiguration; + /** Push permission and registration behavior. */ + notifications?: NotificationsConfiguration; readOnly?: boolean; scheme?: string; unstable_swallowErrors?: boolean; @@ -54,6 +80,7 @@ const CreateEffectRuntime = ( publishableKey: string, readOnly: boolean, atomRegistry: AtomRegistry.AtomRegistry, + measurementRuntime: UnifiedMeasurementRuntime, ) => ManagedRuntime.make( pipe( @@ -82,6 +109,7 @@ const CreateEffectRuntime = ( ingestUrl, publishableKey, readOnly, + measurementRuntime, }), ), ), @@ -102,6 +130,15 @@ type InitializedEffectClient = Effect.Success< >; export class VoidhashClient { + /** Measurement lifecycle, evidence, revenue, validation, and diagnostics. */ + readonly measurement: MeasurementClient; + /** The single deep-link handling and result stream. */ + readonly links: LinksClient; + /** The single revisioned consent input shared by every namespace. */ + readonly consent: ConsentClient; + /** Push permission, registration, badge, and notification streams. */ + readonly notifications: NotificationsClient; + private _isInitialized = false; private analyticsFlushInFlight: Promise | null = null; private appLifecycleSubscription: { remove: () => void } | null = null; @@ -120,6 +157,7 @@ export class VoidhashClient { private unitializedClient: UninitializedEffectClient; private initializedClient?: InitializedEffectClient; + private readonly measurementRuntime: UnifiedMeasurementRuntime; constructor( initialDistinctId: string | null, @@ -133,6 +171,11 @@ export class VoidhashClient { platform: Exclude, debug = false, internalSchema?: RuntimeSchema, + unifiedOptions: Pick< + VoidhashClientOptions, + "consent" | "measurement" | "links" | "notifications" | "endpoints" + > & { readonly nativeAdapter?: MeasurementRuntimeAdapter } = {}, + resolvedEndpoints?: ResolvedMeasurementEndpoints, ) { this.initialDistinctId = initialDistinctId; this.readOnly = readOnly; @@ -140,6 +183,25 @@ export class VoidhashClient { this.internalSchema = internalSchema; this.unstableSwallowErrors = unstableSwallowErrors; this.atomRegistry = atomRegistry; + this.measurementRuntime = new UnifiedMeasurementRuntime({ + publishableKey, + baseUrl, + ingestUrl, + platform, + distinctId: initialDistinctId ?? undefined, + consent: unifiedOptions.consent, + measurement: unifiedOptions.measurement, + links: unifiedOptions.links, + notifications: unifiedOptions.notifications, + adapter: unifiedOptions.nativeAdapter, + linksUrl: resolvedEndpoints?.links, + trustedConfigKeyIds: resolvedEndpoints?.trustedConfigKeyIds, + trustedConfigKeys: unifiedOptions.endpoints?.trustedConfigKeys?.map((key) => ({ + keyId: key.keyId, + publicKeySpki: key.publicKey, + })), + configurationProjectId: unifiedOptions.endpoints?.configurationProjectId, + }); this.effectRuntime = CreateEffectRuntime( platform, baseUrl, @@ -148,13 +210,18 @@ export class VoidhashClient { publishableKey, readOnly, atomRegistry, + this.measurementRuntime, ); this.unitializedClient = VoidhashEffectClient.makeUnitializedClient(); + this.measurement = this.measurementRuntime.measurement; + this.links = this.measurementRuntime.links; + this.consent = this.measurementRuntime.consent; + this.notifications = this.measurementRuntime.notifications; } - private async runSideEffect(operation: string, effect: () => Promise) { + private async runSideEffect(operation: string, effect: () => Promise): Promise { try { - await effect(); + return await effect(); } catch (error) { if (!this.unstableSwallowErrors) { throw error; @@ -162,6 +229,7 @@ export class VoidhashClient { // biome-ignore lint/suspicious/noConsole: This warning is intentionally surfaced in all environments. console.warn(`[voidhash] swallowed error in ${operation}`, error); + return undefined; } } @@ -193,6 +261,12 @@ export class VoidhashClient { this.initializedClient = initializedClient; this._isInitialized = true; + const initializedDistinctId = await this.runEffect( + initializedClient.getDistinctId(), + "FAILED_TO_GET_DISTINCT_ID", + ); + this.measurementRuntime.internalHydrateIdentity(initializedDistinctId); + await this.measurementRuntime.initialize(); // Set up analytics flush callback and transfer pre-init buffer initializedClient.setAnalyticsFlushCallback(() => { @@ -303,14 +377,28 @@ export class VoidhashClient { options: { email?: string; name?: string; - }, - ) { - await this.runSideEffect("identify", async () => { + emails?: ProtectedIdentityTraits["emails"]; + phones?: ProtectedIdentityTraits["phones"]; + clearEmails?: boolean; + clearPhones?: boolean; + } = {}, + ): Promise { + return this.runSideEffect("identify", async () => { this.ensureInitialized(); await this.runEffect( this.initializedClient!.identify(externalUserId, options), "FAILED_TO_IDENTIFY", ); + this.measurementRuntime.setIdentity(externalUserId); + if (options.emails || options.phones || options.clearEmails || options.clearPhones) { + return this.measurementRuntime.setProtectedIdentityTraits({ + emails: options.emails, + phones: options.phones, + clearEmails: options.clearEmails, + clearPhones: options.clearPhones, + }); + } + return undefined; }); } @@ -321,6 +409,11 @@ export class VoidhashClient { await this.runSideEffect("reset", async () => { this.ensureInitialized(); await this.runEffect(this.initializedClient!.reset(), "FAILED_TO_RESET"); + const distinctId = await this.runEffect( + this.initializedClient!.getDistinctId(), + "FAILED_TO_GET_DISTINCT_ID", + ); + this.measurementRuntime.setIdentity(distinctId); }); } @@ -333,6 +426,11 @@ export class VoidhashClient { await this.runSideEffect("signOut", async () => { this.ensureInitialized(); await this.runEffect(this.initializedClient!.signOut(), "FAILED_TO_SIGN_OUT"); + const distinctId = await this.runEffect( + this.initializedClient!.getDistinctId(), + "FAILED_TO_GET_DISTINCT_ID", + ); + this.measurementRuntime.setIdentity(distinctId); }); } @@ -419,6 +517,7 @@ export class VoidhashClient { * Events are batched and delivered on size/time thresholds. */ capture(eventName: string, properties: Record = {}) { + this.measurementRuntime.capture(eventName, properties); if (!this.initializedClient) { const normalized = eventName.trim(); if (normalized) { @@ -450,6 +549,7 @@ export class VoidhashClient { }); await this.analyticsFlushInFlight; + await this.measurementRuntime.flush(); }); } diff --git a/libraries/react-native/src/core/analytics/service.ts b/libraries/react-native/src/core/analytics/service.ts index e04941f43..efaa39e17 100644 --- a/libraries/react-native/src/core/analytics/service.ts +++ b/libraries/react-native/src/core/analytics/service.ts @@ -8,7 +8,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import { CacheManager } from "../caching/cache-manager"; import { IdentityManager } from "../identity/identity-manager"; import { SdkConfiguration } from "../sdk-configuration"; -import { getNonce } from "../utils/crypto"; import { AUTOMATIC_EVENTS } from "./constants"; import { AnalyticsIngestEvent, AnalyticsSendFailure, QueuedAnalyticsEvent } from "./types"; import { @@ -100,7 +99,6 @@ export class AnalyticsService extends Context.Service()( const queueRef = yield* Ref.make>([]); const latch = yield* Latch.make(false); - const sessionId = getNonce(); const getStandardizedProperties = getAnalyticsStandardizedProperties(); let flushCallback: (() => void) | null = null; @@ -148,11 +146,10 @@ export class AnalyticsService extends Context.Service()( Effect.gen(function* () { if (events.length === 0) return; - const distinctId = yield* identityManager.getDistinctId(); yield* eventCaptureClient.eventCaptureBatch({ events: events.map((event) => ({ context: event.context, - distinct_id: distinctId, + distinct_id: event.distinct_id, event: event.event_name, properties: event.properties, session_id: event.session_id, @@ -163,24 +160,24 @@ export class AnalyticsService extends Context.Service()( token: sdkConfiguration.publishableKey, }); }).pipe( - Effect.catchTags({ - CaptureDependencyUnavailableError: (err) => failRetryable(err.response.status), - CaptureInternalServerError: (err) => failRetryable(err.response.status), - CapturePayloadTooLargeError: (err) => failNonRetryable(err.response.status), - CaptureRateLimitedError: (err) => - failRetryable( - err.response.status, - parseRetryAfterMs(err.response.headers["retry-after"]) ?? - err.data.retry_after_ms ?? + Effect.catch((cause) => { + if (cause._tag === "CaptureRateLimitedError") { + return failRetryable( + cause.response.status, + parseRetryAfterMs(cause.response.headers["retry-after"]) ?? + cause.data.retry_after_ms ?? undefined, - ), - CaptureUnauthorizedError: (err) => failNonRetryable(err.response.status), - EventCaptureBatch400: (err) => failNonRetryable(err.response.status), - }), - // Unmapped status codes (e.g. 408/502/504) surface as - // `HttpClientError`; treat network errors and the retryable subset - // as retryable, everything else as non-retryable. - Effect.catchTag("HttpClientError", (cause) => { + ); + } + if ( + cause._tag === "CaptureDependencyUnavailableError" || + cause._tag === "CaptureInternalServerError" + ) { + return failRetryable(cause.response.status); + } + if (cause._tag !== "HttpClientError") { + return failNonRetryable(cause.response.status); + } const status = cause.response?.status; if (status === undefined) { return Effect.fail( @@ -251,10 +248,9 @@ export class AnalyticsService extends Context.Service()( const processQueuedBatch = ( queuedBatch: ReadonlyArray, - standardizedProperties: Record, ): Effect.Effect => { const ingestBatch = queuedBatch.map((event) => - mapQueuedAnalyticsEventToIngestEvent(event, standardizedProperties, sessionId), + mapQueuedAnalyticsEventToIngestEvent(event), ); return sendWithInlineRetry(ingestBatch).pipe( @@ -262,8 +258,8 @@ export class AnalyticsService extends Context.Service()( if (failure.status === 413 && queuedBatch.length > 1) { const midpoint = Math.ceil(queuedBatch.length / 2); return Effect.gen(function* () { - yield* processQueuedBatch(queuedBatch.slice(0, midpoint), standardizedProperties); - yield* processQueuedBatch(queuedBatch.slice(midpoint), standardizedProperties); + yield* processQueuedBatch(queuedBatch.slice(0, midpoint)); + yield* processQueuedBatch(queuedBatch.slice(midpoint)); }); } @@ -289,10 +285,16 @@ export class AnalyticsService extends Context.Service()( }; const capture = (eventName: string, properties: Record = {}) => - Effect.sync(() => { + Effect.gen(function* () { const normalized = eventName.trim(); if (!normalized) return; - const queued = createQueuedAnalyticsEvent(normalized, properties); + const distinctId = yield* identityManager.getDistinctId(); + const standardized = yield* getStandardizedProperties(); + const snapshot = sdkConfiguration.measurementRuntime.getAnalyticsCaptureSnapshot( + distinctId, + standardized, + ); + const queued = createQueuedAnalyticsEvent(normalized, properties, snapshot); // Direct mutation inside `Effect.sync` is safe: the Effect runtime // guarantees no other fiber crosses this sync boundary. const next = [...queueRef.ref.current, queued]; @@ -306,11 +308,9 @@ export class AnalyticsService extends Context.Service()( const flush = () => Effect.gen(function* () { - const standardizedProperties = yield* getStandardizedProperties(); - let batch = yield* takeDueBatch(); while (batch.length > 0) { - yield* processQueuedBatch(batch, standardizedProperties); + yield* processQueuedBatch(batch); batch = yield* takeDueBatch(); } }); @@ -321,12 +321,18 @@ export class AnalyticsService extends Context.Service()( properties: Record; }>, ) => - Effect.sync(() => { + Effect.gen(function* () { const additions: QueuedAnalyticsEvent[] = []; + const distinctId = yield* identityManager.getDistinctId(); + const standardized = yield* getStandardizedProperties(); + const snapshot = sdkConfiguration.measurementRuntime.getAnalyticsCaptureSnapshot( + distinctId, + standardized, + ); for (const event of events) { const normalized = event.eventName.trim(); if (!normalized) continue; - additions.push(createQueuedAnalyticsEvent(normalized, event.properties)); + additions.push(createQueuedAnalyticsEvent(normalized, event.properties, snapshot)); } if (additions.length === 0) return; queueRef.ref.current = [...queueRef.ref.current, ...additions]; @@ -351,14 +357,29 @@ export class AnalyticsService extends Context.Service()( const additions: QueuedAnalyticsEvent[] = []; if (!previousAppRelease) { - additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_INSTALLED, {})); + const distinctId = yield* identityManager.getDistinctId(); + const snapshot = sdkConfiguration.measurementRuntime.getAnalyticsCaptureSnapshot( + distinctId, + standardizedProps, + ); + additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_INSTALLED, {}, snapshot)); } else if ( previousAppRelease.appBuild !== currentAppRelease.appBuild || previousAppRelease.appVersion !== currentAppRelease.appVersion ) { - additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_UPDATED, {})); + const distinctId = yield* identityManager.getDistinctId(); + const snapshot = sdkConfiguration.measurementRuntime.getAnalyticsCaptureSnapshot( + distinctId, + standardizedProps, + ); + additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_UPDATED, {}, snapshot)); } - additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_OPENED, {})); + const distinctId = yield* identityManager.getDistinctId(); + const snapshot = sdkConfiguration.measurementRuntime.getAnalyticsCaptureSnapshot( + distinctId, + standardizedProps, + ); + additions.push(createQueuedAnalyticsEvent(AUTOMATIC_EVENTS.APP_OPENED, {}, snapshot)); queueRef.ref.current = [...queueRef.ref.current, ...additions]; diff --git a/libraries/react-native/src/core/analytics/types.ts b/libraries/react-native/src/core/analytics/types.ts index 1de637d4a..89c145403 100644 --- a/libraries/react-native/src/core/analytics/types.ts +++ b/libraries/react-native/src/core/analytics/types.ts @@ -7,11 +7,16 @@ export interface QueuedAnalyticsEvent { readonly eventTimestamp: string; readonly id: string; readonly properties: Record; + readonly context: Record; + readonly distinctId: string; + readonly sessionId?: string; } export interface AnalyticsIngestEvent { /** Shared metadata attached to every event (for example app, device, or SDK context). */ readonly context: Record; + /** Identity captured with this individual event, never resolved at batch time. */ + readonly distinct_id: string; /** Unique identifier for this event instance. */ readonly event_id: string; /** Canonical event name used for analytics processing. */ @@ -21,7 +26,7 @@ export interface AnalyticsIngestEvent { /** Event-specific payload fields for this event name. */ readonly properties: Record; /** Identifier that groups events belonging to the same user session. */ - readonly session_id: string; + readonly session_id?: string; } /** diff --git a/libraries/react-native/src/core/analytics/utils.ts b/libraries/react-native/src/core/analytics/utils.ts index 622de6bf8..e68e04331 100644 --- a/libraries/react-native/src/core/analytics/utils.ts +++ b/libraries/react-native/src/core/analytics/utils.ts @@ -7,6 +7,11 @@ import { PlatformProvider } from "../platform/platform-provider"; export const createQueuedAnalyticsEvent = ( eventName: string, properties: Record, + snapshot: { + readonly context: Record; + readonly distinctId: string; + readonly sessionId?: string; + }, ): QueuedAnalyticsEvent => ({ attempts: 0, availableAt: Date.now(), @@ -14,6 +19,9 @@ export const createQueuedAnalyticsEvent = ( eventTimestamp: new Date().toISOString(), id: getNonce(), properties, + context: snapshot.context, + distinctId: snapshot.distinctId, + sessionId: snapshot.sessionId, }); export const getAnalyticsStandardizedProperties = () => { @@ -62,16 +70,12 @@ export const getAnalyticsStandardizedProperties = () => { export const mapQueuedAnalyticsEventToIngestEvent = ( event: QueuedAnalyticsEvent, - standardizedProperties: Record, - sessionId: string, ) => ({ - context: {}, + context: event.context, + distinct_id: event.distinctId, event_id: event.id, event_name: event.eventName, event_ts: event.eventTimestamp, - properties: { - ...event.properties, - ...standardizedProperties, - }, - session_id: sessionId, + properties: event.properties, + session_id: event.sessionId, }); diff --git a/libraries/react-native/src/core/measurement/constants.ts b/libraries/react-native/src/core/measurement/constants.ts new file mode 100644 index 000000000..1c533cc0b --- /dev/null +++ b/libraries/react-native/src/core/measurement/constants.ts @@ -0,0 +1,70 @@ +/** Canonical measurement evidence type identifiers. */ +export const MEASUREMENT_RECORD_TYPES = { + INSTALLATION_CREATED: "installation.created.v1", + INSTALLATION_UPDATED: "installation.updated.v1", + SESSION_STARTED: "session.started.v1", + SESSION_ENDED: "session.ended.v1", + IDENTITY_CHANGED: "identity.changed.v1", + CONSENT_CHANGED: "consent.changed.v1", + LINK_RECEIVED: "link.received.v1", + LINK_RESOLVED: "link.resolved.v1", + LINK_ROUTED: "link.routed.v1", + ANDROID_INSTALL_REFERRER: "android.install_referrer.v1", + ANDROID_PREINSTALL: "android.preinstall.v1", + IOS_ADSERVICES: "ios.adservices.v1", + IOS_ATT_CHANGED: "ios.att.changed.v1", + IDENTIFIER_OBSERVED: "identifier.observed.v1", + PUSH_TOKEN: "push.token.v1", + PUSH_RECEIVED: "push.received.v1", + PUSH_OPENED: "push.opened.v1", + AD_REVENUE: "revenue.ad_impression.v1", + PURCHASE_OBSERVED: "purchase.observed.v1", + PURCHASE_VALIDATION_REQUESTED: "purchase.validation_requested.v1", + PURCHASE_VALIDATION_RESULT: "purchase.validation_result.v1", + DIAGNOSTIC_CAPABILITY: "diagnostic.capability.v1", + PARTNER_CONTEXT_CHANGED: "partner.context_changed.v1", +} as const; + +/** Standard event aliases routed exclusively through `client.capture`. */ +export const STANDARD_EVENTS = { + ADD_PAYMENT_INFO: "add payment info", + ADD_TO_CART: "add to cart", + ADD_TO_WISHLIST: "add to wishlist", + COMPLETE_REGISTRATION: "complete registration", + INITIATED_CHECKOUT: "initiated checkout", + INVITE_SHARED: "invite shared", + LEVEL_ACHIEVED: "level achieved", + LOCATION: "location", + LOGIN: "login", + PURCHASE: "purchase", + RATE: "rate", + SEARCH: "search", + SHARE: "share", + SPENT_CREDITS: "spent credits", + SUBSCRIBE: "subscribe", + TUTORIAL_COMPLETION: "tutorial completion", + UNLOCK_ACHIEVEMENT: "unlock achievement", + VIEWED_CONTENT: "viewed content", + /** Captured automatically by the notifications namespace on user open. */ + OPENED_FROM_PUSH_NOTIFICATION: "opened from push notification", +} as const; + +/** Standard aliases applications may emit themselves. */ +export const APP_EMITTABLE_STANDARD_EVENTS = Object.freeze( + Object.fromEntries( + Object.entries(STANDARD_EVENTS).filter(([key]) => key !== "OPENED_FROM_PUSH_NOTIFICATION"), + ) as Omit, +); + +/** Evidence types which may never be evicted for low-priority analytics. */ +export const NON_EVICTABLE_RECORD_TYPES = new Set([ + MEASUREMENT_RECORD_TYPES.INSTALLATION_CREATED, + MEASUREMENT_RECORD_TYPES.INSTALLATION_UPDATED, + MEASUREMENT_RECORD_TYPES.CONSENT_CHANGED, + MEASUREMENT_RECORD_TYPES.LINK_RECEIVED, + MEASUREMENT_RECORD_TYPES.LINK_RESOLVED, + MEASUREMENT_RECORD_TYPES.ANDROID_INSTALL_REFERRER, + MEASUREMENT_RECORD_TYPES.PURCHASE_OBSERVED, + MEASUREMENT_RECORD_TYPES.PURCHASE_VALIDATION_REQUESTED, + MEASUREMENT_RECORD_TYPES.PURCHASE_VALIDATION_RESULT, +]); diff --git a/libraries/react-native/src/core/measurement/diagnostics.ts b/libraries/react-native/src/core/measurement/diagnostics.ts new file mode 100644 index 000000000..313a948fa --- /dev/null +++ b/libraries/react-native/src/core/measurement/diagnostics.ts @@ -0,0 +1,98 @@ +export interface DiagnosticAuthorization { + readonly expiresAt: string; + readonly keyId: string; + readonly projectId: string; + readonly sessionId: string; + readonly signature: string; +} + +export interface RedactedDiagnosticEntry { + readonly fields: Readonly>; + readonly level: "debug" | "info" | "warning"; + readonly message: string; + readonly occurredAt: string; +} + +const protectedKey = /(?:url|uri|token|receipt|jws|phone|email|idfa|idfv|gaid|oaid|aaid|android.?id|imei|referrer|secret|password|authorization)/i; +const protectedText = /(?:[a-z][a-z\d+.-]*:\/\/\S+|[^\s@]+@[^\s@]+\.[^\s@]+|(?:token|receipt|authorization|password|secret)\s*[=:]\s*\S+)/gi; + +const redact = (value: unknown): unknown => { + if (typeof value === "string") return value.replace(protectedText, "[redacted]"); + if (Array.isArray(value)) return value.map(redact); + if (!value || typeof value !== "object") return value; + return Object.fromEntries(Object.entries(value).map(([key, nested]) => [ + key, + protectedKey.test(key) ? "[redacted]" : redact(nested), + ])); +}; + +const decodeBase64 = (value: string): ArrayBuffer => { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); + return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0)).buffer; +}; + +const authorizationPayload = (authorization: Omit): ArrayBuffer => + new TextEncoder().encode(JSON.stringify({ + expiresAt: authorization.expiresAt, + keyId: authorization.keyId, + projectId: authorization.projectId, + sessionId: authorization.sessionId, + })).buffer; + +/** Release-safe logger that requires an expiring project-bound Ed25519 authorization. */ +export class SecureDiagnosticLogger { + private authorizedUntil = 0; + + constructor( + private readonly projectId: string, + private readonly trustedKeys: ReadonlyMap, + private readonly releaseBuild: boolean, + private readonly sink: (entry: RedactedDiagnosticEntry) => void, + private readonly now: () => Date = () => new Date(), + ) {} + + /** Enables a release diagnostic session only after signature, project, and expiry validation. */ + async authorize(authorization: DiagnosticAuthorization): Promise { + const expiresAt = Date.parse(authorization.expiresAt); + const key = this.trustedKeys.get(authorization.keyId); + if ( + !key || + authorization.projectId !== this.projectId || + !Number.isFinite(expiresAt) || + expiresAt <= this.now().getTime() + ) return false; + try { + const verified = await crypto.subtle.verify( + "Ed25519", + key, + decodeBase64(authorization.signature), + authorizationPayload(authorization), + ); + if (!verified) return false; + this.authorizedUntil = expiresAt; + return true; + } catch { + return false; + } + } + + /** Emits a redacted diagnostic entry when the build/session policy allows it. */ + log( + level: RedactedDiagnosticEntry["level"], + message: string, + fields: Readonly> = {}, + ): boolean { + if (this.releaseBuild && this.authorizedUntil <= this.now().getTime()) return false; + this.sink({ + fields: redact(fields) as Readonly>, + level, + message: redact(message) as string, + occurredAt: this.now().toISOString(), + }); + return true; + } +} + +/** Encodes the canonical diagnostic-authorization payload for server and test signers. */ +export const encodeDiagnosticAuthorizationPayload = authorizationPayload; diff --git a/libraries/react-native/src/core/measurement/endpoints.ts b/libraries/react-native/src/core/measurement/endpoints.ts new file mode 100644 index 000000000..6f9314108 --- /dev/null +++ b/libraries/react-native/src/core/measurement/endpoints.ts @@ -0,0 +1,76 @@ +import { MeasurementConfigurationError } from "./errors"; + +/** Endpoint and signed-configuration trust overrides for self-hosted deployments. */ +export interface MeasurementEndpointOverrides { + readonly api?: string; + readonly ingest?: string; + readonly links?: string; + readonly trustedConfigKeys?: ReadonlyArray<{ + readonly keyId: string; + readonly publicKey: string; + }>; + /** Project identifier signed into remote measurement configuration. */ + readonly configurationProjectId?: string; + readonly allowInsecureDebugTransport?: boolean; +} + +/** Normalized endpoint overrides safe to pass to native startup configuration. */ +export interface ResolvedMeasurementEndpoints { + readonly api: string; + readonly ingest: string; + readonly links: string; + readonly trustedConfigKeyIds: ReadonlyArray; +} + +const normalizeOrigin = (value: string, field: string, allowInsecure: boolean): string => { + let url: URL; + try { + url = new URL(value); + } catch { + throw new MeasurementConfigurationError(`${field} must be an absolute URL`, field); + } + if (url.protocol !== "https:" && !(allowInsecure && url.protocol === "http:")) { + throw new MeasurementConfigurationError(`${field} must use HTTPS`, field); + } + if (url.username || url.password || url.search || url.hash || url.pathname !== "/") { + throw new MeasurementConfigurationError( + `${field} must be an origin without credentials, query, fragment, or path`, + field, + ); + } + return url.origin; +}; + +/** Validates and resolves cloud or self-host endpoint configuration. */ +export const resolveMeasurementEndpoints = ( + overrides: MeasurementEndpointOverrides | undefined, + debug: boolean, +): ResolvedMeasurementEndpoints => { + const allowInsecure = debug && overrides?.allowInsecureDebugTransport === true; + const api = normalizeOrigin(overrides?.api ?? "https://api.voidhash.com", "endpoints.api", allowInsecure); + const ingest = normalizeOrigin(overrides?.ingest ?? api, "endpoints.ingest", allowInsecure); + const links = normalizeOrigin(overrides?.links ?? api, "endpoints.links", allowInsecure); + const keyIds = new Set(); + for (const key of overrides?.trustedConfigKeys ?? []) { + if (!key.keyId.trim() || !key.publicKey.trim()) { + throw new MeasurementConfigurationError( + "Trusted configuration keys require non-empty keyId and publicKey", + "endpoints.trustedConfigKeys", + ); + } + if (keyIds.has(key.keyId)) { + throw new MeasurementConfigurationError( + `Duplicate trusted configuration key '${key.keyId}'`, + "endpoints.trustedConfigKeys", + ); + } + keyIds.add(key.keyId); + } + if (keyIds.size > 0 && !overrides?.configurationProjectId?.trim()) { + throw new MeasurementConfigurationError( + "configurationProjectId is required when trusted configuration keys are supplied", + "endpoints.configurationProjectId", + ); + } + return { api, ingest, links, trustedConfigKeyIds: [...keyIds].sort() }; +}; diff --git a/libraries/react-native/src/core/measurement/errors.ts b/libraries/react-native/src/core/measurement/errors.ts new file mode 100644 index 000000000..7d70f9c04 --- /dev/null +++ b/libraries/react-native/src/core/measurement/errors.ts @@ -0,0 +1,106 @@ +/** Stable error codes exposed by all unified measurement namespaces. */ +export type MeasurementErrorCode = + | "capabilityUnavailable" + | "invalidConfiguration" + | "invalidInput" + | "policyBlocked" + | "notInitialized" + | "transport" + | "timeout" + | "unknownNative"; + +/** Base typed error for measurement, links, consent, and notifications. */ +export class MeasurementError extends Error { + readonly code: MeasurementErrorCode; + readonly source: "ios" | "android" | "core"; + readonly detail?: Readonly>; + + constructor(options: { + readonly code: MeasurementErrorCode; + readonly message: string; + readonly source?: "ios" | "android" | "core"; + readonly detail?: Readonly>; + readonly cause?: unknown; + }) { + super(options.message, { cause: options.cause }); + this.name = "MeasurementError"; + this.code = options.code; + this.source = options.source ?? "core"; + this.detail = options.detail; + } +} + +/** Raised when a platform or build does not contain a requested capability. */ +export class MeasurementCapabilityUnavailable extends MeasurementError { + readonly capability: string; + readonly reason: "notConfigured" | "notImplemented" | "notInstalled" | "unsupported" | "disabled"; + + constructor(capability: string, reason: MeasurementCapabilityUnavailable["reason"]) { + super({ + code: "capabilityUnavailable", + message: `Measurement capability '${capability}' is ${reason}`, + detail: { capability, reason }, + }); + this.name = "MeasurementCapabilityUnavailable"; + this.capability = capability; + this.reason = reason; + } +} + +/** Raised when a configuration patch violates a documented invariant. */ +export class MeasurementConfigurationError extends MeasurementError { + constructor(message: string, field?: string) { + super({ + code: "invalidConfiguration", + message, + detail: field ? { field } : undefined, + }); + this.name = "MeasurementConfigurationError"; + } +} + +/** Raised for malformed or unsafe public inputs. */ +export class MeasurementInputError extends MeasurementError { + constructor(message: string, field?: string) { + super({ code: "invalidInput", message, detail: field ? { field } : undefined }); + this.name = "MeasurementInputError"; + } +} + +/** Raised when an effective collection policy denies an operation. */ +export class MeasurementPolicyBlocked extends MeasurementError { + constructor(category: string) { + super({ + code: "policyBlocked", + message: `Measurement policy blocks '${category}'`, + detail: { category }, + }); + this.name = "MeasurementPolicyBlocked"; + } +} + +/** Converts a structured native bridge failure into the public hierarchy. */ +export const mapNativeMeasurementError = (input: { + readonly code: string; + readonly message?: string; + readonly source?: "ios" | "android" | "core"; +}): MeasurementError => { + const knownCodes = new Set([ + "capabilityUnavailable", + "invalidConfiguration", + "invalidInput", + "policyBlocked", + "notInitialized", + "transport", + "timeout", + "unknownNative", + ]); + const code = knownCodes.has(input.code as MeasurementErrorCode) + ? (input.code as MeasurementErrorCode) + : "unknownNative"; + return new MeasurementError({ + code, + message: input.message?.slice(0, 256) || "Native measurement operation failed", + source: input.source, + }); +}; diff --git a/libraries/react-native/src/core/measurement/index.ts b/libraries/react-native/src/core/measurement/index.ts new file mode 100644 index 000000000..b06a147c6 --- /dev/null +++ b/libraries/react-native/src/core/measurement/index.ts @@ -0,0 +1,10 @@ +export * from "./constants"; +export * from "./errors"; +export * from "./endpoints"; +export * from "./runtime"; +export * from "./protected-identity"; +export * from "./policy"; +export * from "./protected-fields"; +export * from "./support-bundle"; +export * from "./signed-config"; +export * from "./types"; diff --git a/libraries/react-native/src/core/measurement/native-adapter.ts b/libraries/react-native/src/core/measurement/native-adapter.ts new file mode 100644 index 000000000..8a53a2957 --- /dev/null +++ b/libraries/react-native/src/core/measurement/native-adapter.ts @@ -0,0 +1,228 @@ +import { Measurement, Notifications } from "../../nitro"; +import type { MeasurementCommand } from "../../specs/measurement/MeasurementTypes.nitro"; +import type { PushPermissionStatus } from "./types"; +import type { MeasurementRuntimeAdapter } from "./runtime"; + +const permissionStatuses = new Set([ + "notDetermined", + "denied", + "authorized", + "provisional", + "ephemeral", + "notRequired", +]); + +const commandKind = (recordType: string): MeasurementCommand["kind"] => { + if (recordType.startsWith("session.")) return "sessionSignal"; + if (recordType.startsWith("identity.")) return "identityTransition"; + if (recordType.startsWith("consent.")) return "consentTransition"; + if (recordType.startsWith("link.") || recordType.startsWith("referrer.")) return "linkInput"; + if (recordType.startsWith("push.")) return "pushInput"; + if (recordType.startsWith("purchase.")) return "purchaseInput"; + if (recordType.startsWith("identifier.")) return "identifierInput"; + return "enqueueRecord"; +}; + +/** Creates the React Native bridge adapter used by the framework-independent coordinator. */ +export const createNativeMeasurementRuntimeAdapter = (): MeasurementRuntimeAdapter => { + let nativeWrites = Promise.resolve(); + return { + isReleaseBuild: typeof __DEV__ === "boolean" ? !__DEV__ : true, + initializeMeasurement: async (publishableKey, configuration) => { + const state = await Measurement.initialize(publishableKey, { + ...configuration, + trustedConfigKeyIds: [...configuration.trustedConfigKeyIds], + }); + return { + installationId: state.installationId, + firstOpenedAt: state.firstOpenedAt, + installationSequence: state.installationSequence, + }; + }, + enqueueMeasurement: (command) => { + nativeWrites = nativeWrites.then(async () => { + const result = await Measurement.enqueue({ + commandId: command.commandId, + kind: commandKind(command.recordType), + recordType: command.recordType, + occurredAt: command.occurredAt, + source: command.source, + priority: command.priority, + publicPayload: new TextEncoder().encode(JSON.stringify(command.envelope)).buffer, + protectedEvidenceRef: command.protectedPayload, + identity: command.identity, + consent: command.consent, + session: command.session, + }); + if (!result.accepted) throw new Error(result.error?.code ?? "NATIVE_ENQUEUE_REJECTED"); + }); + return nativeWrites; + }, + flushMeasurement: async () => { + await nativeWrites; + return Measurement.flush(); + }, + putProtectedEvidence: (input) => { + let result = input.blobId; + nativeWrites = nativeWrites.then(async () => { + result = await Measurement.putProtectedEvidence({ + blobId: input.blobId, + purpose: input.purpose, + consentRevision: input.consentRevision, + retentionClass: input.retentionClass, + value: new Uint8Array(input.value).buffer, + }); + }); + return nativeWrites.then(() => result); + }, + deleteProtectedEvidence: (blobId) => { + let deleted = false; + nativeWrites = nativeWrites.then(async () => { + deleted = await Measurement.deleteProtectedEvidence(blobId); + }); + return nativeWrites.then(() => deleted); + }, + deleteProtectedData: (requestId) => { + let deleted = false; + nativeWrites = nativeWrites.then(async () => { + deleted = await Measurement.deleteProtectedData(requestId); + }); + return nativeWrites.then(() => deleted); + }, + getMeasurementConfigurationState: async () => { + await nativeWrites; + const state = await Measurement.getMeasurementConfigurationState(); + return { + version: state.version, + payload: state.payload ? new Uint8Array(state.payload) : undefined, + }; + }, + persistMeasurementConfigurationState: (version, payload) => { + let persisted = false; + nativeWrites = nativeWrites.then(async () => { + persisted = await Measurement.persistMeasurementConfigurationState( + version, + new Uint8Array(payload).buffer, + ); + }); + return nativeWrites.then(() => persisted); + }, + applyMeasurementConfiguration: async (version, payload) => { + await nativeWrites; + await Measurement.applyMeasurementConfiguration( + version, + new TextEncoder().encode(JSON.stringify(payload)).buffer, + ); + }, + applyMeasurementStorageLimits: async (limits) => { + await nativeWrites; + await Measurement.applyMeasurementStorageLimits( + limits.maxOutboxRecords, + limits.maxOutboxBytes, + limits.maxProtectedBytes, + ); + }, + getPushRegistrationState: async () => { + await nativeWrites; + const state = await Measurement.getPushRegistrationState(); + return state.payload ? new Uint8Array(state.payload) : undefined; + }, + persistPushRegistrationState: (payload) => { + let persisted = false; + nativeWrites = nativeWrites.then(async () => { + persisted = await Measurement.persistPushRegistrationState( + new Uint8Array(payload).buffer, + ); + }); + return nativeWrites.then(() => persisted); + }, + clearPushRegistrationState: () => { + let cleared = false; + nativeWrites = nativeWrites.then(async () => { + cleared = await Measurement.clearPushRegistrationState(); + }); + return nativeWrites.then(() => cleared); + }, + getTestDeviceState: async () => { + await nativeWrites; + return Measurement.getTestDeviceState(); + }, + persistTestDeviceState: async (enabled) => { + await nativeWrites; + return Measurement.persistTestDeviceState(enabled); + }, + waitForPendingWrites: () => nativeWrites, + hasDedupe: async (namespace, key) => { + await nativeWrites; + return Measurement.hasDedupe(namespace, key); + }, + checkAndSetDedupe: async (namespace, key, expiresAtMs) => { + await nativeWrites; + return Measurement.checkAndSetDedupe(namespace, key, expiresAtMs); + }, + getPermissionStatus: async () => { + const status = await Notifications.getPermissionStatus(); + return permissionStatuses.has(status as PushPermissionStatus) + ? (status as PushPermissionStatus) + : "notDetermined"; + }, + requestPermission: async (options) => { + const status = await Notifications.requestPermission(options?.provisional ?? false); + return permissionStatuses.has(status as PushPermissionStatus) + ? (status as PushPermissionStatus) + : "notDetermined"; + }, + getPushToken: () => Notifications.getToken(), + setBadgeCount: (count) => Notifications.setBadgeCount(count), + subscribeNotificationEvents: (listener) => { + const subscriptionId = `notification-${Date.now()}-${Math.random().toString(36).slice(2)}`; + Notifications.subscribe(subscriptionId, listener); + return () => Notifications.unsubscribe(subscriptionId); + }, + subscribeNativeInbox: (listener) => { + let active = true; + let timer: ReturnType | undefined; + let draining = false; + const schedule = (delay: number) => { + if (!active) return; + timer = setTimeout(() => void drain(), delay); + }; + const drain = async () => { + if (!active || draining) return; + draining = true; + try { + await nativeWrites; + while (active) { + const entries = await Measurement.peekInbox(32); + if (entries.length === 0) break; + for (const entry of entries) { + if (!active) break; + const payload = await Measurement.readProtectedEvidence(entry.protectedEvidenceRef); + await listener({ + id: entry.id, + kind: entry.kind, + source: entry.source, + appState: entry.appState, + receivedAt: entry.receivedAt, + value: new TextDecoder().decode(payload), + protectedEvidenceRef: entry.protectedEvidenceRef, + }); + await Measurement.acknowledgeInbox(entry.id); + } + if (entries.length < 32) break; + } + } catch { + // The entry remains unacknowledged and is retried on the next drain. + } finally { + draining = false; + schedule(250); + } + }; + void drain(); + return () => { + active = false; + if (timer) clearTimeout(timer); + }; + }, + }; +}; diff --git a/libraries/react-native/src/core/measurement/policy.ts b/libraries/react-native/src/core/measurement/policy.ts new file mode 100644 index 000000000..181a0cc5b --- /dev/null +++ b/libraries/react-native/src/core/measurement/policy.ts @@ -0,0 +1,100 @@ +import type { CollectionPolicy, ConsentSnapshot, PartnerSharingPolicy } from "./types"; + +export type MeasurementCollectionCategory = + | "analytics" + | "attribution" + | "advertisingIdentifier" + | "vendorIdentifier" + | "networkMetadata" + | "location" + | "protectedEmail" + | "protectedPhone" + | "upload"; + +export interface MeasurementPolicyDecision { + readonly allowed: boolean; + readonly category: MeasurementCollectionCategory; + readonly reason: + | "allowed" + | "collection-opt-out" + | "consent-denied" + | "disabled" + | "manual-only" + | "upload-paused"; +} + +/** Evaluates one collection input against configuration and the current consent revision. */ +export const evaluateMeasurementCollection = ( + category: MeasurementCollectionCategory, + policy: CollectionPolicy, + consent: ConsentSnapshot, + manuallySupplied = false, +): MeasurementPolicyDecision => { + if (consent.collectionOptOut === true) return { allowed: false, category, reason: "collection-opt-out" }; + switch (category) { + case "analytics": + return policy.analytics === "enabled" + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "disabled" }; + case "attribution": + return policy.attribution === "enabled" + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "disabled" }; + case "advertisingIdentifier": + if (policy.advertisingIdentifiers === "denied") return { allowed: false, category, reason: "disabled" }; + return policy.advertisingIdentifiers === "allowed" || consent.adStorage === true + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "consent-denied" }; + case "vendorIdentifier": + if (policy.vendorIdentifiers === "denied") return { allowed: false, category, reason: "disabled" }; + return policy.vendorIdentifiers === "allowed" || consent.dataUsage === true + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "consent-denied" }; + case "networkMetadata": + return policy.networkMetadata === "allowed" + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "disabled" }; + case "location": + if (policy.location === "denied") return { allowed: false, category, reason: "disabled" }; + return manuallySupplied + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "manual-only" }; + case "protectedEmail": + case "protectedPhone": + return consent.dataUsage === true + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "consent-denied" }; + case "upload": + return policy.upload === "enabled" + ? { allowed: true, category, reason: "allowed" } + : { allowed: false, category, reason: "upload-paused" }; + } +}; + +export interface PartnerPayloadDecision>> { + readonly allowed: boolean; + readonly consentRevision: number; + readonly payload?: Partial; + readonly reason: "allowed" | "partner-sharing-disabled" | "partner-excluded" | "consent-denied"; +} + +/** Applies send-time partner and field exclusions without mutating source evidence. */ +export const filterPartnerPayload = >>( + partner: string, + payload: T, + policy: PartnerSharingPolicy | undefined, + consent: ConsentSnapshot, +): PartnerPayloadDecision => { + if (consent.collectionOptOut === true || consent.partnerSharingOptOut === true) { + return { allowed: false, consentRevision: consent.revision, reason: "consent-denied" }; + } + if (policy?.mode === "disabled") { + return { allowed: false, consentRevision: consent.revision, reason: "partner-sharing-disabled" }; + } + if (policy?.excludedPartners?.includes(partner)) { + return { allowed: false, consentRevision: consent.revision, reason: "partner-excluded" }; + } + const excluded = new Set(policy?.excludedFields?.[partner] ?? []); + const filtered = Object.fromEntries(Object.entries(payload).filter(([key]) => !excluded.has(key))) as Partial; + return { allowed: true, consentRevision: consent.revision, payload: filtered, reason: "allowed" }; +}; diff --git a/libraries/react-native/src/core/measurement/protected-fields.ts b/libraries/react-native/src/core/measurement/protected-fields.ts new file mode 100644 index 000000000..842fa0870 --- /dev/null +++ b/libraries/react-native/src/core/measurement/protected-fields.ts @@ -0,0 +1,21 @@ +import { MeasurementInputError } from "./errors"; + +const PROTECTED_KEY = /(url|uri|token|receipt|jws|phone|email|idfa|idfv|gaid|oaid|aaid|android.?id|imei|referrer|secret|password|authorization)/i; + +/** Rejects protected keys and URL/email-shaped values from public data structures. */ +export const assertSafePublicValue = (value: unknown, path = "properties"): void => { + if (typeof value === "string") { + if (/^[a-z][a-z\d+.-]*:\/\//i.test(value) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + throw new MeasurementInputError(`Protected value is not allowed in ${path}`, path); + } + return; + } + if (!value || typeof value !== "object") return; + for (const [key, nested] of Object.entries(value)) { + const nestedPath = `${path}.${key}`; + if (PROTECTED_KEY.test(key)) { + throw new MeasurementInputError(`Protected field is not allowed in ${path}`, nestedPath); + } + assertSafePublicValue(nested, nestedPath); + } +}; diff --git a/libraries/react-native/src/core/measurement/protected-identity.ts b/libraries/react-native/src/core/measurement/protected-identity.ts new file mode 100644 index 000000000..9a977514a --- /dev/null +++ b/libraries/react-native/src/core/measurement/protected-identity.ts @@ -0,0 +1,79 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { bytesToHex } from "@noble/hashes/utils.js"; + +import { MeasurementInputError } from "./errors"; + +export interface ProtectedIdentityValue { + readonly value: string; + readonly format?: "plaintext" | "sha256"; +} + +export interface ProtectedIdentityTraits { + readonly emails?: ReadonlyArray; + readonly phones?: ReadonlyArray; + readonly clearEmails?: boolean; + readonly clearPhones?: boolean; +} + +export interface NormalizedProtectedIdentityValue { + readonly hash: string; + readonly normalized?: string; + readonly provenance: "sdk-hashed" | "caller-hashed"; +} + +export interface ProtectedIdentityUpdateResult { + readonly status: "stored" | "policyBlocked" | "disabled"; + readonly references: ReadonlyArray; + readonly cleared: ReadonlyArray<"email" | "phone">; +} + +/** Normalizes an email address before protected hashing and storage. */ +export const normalizeProtectedEmail = (value: string): string => { + const normalized = value.normalize("NFKC").trim().toLowerCase(); + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) || normalized.length > 320) { + throw new MeasurementInputError("Email must be a valid address", "email"); + } + return normalized; +}; + +/** Normalizes a phone number to the E.164 representation. */ +export const normalizeProtectedPhone = (value: string): string => { + const compact = value.normalize("NFKC").trim().replace(/[\s().-]/g, ""); + const normalized = compact.startsWith("00") ? `+${compact.slice(2)}` : compact; + if (!/^\+[1-9]\d{7,14}$/.test(normalized)) { + throw new MeasurementInputError("Phone must use E.164 format", "phone"); + } + return normalized; +}; + +/** Computes the lowercase SHA-256 value used for protected identity matching. */ +export const hashProtectedIdentityValue = (value: string): string => + bytesToHex(sha256(new TextEncoder().encode(value))); + +const normalizeValue = ( + input: string | ProtectedIdentityValue, + normalize: (value: string) => string, +): NormalizedProtectedIdentityValue => { + const descriptor = typeof input === "string" ? { value: input, format: "plaintext" as const } : input; + if (descriptor.format === "sha256") { + const hash = descriptor.value.trim().toLowerCase(); + if (!/^[a-f\d]{64}$/.test(hash)) { + throw new MeasurementInputError("Pre-hashed identity values must be lowercase SHA-256", "hash"); + } + return { hash, provenance: "caller-hashed" }; + } + const normalized = normalize(descriptor.value); + return { hash: hashProtectedIdentityValue(normalized), normalized, provenance: "sdk-hashed" }; +}; + +/** Normalizes and hashes protected email and phone traits deterministically. */ +export const normalizeProtectedIdentityTraits = (traits: ProtectedIdentityTraits) => ({ + emails: [...new Map((traits.emails ?? []).map((value) => { + const normalized = normalizeValue(value, normalizeProtectedEmail); + return [normalized.hash, normalized] as const; + })).values()], + phones: [...new Map((traits.phones ?? []).map((value) => { + const normalized = normalizeValue(value, normalizeProtectedPhone); + return [normalized.hash, normalized] as const; + })).values()], +}); diff --git a/libraries/react-native/src/core/measurement/runtime.ts b/libraries/react-native/src/core/measurement/runtime.ts new file mode 100644 index 000000000..17a211efd --- /dev/null +++ b/libraries/react-native/src/core/measurement/runtime.ts @@ -0,0 +1,2013 @@ +import { SDK_VERSION } from "../constants"; +import { getNonce } from "../utils/crypto"; +import { MEASUREMENT_RECORD_TYPES, NON_EVICTABLE_RECORD_TYPES, STANDARD_EVENTS } from "./constants"; +import { + MeasurementCapabilityUnavailable, + MeasurementConfigurationError, + MeasurementError, + MeasurementInputError, + MeasurementPolicyBlocked, +} from "./errors"; +import { + normalizeProtectedIdentityTraits, + type ProtectedIdentityTraits, + type ProtectedIdentityUpdateResult, +} from "./protected-identity"; +import { assertSafePublicValue } from "./protected-fields"; +import { buildMeasurementSupportBundle } from "./support-bundle"; +import { evaluateMeasurementCollection } from "./policy"; +import { + fetchSignedMeasurementConfiguration, + SignedConfigurationRejected, +} from "./signed-config"; +import type { + AdRevenueInput, + AppSnapshot, + CollectionPolicy, + ConsentClient, + ConsentSnapshot, + ConsentState, + CrossPromotionInput, + CrossPromotionResult, + DeepLinkResult, + DeliveryDiagnostic, + DeviceSnapshot, + GeneratedLink, + IdentitySnapshot, + IncomingNotification, + InviteLinkInput, + JsonValue, + LinkConfiguration, + LinksClient, + MeasurementClient, + MeasurementConfiguration, + MeasurementConfigurationPatch, + MeasurementDebugState, + MeasurementEnvelopeV1, + MeasurementEventMap, + MeasurementFlushResult, + MeasurementHandleResult, + MeasurementInput, + NormalizedStorePurchaseState, + MeasurementPriority, + MeasurementState, + MeasurementStopOptions, + NotificationEventMap, + NotificationsClient, + NotificationsConfiguration, + ObservedPurchaseTransaction, + OpenedNotification, + PurchaseValidationInput, + PurchaseValidationResult, + PushPermissionOptions, + PushPermissionStatus, + PushRegistration, + SessionSnapshot, + SessionStartReason, + UrlSource, +} from "./types"; +import type { LinksCreateLinkRequest } from "@voidhash/generated-clients/links"; + +const DEFAULT_COLLECTION_POLICY: CollectionPolicy = { + analytics: "enabled", + attribution: "enabled", + advertisingIdentifiers: "consent-dependent", + vendorIdentifiers: "consent-dependent", + networkMetadata: "denied", + location: "denied", + upload: "enabled", +}; + +const DEFAULT_CONSENT: ConsentSnapshot = { + decidedAt: "1970-01-01T00:00:00.000Z", + revision: 0, + source: "unknown", +}; + +const MAX_JSON_DEPTH = 8; +const MAX_CONTEXT_BYTES = 16 * 1024; +const MAX_LINK_LENGTH = 8 * 1024; +const MAX_LINK_VALUE_LENGTH = 1024; +const MAX_OUTBOX_RECORDS = 10_000; +const MAX_OUTBOX_BYTES = 20 * 1024 * 1024; +const ISO_4217_CODES = new Set( + "AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BOV BRL BSD BTN BWP BYN BZD CAD CDF CHE CHF CHW CLF CLP CNY COP COU CRC CUC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HRK HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN MXV MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SLL SOS SRD SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD USN UYI UYU UYW UZS VED VES VND VUV WST XAF XAG XAU XBA XBB XBC XBD XCD XDR XOF XPD XPF XPT XSU XTS XUA XXX YER ZAR ZMW ZWG".split( + " ", + ), +); + +interface StoredEnvelope { + readonly envelope: MeasurementEnvelopeV1; + readonly priority: MeasurementPriority; + readonly bytes: number; + attempts: number; + eligibleAt: number; +} + +interface RuntimeState { + installationId: string; + firstOpenedAt: string; + sequence: number; + identity: IdentitySnapshot; + consent: ConsentSnapshot; + configuration: MeasurementState; + session?: SessionSnapshot; + sessionSequence: number; + readiness: MeasurementDebugState["session"]["readiness"]; + stopped: MeasurementStopOptions; + outbox: StoredEnvelope[]; + protectedEvidence: Map; + protectedIdentityReferences: Map<"email" | "phone", Set>; + dedupe: Map; + lastDelivery?: DeliveryDiagnostic; + permission: PushPermissionStatus; + registration?: PushRegistration; + badgeCount: number; + deletion: { requested: boolean; completed: boolean }; + testDevice: boolean; + signedConfiguration?: { + keyId: string; + version: number; + source: "network" | "persisted"; + }; + lastSignedConfigurationRejection?: string; +} + +interface RemoteMeasurementConfiguration { + readonly collectors: { + readonly appleAttributionEnabled: boolean; + readonly linkAllowedDomains: ReadonlyArray; + }; + readonly conversionRules: ReadonlyArray<{ + readonly coarseValue?: "low" | "medium" | "high"; + readonly eventName: string; + readonly fineValue: number; + readonly lockWindow?: boolean; + readonly minimumCount: number; + readonly window: number; + }>; + readonly schemaVersion: 1; + readonly storage: { + readonly maxOutboxBytes: number; + readonly maxOutboxRecords: number; + readonly maxProtectedBytes: number; + }; +} + +interface PersistedRemoteMeasurementConfiguration { + readonly keyId: string; + readonly payload: RemoteMeasurementConfiguration; +} + +/** Platform hooks used by the unified core. Native implementations may supply all hooks. */ +export interface MeasurementRuntimeAdapter { + readonly now?: () => Date; + readonly monotonicNowMs?: () => number; + readonly makeId?: (prefix: string) => string; + readonly fetch?: typeof globalThis.fetch; + readonly isReleaseBuild?: boolean; + readonly getPermissionStatus?: () => Promise; + readonly requestPermission?: (options?: PushPermissionOptions) => Promise; + readonly getPushToken?: () => Promise<{ readonly token: string; readonly provider: "apns" | "fcm"; readonly environment: "development" | "production" }>; + readonly setBadgeCount?: (count: number) => Promise; + readonly openUrl?: (url: string) => Promise; + readonly validatePurchase?: (input: PurchaseValidationInput & { readonly requestId: string }) => Promise<{ + readonly outcome: PurchaseValidationResult["outcome"]; + readonly storeState?: PurchaseValidationResult["storeState"]; + readonly failure?: PurchaseValidationResult["failure"]; + readonly protectedResponse?: string; + }>; + readonly initializeMeasurement?: ( + publishableKey: string, + configuration: { + readonly apiUrl: string; + readonly ingestUrl: string; + readonly linksUrl: string; + readonly trustedConfigKeyIds: ReadonlyArray; + }, + ) => Promise<{ + readonly installationId: string; + readonly firstOpenedAt: string; + readonly installationSequence: number; + }>; + readonly enqueueMeasurement?: (command: { + readonly commandId: string; + readonly recordType: string; + readonly occurredAt: string; + readonly source: MeasurementEnvelopeV1["source"]; + readonly priority: MeasurementPriority; + readonly envelope: MeasurementEnvelopeV1; + readonly protectedPayload?: string; + readonly identity: IdentitySnapshot; + readonly consent: ConsentSnapshot; + readonly session?: SessionSnapshot; + }) => Promise; + readonly flushMeasurement?: () => Promise; + readonly putProtectedEvidence?: (input: { + readonly blobId: string; + readonly purpose: "advertising-identifier" | "diagnostic-authorization" | "email" | "install-referrer" | "link-capture" | "partner-context" | "phone" | "purchase-receipt" | "push-token"; + readonly consentRevision: number; + readonly retentionClass: "ephemeral" | "installation" | "legal" | "transaction"; + readonly value: Uint8Array; + }) => Promise; + readonly deleteProtectedEvidence?: (blobId: string) => Promise; + readonly deleteProtectedData?: (requestId: string) => Promise; + readonly getMeasurementConfigurationState?: () => Promise<{ + readonly version: number; + readonly payload?: Uint8Array; + }>; + readonly persistMeasurementConfigurationState?: ( + version: number, + payload: Uint8Array, + ) => Promise; + readonly applyMeasurementStorageLimits?: ( + limits: RemoteMeasurementConfiguration["storage"], + ) => Promise; + readonly applyMeasurementConfiguration?: ( + version: number, + payload: RemoteMeasurementConfiguration, + ) => Promise; + readonly getPushRegistrationState?: () => Promise; + readonly persistPushRegistrationState?: (payload: Uint8Array) => Promise; + readonly clearPushRegistrationState?: () => Promise; + readonly getTestDeviceState?: () => Promise; + readonly persistTestDeviceState?: (enabled: boolean) => Promise; + readonly subscribeNotificationEvents?: (listener: (event: { + readonly id: string; + readonly kind: "received" | "opened" | "tokenChanged" | "registrationError"; + readonly occurredAt: string; + readonly protectedPayloadRef?: string; + readonly pushNotificationSendId?: string; + readonly link?: string; + readonly errorCode?: string; + }) => void) => () => void; + readonly subscribeNativeInbox?: (listener: (entry: { + readonly id: string; + readonly kind: string; + readonly source: string; + readonly appState: string; + readonly receivedAt: string; + readonly value: string; + readonly protectedEvidenceRef: string; + }) => Promise) => () => void; + readonly waitForPendingWrites?: () => Promise; + readonly checkAndSetDedupe?: (namespace: string, key: string, expiresAtMs: number) => Promise; + readonly hasDedupe?: (namespace: string, key: string) => Promise; +} + +/** Construction options for the framework-independent unified core. */ +export interface UnifiedMeasurementRuntimeOptions { + readonly publishableKey: string; + readonly baseUrl: string; + readonly ingestUrl?: string; + readonly linksUrl?: string; + readonly trustedConfigKeyIds?: ReadonlyArray; + readonly trustedConfigKeys?: ReadonlyArray<{ + readonly keyId: string; + readonly publicKeySpki: string; + }>; + readonly configurationProjectId?: string; + readonly platform: "ios" | "android"; + readonly bundleId?: string; + readonly appVersion?: string; + readonly appBuild?: string; + readonly platformVersion?: string; + readonly locale?: string; + readonly distinctId?: string; + readonly consent?: ConsentSnapshot; + readonly measurement?: MeasurementConfiguration; + readonly links?: LinkConfiguration; + readonly notifications?: NotificationsConfiguration; + readonly adapter?: MeasurementRuntimeAdapter; +} + +class TypedEventHub { + private readonly listeners = new Map void>>(); + + on(event: K, listener: (value: T[K]) => void): () => void { + const listeners = this.listeners.get(event) ?? new Set<(value: never) => void>(); + listeners.add(listener as (value: never) => void); + this.listeners.set(event, listeners); + return () => { + listeners.delete(listener as (value: never) => void); + }; + } + + emit(event: K, value: T[K]): void { + for (const listener of this.listeners.get(event) ?? []) listener(value as never); + } +} + +const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T; + +const jsonDepth = (value: JsonValue, depth = 0): number => { + if (value === null || typeof value !== "object") return depth; + if (Array.isArray(value)) { + return value.reduce((maximum, item) => Math.max(maximum, jsonDepth(item, depth + 1)), depth); + } + return Object.values(value).reduce( + (maximum, item) => Math.max(maximum, jsonDepth(item, depth + 1)), + depth, + ); +}; + +const assertJsonObject = (value: Readonly>, field: string): void => { + if (jsonDepth(value) > MAX_JSON_DEPTH) { + throw new MeasurementConfigurationError(`${field} exceeds the maximum nesting depth`, field); + } + if (new TextEncoder().encode(JSON.stringify(value)).byteLength > MAX_CONTEXT_BYTES) { + throw new MeasurementConfigurationError(`${field} exceeds the maximum encoded size`, field); + } +}; + +const assertIsoCurrency = (currency: string): string => { + const normalized = currency.trim().toUpperCase(); + if (!ISO_4217_CODES.has(normalized)) { + throw new MeasurementInputError("Currency must be an ISO 4217 alpha-3 code", "currency"); + } + try { + new Intl.NumberFormat("en", { currency: normalized, style: "currency" }).format(0); + } catch { + throw new MeasurementInputError("Currency must be an ISO 4217 alpha-3 code", "currency"); + } + return normalized; +}; + +const assertNormalizedStoreState = (state: NormalizedStorePurchaseState | undefined): void => { + if (!state || typeof state !== "object") return; + const allowed = new Set([ + "state", "productId", "subscriptionState", "test", "lineItems", "cancellation", + "pause", "offer", "replacement", "prepaid", "priceChange", + ]); + if (Object.keys(state).some((key) => !allowed.has(key))) throw new TypeError("Unknown normalized store state field"); + if (!new Set(["purchased", "pending", "cancelled", "refunded", "expired", "paused", "grace"]).has(state.state)) { + throw new TypeError("Unknown normalized store purchase state"); + } + if (state.lineItems?.some((item) => !item.productId || !Number.isSafeInteger(item.quantity) || item.quantity <= 0)) { + throw new TypeError("Invalid normalized store line item"); + } + if (state.priceChange?.currency) assertIsoCurrency(state.priceChange.currency); + if (state.priceChange?.price && !/^(?:0|[1-9]\d{0,15})(?:\.\d{1,8})?$/.test(state.priceChange.price)) { + throw new TypeError("Invalid normalized store price change"); + } +}; + +const mergeConfiguration = ( + current: MeasurementState, + patch: MeasurementConfigurationPatch, +): MeasurementState => { + const allowedKeys = new Set([ + "startMode", + "sessionTimeoutMs", + "context", + "defaultCurrency", + "localeOverride", + "collection", + "partnerSharing", + "partnerData", + "purchases", + "android", + "ios", + "protectedIdentity", + ]); + for (const key of Object.keys(patch)) { + if (!allowedKeys.has(key)) throw new MeasurementConfigurationError(`Unknown configuration key '${key}'`, key); + } + if (patch.sessionTimeoutMs !== undefined && (!Number.isInteger(patch.sessionTimeoutMs) || patch.sessionTimeoutMs < 0)) { + throw new MeasurementConfigurationError("sessionTimeoutMs must be a non-negative integer", "sessionTimeoutMs"); + } + if (patch.defaultCurrency !== undefined) assertIsoCurrency(patch.defaultCurrency); + if (patch.context) { + assertJsonObject(patch.context, "context"); + assertSafePublicValue(patch.context, "context"); + } + if (patch.partnerData) assertJsonObject(patch.partnerData, "partnerData"); + const previous = current.configuration; + const configuration = { + ...previous, + ...clone(patch), + collection: { ...DEFAULT_COLLECTION_POLICY, ...previous.collection, ...patch.collection }, + context: patch.context === undefined ? previous.context : clone(patch.context), + purchases: patch.purchases === undefined + ? previous.purchases + : { ...previous.purchases, ...clone(patch.purchases) }, + }; + if (configuration.defaultCurrency) configuration.defaultCurrency = configuration.defaultCurrency.toUpperCase(); + return { configuration, revision: current.revision + 1 }; +}; + +const consentState = (snapshot: ConsentSnapshot, policy: CollectionPolicy): ConsentState => { + const optedOut = snapshot.collectionOptOut === true; + return { + snapshot: clone(snapshot), + effective: { + analytics: !optedOut && policy.analytics === "enabled", + attribution: !optedOut && policy.attribution === "enabled", + partnerSharing: !optedOut && snapshot.partnerSharingOptOut !== true, + upload: !optedOut && policy.upload === "enabled", + }, + }; +}; + +const priorityForRecord = (type: string): MeasurementPriority => { + if (type.startsWith("installation.") || type.startsWith("consent.") || type.includes("delete")) return "critical"; + if (type.startsWith("link.") || type.startsWith("purchase.") || type.startsWith("revenue.")) return "high"; + if (type.startsWith("diagnostic.")) return "low"; + return "normal"; +}; + +/** + * Framework-independent implementation of the four unified SDK namespaces. + * Native collectors feed this coordinator through the `internal*` methods; + * application code uses only `measurement`, `links`, `consent`, and `notifications`. + */ +export class UnifiedMeasurementRuntime { + readonly measurement: MeasurementClient; + readonly links: LinksClient; + readonly consent: ConsentClient; + readonly notifications: NotificationsClient; + + private readonly measurementEvents = new TypedEventHub(); + private readonly notificationEvents = new TypedEventHub(); + private readonly linkEvents = new TypedEventHub<{ deepLink: DeepLinkResult }>(); + private readonly adapter: MeasurementRuntimeAdapter; + private readonly options: UnifiedMeasurementRuntimeOptions; + private readonly linkConfiguration: LinkConfiguration; + private state: RuntimeState; + private lastBackgroundAt?: number; + private notificationRegistrationInFlight?: Promise; + private notificationSubscription?: () => void; + private nativeInboxSubscription?: () => void; + private pendingPartnerData?: MeasurementConfiguration["partnerData"]; + private purchaseEnrichment?: NonNullable["enrichment"]; + + constructor(options: UnifiedMeasurementRuntimeOptions) { + this.options = options; + this.adapter = options.adapter ?? {}; + const { wrappedDomainHeaderProvider, ...serializableLinkConfiguration } = options.links ?? {}; + this.linkConfiguration = { + ...clone(serializableLinkConfiguration), + wrappedDomainHeaderProvider, + }; + const now = this.now(); + const { partnerData, purchases, ...initialMeasurement } = options.measurement ?? {}; + this.pendingPartnerData = partnerData; + this.purchaseEnrichment = purchases?.enrichment; + const { enrichment: _, ...serializablePurchases } = purchases ?? {}; + const initialConfiguration: MeasurementState = { + revision: 1, + configuration: { + startMode: options.measurement?.startMode ?? "automatic", + sessionTimeoutMs: options.measurement?.sessionTimeoutMs ?? 30_000, + ...clone(initialMeasurement), + purchases: purchases ? clone(serializablePurchases) : undefined, + collection: { ...DEFAULT_COLLECTION_POLICY, ...options.measurement?.collection }, + }, + }; + this.state = { + installationId: this.makeId("install"), + firstOpenedAt: now, + sequence: 0, + identity: { distinctId: options.distinctId ?? `vh:anon:${this.makeId("identity")}`, revision: 1 }, + consent: clone(options.consent ?? DEFAULT_CONSENT), + configuration: initialConfiguration, + sessionSequence: 0, + readiness: "uninitialized", + stopped: {}, + outbox: [], + protectedEvidence: new Map(), + protectedIdentityReferences: new Map(), + dedupe: new Map(), + permission: "notDetermined", + badgeCount: 0, + deletion: { requested: false, completed: false }, + testDevice: false, + }; + + this.measurement = { + configure: (patch) => this.configure(patch), + start: (startOptions) => this.start(startOptions?.reason), + stop: (stopOptions) => this.stop(stopOptions), + handle: (input) => this.handleMeasurementInput(input), + on: (event, listener) => this.measurementEvents.on(event, listener), + getState: () => this.getState(), + createSupportBundle: async () => buildMeasurementSupportBundle(await this.getState(), this.adapter.now?.() ?? new Date()), + getInstallationId: () => Promise.resolve(this.state.installationId), + createInviteLink: (input) => this.createInviteLink(input), + trackInviteShare: (input) => this.trackInviteShare(input), + trackCrossPromotion: (input) => this.trackCrossPromotion(input), + trackAdRevenue: (input) => this.trackAdRevenue(input), + validatePurchase: (input) => this.validatePurchase(input), + deleteData: () => this.deleteData(), + setTestDevice: (enabled) => this.setTestDevice(enabled), + }; + this.links = { + handle: (input) => this.handleLink(input), + on: (_event, listener) => this.linkEvents.on("deepLink", listener), + }; + this.consent = { + set: (snapshot) => this.setConsent(snapshot), + get: () => Promise.resolve(this.getConsentState()), + }; + this.notifications = { + getPermissionStatus: () => this.getPermissionStatus(), + requestPermission: (permissionOptions) => this.requestPermission(permissionOptions), + register: () => this.registerNotifications(), + unregister: () => this.unregisterNotifications(), + getRegistration: () => Promise.resolve(this.state.registration && clone(this.state.registration)), + setBadgeCount: (count) => this.setBadgeCount(count), + on: (event, listener) => this.notificationEvents.on(event, listener), + }; + } + + /** Advances readiness and records first-install evidence before app events. */ + async initialize(): Promise { + if (this.state.readiness !== "uninitialized") return; + if (this.adapter.initializeMeasurement) { + const nativeState = await this.adapter.initializeMeasurement( + this.options.publishableKey, + { + apiUrl: this.options.baseUrl, + ingestUrl: this.options.ingestUrl ?? this.options.baseUrl, + linksUrl: this.options.linksUrl ?? this.options.baseUrl, + trustedConfigKeyIds: this.options.trustedConfigKeyIds ?? [], + }, + ); + this.state.installationId = nativeState.installationId; + this.state.firstOpenedAt = nativeState.firstOpenedAt; + this.state.sequence = nativeState.installationSequence; + } + await this.loadSignedMeasurementConfiguration(); + if (this.pendingPartnerData) { + await this.persistPartnerData(this.pendingPartnerData); + this.pendingPartnerData = undefined; + } + this.state.testDevice = await this.adapter.getTestDeviceState?.() ?? false; + await this.hydratePushRegistration(); + if (!this.notificationSubscription && this.adapter.subscribeNotificationEvents) { + this.notificationSubscription = this.adapter.subscribeNotificationEvents((event) => { + void this.handleNativeNotificationEvent(event); + }); + } + if (!this.nativeInboxSubscription && this.adapter.subscribeNativeInbox) { + this.nativeInboxSubscription = this.adapter.subscribeNativeInbox(async (entry) => { + if (entry.kind !== "link") return; + const source = this.nativeLinkSource(entry.source); + await this.handleLink( + { source, url: entry.value, receivedAt: entry.receivedAt }, + entry.protectedEvidenceRef, + ); + }); + } + this.state.readiness = "nativeInitialized"; + this.enqueue(MEASUREMENT_RECORD_TYPES.INSTALLATION_CREATED, { + firstOpenedAt: this.state.firstOpenedAt, + appVersion: this.options.appVersion, + appBuild: this.options.appBuild, + collectorCapabilities: Object.keys(this.collectorStates()), + }, "native"); + this.state.readiness = "collectorsReady"; + this.state.readiness = "sdkReady"; + if (this.state.configuration.configuration.startMode === "automatic") await this.start("coldStart"); + if (this.state.configuration.configuration.startMode === "consent-gated" && this.getConsentState().effective.analytics) { + await this.start("coldStart"); + } + if (this.options.notifications?.registration === "automatic") { + const permission = await this.getPermissionStatus(); + if (permission === "authorized" || permission === "provisional" || permission === "notRequired") { + await this.registerNotifications().catch((error) => this.emitRegistrationError(error)); + } + } + } + + private async handleNativeNotificationEvent(event: { + readonly id: string; + readonly kind: "received" | "opened" | "tokenChanged" | "registrationError"; + readonly occurredAt: string; + readonly protectedPayloadRef?: string; + readonly pushNotificationSendId?: string; + readonly link?: string; + readonly errorCode?: string; + }): Promise { + if (event.kind === "registrationError") { + this.emitRegistrationError(new MeasurementError({ + code: "unknownNative", + message: "Native push registration failed", + detail: { reason: event.errorCode ?? "unknown" }, + })); + return; + } + if (event.kind === "tokenChanged") { + if (this.state.registration) { + await this.refreshNotificationRegistration().catch((error) => this.emitRegistrationError(error)); + } else if (this.options.notifications?.registration === "automatic") { + await this.registerNotifications().catch((error) => this.emitRegistrationError(error)); + } + return; + } + const notification: IncomingNotification = { + id: event.id, + pushNotificationSendId: event.pushNotificationSendId, + receivedAt: event.occurredAt, + }; + if (event.kind === "received") { + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_RECEIVED, notification, "push", event.protectedPayloadRef, event.occurredAt); + this.notificationEvents.emit("received", notification); + return; + } + await this.internalOpenNotification(notification, event.link); + } + + private async hydratePushRegistration(): Promise { + const payload = await this.adapter.getPushRegistrationState?.(); + if (!payload) return; + try { + const registration = JSON.parse(new TextDecoder().decode(payload)) as PushRegistration; + if ( + typeof registration.pushDeviceTokenId === "string" && + (registration.provider === "apns" || registration.provider === "fcm") && + (registration.environment === "development" || registration.environment === "production") && + Number.isFinite(Date.parse(registration.registeredAt)) + ) { + this.state.registration = registration; + } + } catch { + await this.adapter.clearPushRegistrationState?.(); + } + } + + private async persistPushRegistration(registration: PushRegistration): Promise { + await this.adapter.persistPushRegistrationState?.( + new TextEncoder().encode(JSON.stringify(registration)), + ); + } + + private async loadSignedMeasurementConfiguration(): Promise { + const trustedKeys = this.options.trustedConfigKeys ?? []; + if (trustedKeys.length === 0) return; + const projectId = this.options.configurationProjectId; + if (!projectId) { + throw new MeasurementConfigurationError( + "configurationProjectId is required when trusted configuration keys are supplied", + "endpoints.configurationProjectId", + ); + } + + let persistedVersion = 0; + const persisted = await this.adapter.getMeasurementConfigurationState?.(); + if (persisted) persistedVersion = persisted.version; + if (persisted?.payload) { + try { + const decoded = JSON.parse( + new TextDecoder().decode(persisted.payload), + ) as PersistedRemoteMeasurementConfiguration; + await this.applyRemoteMeasurementConfiguration(decoded.payload, persisted.version); + this.state.signedConfiguration = { + keyId: decoded.keyId, + source: "persisted", + version: persisted.version, + }; + } catch { + this.state.lastSignedConfigurationRejection = "persisted-config-malformed"; + } + } + + try { + const accepted = await fetchSignedMeasurementConfiguration({ + endpoint: this.options.baseUrl, + expectedProjectId: projectId, + fetch: this.adapter.fetch, + persistedVersion, + publishableKey: this.options.publishableKey, + trustedKeys, + }); + await this.applyRemoteMeasurementConfiguration(accepted.payload, accepted.version); + const persistedPayload = new TextEncoder().encode(JSON.stringify({ + keyId: accepted.keyId, + payload: accepted.payload, + } satisfies PersistedRemoteMeasurementConfiguration)); + if (this.adapter.persistMeasurementConfigurationState) { + const stored = await this.adapter.persistMeasurementConfigurationState( + accepted.version, + persistedPayload, + ); + if (!stored) throw new SignedConfigurationRejected("version-replay"); + } + this.state.signedConfiguration = { + keyId: accepted.keyId, + source: "network", + version: accepted.version, + }; + this.state.lastSignedConfigurationRejection = undefined; + } catch (error) { + this.state.lastSignedConfigurationRejection = + error instanceof SignedConfigurationRejected ? error.code : "unavailable"; + } + } + + private async applyRemoteMeasurementConfiguration( + configuration: RemoteMeasurementConfiguration, + version: number, + ): Promise { + const limits = configuration.storage; + if ( + configuration.schemaVersion !== 1 || + !Number.isInteger(limits.maxOutboxRecords) || + limits.maxOutboxRecords < 1 || + !Number.isInteger(limits.maxOutboxBytes) || + limits.maxOutboxBytes < 1 || + !Number.isInteger(limits.maxProtectedBytes) || + limits.maxProtectedBytes < 1 + ) { + throw new SignedConfigurationRejected("malformed"); + } + await this.adapter.applyMeasurementConfiguration?.(version, configuration); + await this.adapter.applyMeasurementStorageLimits?.(limits); + } + + /** Records one product event with capture-time identity, consent, session, and configuration. */ + capture(eventName: string, properties: Readonly> = {}): string | undefined { + const normalized = eventName.trim(); + if (!normalized) return undefined; + if (this.state.stopped.collection || !this.getConsentState().effective.analytics) return undefined; + assertSafePublicValue(properties); + return this.enqueue("analytics.capture.v1", { + eventName: normalized, + properties: clone(properties), + measurementContext: clone(this.state.configuration.configuration.context ?? {}), + currency: this.state.configuration.configuration.defaultCurrency, + localeOverride: this.state.configuration.configuration.localeOverride, + }, "javascript"); + } + + /** Stores explicitly enabled email and phone identity traits only through the protected vault. */ + async setProtectedIdentityTraits(traits: ProtectedIdentityTraits): Promise { + const configuration = this.state.configuration.configuration.protectedIdentity; + if (configuration?.enabled !== true) { + return { status: "disabled", references: [], cleared: [] }; + } + const cleared: Array<"email" | "phone"> = []; + for (const [kind, requested] of [["email", traits.clearEmails], ["phone", traits.clearPhones]] as const) { + if (!requested) continue; + for (const reference of this.state.protectedIdentityReferences.get(kind) ?? []) { + this.state.protectedEvidence.delete(reference); + await this.adapter.deleteProtectedEvidence?.(reference); + } + this.state.protectedIdentityReferences.delete(kind); + cleared.push(kind); + } + if (this.state.consent.collectionOptOut === true || this.state.consent.dataUsage === false) { + this.enqueue("protected_identity.policy_blocked.v1", { + fields: [traits.emails?.length ? "email" : undefined, traits.phones?.length ? "phone" : undefined].filter(Boolean), + consentRevision: this.state.consent.revision, + }, "javascript"); + return { status: "policyBlocked", references: [], cleared }; + } + const normalized = normalizeProtectedIdentityTraits(traits); + const references: string[] = []; + for (const [kind, values, enabled] of [ + ["email", normalized.emails, configuration.email === true], + ["phone", normalized.phones, configuration.phone === true], + ] as const) { + if (!enabled && values.length > 0) continue; + for (const value of values) { + const reference = await this.persistProtected( + JSON.stringify(value), + kind, + "legal", + ); + const current = this.state.protectedIdentityReferences.get(kind) ?? new Set(); + current.add(reference); + this.state.protectedIdentityReferences.set(kind, current); + references.push(reference); + this.enqueue("protected_identity.updated.v1", { + field: kind, + protectedPayloadRef: reference, + provenance: value.provenance, + }, "javascript", reference); + } + } + return { status: "stored", references, cleared }; + } + + /** Captures a sequenced identity transition without modifying prior records. */ + setIdentity(distinctId: string, personId?: string): void { + const previous = clone(this.state.identity); + this.state.identity = { + distinctId, + personId, + anonymousId: distinctId.startsWith("vh:anon:") ? distinctId : previous.anonymousId, + revision: previous.revision + 1, + }; + this.enqueue(MEASUREMENT_RECORD_TYPES.IDENTITY_CHANGED, { + previous, + current: clone(this.state.identity), + }, "javascript"); + if (this.state.registration) { + void this.relinkNotificationRegistration().catch((error) => this.emitRegistrationError(error)); + } + } + + /** Returns a defensive snapshot of queued evidence for tests and native adapters. */ + inspectOutbox(): ReadonlyArray> { + return this.state.outbox.map((record) => clone(record.envelope)); + } + + /** Checks the durable native dedupe registry without mutating it. */ + async hasDurableDedupe(namespace: string, key: string): Promise { + if (this.adapter.hasDedupe) return this.adapter.hasDedupe(namespace, key); + return this.state.dedupe.has(`${namespace}:${key}`); + } + + /** Atomically records a durable dedupe key and reports whether it was newly inserted. */ + async checkAndSetDurableDedupe(namespace: string, key: string): Promise { + const expiresAtMs = Number.MAX_SAFE_INTEGER; + if (this.adapter.checkAndSetDedupe) return this.adapter.checkAndSetDedupe(namespace, key, expiresAtMs); + const namespaced = `${namespace}:${key}`; + if (this.state.dedupe.has(namespaced)) return false; + this.state.dedupe.set(namespaced, { at: this.nowMs() }); + return true; + } + + /** Records normalized store observation while retaining receipt material only in the protected vault. */ + async recordObservedPurchase(transaction: ObservedPurchaseTransaction): Promise { + const configuration = this.state.configuration.configuration.purchases; + if (configuration?.enabled !== true) return undefined; + const subscription = transaction.expirationDate !== undefined || transaction.isAutoRenewing !== undefined; + if (subscription && configuration.subscriptions === false) return undefined; + if (!subscription && configuration.inAppPurchases === false) return undefined; + const dedupeKey = `${transaction.platform}:${transaction.transactionId}:${transaction.purchaseDate}`; + if (await this.hasDurableDedupe("purchase-observed", dedupeKey)) return undefined; + const protectedPayloadRef = await this.persistProtected(JSON.stringify({ + appAccountToken: transaction.appAccountToken, + purchaseToken: transaction.purchaseToken, + receipt: transaction.receipt, + }), "purchase-receipt", "transaction"); + const purchaseKind = subscription ? "subscription" : "inApp"; + const enrichmentCallback = transaction.platform === "ios" + ? this.purchaseEnrichment?.ios + : this.purchaseEnrichment?.android?.[purchaseKind]; + let enrichment: Readonly> | undefined; + let enrichmentOutcome: "collected" | "failed" | "notConfigured" = "notConfigured"; + if (enrichmentCallback) { + try { + enrichment = clone(await enrichmentCallback(clone(transaction))); + assertJsonObject(enrichment, "purchases.enrichment"); + assertSafePublicValue(enrichment, "purchases.enrichment"); + enrichmentOutcome = "collected"; + } catch { + enrichment = undefined; + enrichmentOutcome = "failed"; + } + } + const recordId = `purchase_${dedupeKey.replace(/[^a-zA-Z\d_-]/g, "_").slice(0, 180)}`; + this.enqueue(MEASUREMENT_RECORD_TYPES.PURCHASE_OBSERVED, { + acknowledged: transaction.isAcknowledged, + environment: configuration.environment ?? "production", + expirationDate: transaction.expirationDate, + hasAccountToken: transaction.appAccountToken !== undefined, + originalTransactionId: transaction.originalTransactionId, + platform: transaction.platform, + productId: transaction.productId, + purchaseDate: transaction.purchaseDate, + purchaseKind, + purchaseState: transaction.purchaseState, + quantity: transaction.quantity, + transactionId: transaction.transactionId, + enrichment, + enrichmentOutcome, + }, "store", protectedPayloadRef, undefined, recordId); + await this.checkAndSetDurableDedupe("purchase-observed", dedupeKey); + return recordId; + } + + /** Hydrates the identity obtained during SDK initialization without inventing a transition. */ + internalHydrateIdentity(distinctId: string): void { + if (this.state.readiness === "uninitialized") { + this.state.identity = { ...this.state.identity, distinctId }; + return; + } + if (this.state.identity.distinctId !== distinctId) this.setIdentity(distinctId); + } + + /** Returns the exact event context and state snapshot used by analytics capture. */ + getAnalyticsCaptureSnapshot( + distinctId: string, + standardized: Readonly>, + ): { readonly context: Record; readonly distinctId: string; readonly sessionId?: string } { + return { + distinctId, + sessionId: this.state.session?.id, + context: { + schemaVersion: 1, + installation: { id: this.state.installationId, sequence: this.state.sequence + 1 }, + identity: { ...clone(this.state.identity), distinctId }, + consentRevision: this.state.consent.revision, + app: { + bundleId: standardized.$bundle_id ?? this.options.bundleId ?? null, + build: standardized.$app_build ?? this.options.appBuild ?? null, + name: standardized.$app_name ?? null, + version: standardized.$app_version ?? this.options.appVersion ?? null, + sdk: standardized.$sdk ?? "react-native", + sdkVersion: standardized.$sdk_version ?? SDK_VERSION, + }, + device: { + brand: standardized.$device_brand ?? null, + name: standardized.$device_name ?? null, + locale: this.state.configuration.configuration.localeOverride ?? standardized.$locale ?? null, + platform: standardized.$platform ?? this.options.platform, + platformVersion: standardized.$platform_version ?? this.options.platformVersion ?? null, + }, + measurement: clone(this.state.configuration.configuration.context ?? {}), + }, + }; + } + + /** Drains eligible records and emits one redacted diagnostic per record. */ + async flush(): Promise { + if (this.state.stopped.upload || !this.getConsentState().effective.upload) { + const policyBlocked = this.state.outbox.length; + for (const item of this.state.outbox) this.emitDelivery(item, "policyBlocked", "uploadPaused"); + return { accepted: 0, scheduled: 0, quarantined: 0, policyBlocked }; + } + const now = this.nowMs(); + const eligible = this.state.outbox.filter((item) => item.eligibleAt <= now); + const nativeResult = this.adapter.flushMeasurement + ? await this.adapter.flushMeasurement() + : undefined; + if (nativeResult) { + if (nativeResult.scheduled > 0) { + for (const item of eligible) this.emitDelivery(item, "retryScheduled", "nativeDeliveryPending"); + return nativeResult; + } + this.state.outbox = this.state.outbox.filter((item) => item.eligibleAt > now); + for (const item of eligible) this.emitDelivery(item, "accepted"); + return nativeResult; + } + this.state.outbox = this.state.outbox.filter((item) => item.eligibleAt > now); + for (const item of eligible) this.emitDelivery(item, "accepted"); + return { accepted: eligible.length, scheduled: this.state.outbox.length, quarantined: 0, policyBlocked: 0 }; + } + + /** Records a native notification receipt after vaulting the raw payload. */ + internalReceiveNotification(input: { + readonly rawPayload: Readonly>; + readonly title?: string; + readonly body?: string; + readonly pushNotificationSendId?: string; + }): IncomingNotification { + const protectedPayloadRef = this.vault(JSON.stringify(input.rawPayload), "push-token", "ephemeral"); + const received: IncomingNotification = { + id: this.makeId("notification"), + title: input.title, + body: input.body, + receivedAt: this.now(), + pushNotificationSendId: input.pushNotificationSendId, + }; + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_RECEIVED, received, "push", protectedPayloadRef); + this.notificationEvents.emit("received", received); + return received; + } + + /** Records a native notification open and routes its allowlisted link. */ + async internalOpenNotification( + notification: IncomingNotification, + link?: string, + ): Promise { + const opened: OpenedNotification = { ...notification, openedAt: this.now(), link }; + const linkResult = link ? await this.handleLink({ source: "push", url: link }) : undefined; + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_OPENED, { + notificationId: notification.id, + pushNotificationSendId: notification.pushNotificationSendId, + linkResolutionId: linkResult?.resolutionId, + openedAt: opened.openedAt, + }, "push"); + this.capture(STANDARD_EVENTS.OPENED_FROM_PUSH_NOTIFICATION, { + notification_id: notification.id, + ...(notification.pushNotificationSendId + ? { push_notification_send_id: notification.pushNotificationSendId } + : {}), + }); + this.notificationEvents.emit("opened", opened); + return opened; + } + + private async configure(patch: MeasurementConfigurationPatch): Promise { + if (patch.ios?.disableSKAD !== undefined && this.state.readiness !== "uninitialized") { + throw new MeasurementConfigurationError("disableSKAD can only be configured before initialization", "ios.disableSKAD"); + } + const { partnerData, purchases, ...remainingPatch } = patch; + if (partnerData) await this.persistPartnerData(partnerData); + if (purchases?.enrichment !== undefined) this.purchaseEnrichment = purchases.enrichment; + const { enrichment: _, ...serializablePurchases } = purchases ?? {}; + const publicPatch: MeasurementConfigurationPatch = { + ...remainingPatch, + purchases: purchases ? serializablePurchases : undefined, + }; + this.state.configuration = mergeConfiguration(this.state.configuration, publicPatch); + return clone(this.state.configuration); + } + + private async persistPartnerData( + partnerData: NonNullable, + ): Promise { + const partners = Object.keys(partnerData).sort(); + if (partners.length > 64 || partners.some((partner) => !/^[a-zA-Z\d._-]{1,64}$/.test(partner))) { + throw new MeasurementConfigurationError("Partner IDs must be 1-64 safe characters", "partnerData"); + } + for (const [partner, value] of Object.entries(partnerData)) { + assertJsonObject(value, `partnerData.${partner}`); + if (new TextEncoder().encode(JSON.stringify(value)).byteLength > 16_384) { + throw new MeasurementConfigurationError("Partner data exceeds the per-partner size bound", `partnerData.${partner}`); + } + } + const encoded = JSON.stringify(partnerData); + if (new TextEncoder().encode(encoded).byteLength > 65_536) { + throw new MeasurementConfigurationError("Partner data exceeds the total size bound", "partnerData"); + } + const protectedPayloadRef = await this.persistProtected(encoded, "partner-context", "installation"); + this.enqueue(MEASUREMENT_RECORD_TYPES.PARTNER_CONTEXT_CHANGED, { + partners, + configurationRevision: this.state.configuration.revision + 1, + }, "javascript", protectedPayloadRef); + } + + private async start(reason: SessionStartReason = "manual"): Promise { + if (this.state.session && this.state.readiness === "sessionStarted") return clone(this.state.session); + this.state.sessionSequence += 1; + const session: SessionSnapshot = { + id: this.makeId("session"), + sequence: this.state.sessionSequence, + startedAt: this.now(), + reason, + }; + this.state.session = session; + this.state.readiness = "sessionStarted"; + this.enqueue(MEASUREMENT_RECORD_TYPES.SESSION_STARTED, session, "native"); + this.measurementEvents.emit("session", clone(session)); + return clone(session); + } + + private async stop(options: MeasurementStopOptions = { collection: true, upload: true, partnerSharing: true }): Promise { + this.state.stopped = { ...this.state.stopped, ...options }; + } + + /** Marks the current session backgrounded using a monotonic timestamp. */ + background(): void { + this.lastBackgroundAt = this.adapter.monotonicNowMs?.() ?? this.nowMs(); + this.state.readiness = "backgrounded"; + } + + /** Resumes or rotates the session based on the configured inactivity threshold. */ + async foreground(): Promise { + const now = this.adapter.monotonicNowMs?.() ?? this.nowMs(); + const timeout = this.state.configuration.configuration.sessionTimeoutMs; + if (this.state.session && this.lastBackgroundAt !== undefined && now - this.lastBackgroundAt <= timeout) { + this.state.readiness = "sessionStarted"; + return clone(this.state.session); + } + if (this.state.session) { + this.enqueue(MEASUREMENT_RECORD_TYPES.SESSION_ENDED, { + sessionId: this.state.session.id, + durationMs: Math.max(0, now - (this.lastBackgroundAt ?? now)), + reason: "timeout", + }, "native"); + this.state.session = undefined; + } + if (this.state.configuration.configuration.startMode === "manual") { + this.state.readiness = "sdkReady"; + return undefined; + } + return this.start("foreground"); + } + + private async handleMeasurementInput(input: MeasurementInput): Promise { + if (input.type === "location") { + const policy = this.collectionPolicy(); + if (policy.location !== "manual-only") throw new MeasurementPolicyBlocked("location"); + if (!Number.isFinite(input.latitude) || input.latitude < -90 || input.latitude > 90) { + throw new MeasurementInputError("latitude must be between -90 and 90", "latitude"); + } + if (!Number.isFinite(input.longitude) || input.longitude < -180 || input.longitude > 180) { + throw new MeasurementInputError("longitude must be between -180 and 180", "longitude"); + } + const recordId = this.enqueue("location.observed.v1", input, "javascript", undefined, input.occurredAt); + return { accepted: true, recordId }; + } + if (input.type === "identifier") { + const decision = evaluateMeasurementCollection( + "advertisingIdentifier", + this.collectionPolicy(), + this.state.consent, + true, + ); + if (!decision.allowed) throw new MeasurementPolicyBlocked("advertisingIdentifier"); + if (!input.value.trim() || input.value.length > 1_024) { + throw new MeasurementInputError("Identifier must be 1-1024 characters", "value"); + } + const protectedPayloadRef = await this.persistProtected(input.value, "advertising-identifier", "installation"); + const recordId = this.enqueue(MEASUREMENT_RECORD_TYPES.IDENTIFIER_OBSERVED, { + kind: input.kind, + outcome: "collected", + policyBasis: decision.reason, + manuallySupplied: true, + }, "javascript", protectedPayloadRef); + return { accepted: true, recordId }; + } + const previous = this.state.consent.att; + if (previous === input.status) { + for (let index = this.state.outbox.length - 1; index >= 0; index -= 1) { + const envelope = this.state.outbox[index]?.envelope; + if (envelope?.type === MEASUREMENT_RECORD_TYPES.IOS_ATT_CHANGED) { + return { accepted: true, recordId: envelope.recordId }; + } + } + } + this.state.consent = { ...this.state.consent, att: input.status }; + const recordId = this.enqueue(MEASUREMENT_RECORD_TYPES.IOS_ATT_CHANGED, { + previous, + current: input.status, + source: input.source, + }, "javascript"); + return { accepted: true, recordId }; + } + + private async setConsent(snapshot: ConsentSnapshot): Promise { + if (!Number.isInteger(snapshot.revision) || snapshot.revision <= this.state.consent.revision) { + throw new MeasurementInputError("Consent revision must increase monotonically", "revision"); + } + if (!Number.isFinite(Date.parse(snapshot.decidedAt))) { + throw new MeasurementInputError("decidedAt must be an ISO timestamp", "decidedAt"); + } + const previous = clone(this.state.consent); + this.state.consent = clone(snapshot); + this.enqueue(MEASUREMENT_RECORD_TYPES.CONSENT_CHANGED, { previous, current: clone(snapshot) }, "javascript"); + const state = this.getConsentState(); + if (this.state.configuration.configuration.startMode === "consent-gated" && state.effective.analytics && !this.state.session) { + await this.start("manual"); + } + return state; + } + + private getConsentState(): ConsentState { + return consentState(this.state.consent, this.collectionPolicy()); + } + + private collectionPolicy(): CollectionPolicy { + return { ...DEFAULT_COLLECTION_POLICY, ...this.state.configuration.configuration.collection }; + } + + private nativeLinkSource(source: string): UrlSource { + switch (source) { + case "appLink": + case "universalLink": + case "customScheme": + case "push": + case "deferred": + case "esp": + return source; + default: + return "manual"; + } + } + + private async handleLink( + input: { readonly url: string; readonly source: UrlSource; readonly receivedAt?: string }, + existingProtectedRef?: string, + ): Promise { + const resolutionId = this.makeId("resolution"); + const receivedAt = input.receivedAt ?? this.now(); + if (input.url.length > MAX_LINK_LENGTH) { + return this.emitLinkError(resolutionId, new MeasurementInputError("Link exceeds the maximum length", "url")); + } + const protectedRef = existingProtectedRef ?? await this.persistProtected(input.url, "link-capture", "installation"); + this.enqueue(MEASUREMENT_RECORD_TYPES.LINK_RECEIVED, { source: input.source, receivedAt }, "native", protectedRef, receivedAt); + let url: URL; + try { + const authorityStart = input.url.indexOf("://"); + const pathStart = authorityStart < 0 ? input.url.indexOf(":") + 1 : input.url.indexOf("/", authorityStart + 3); + const rawPath = pathStart < 0 ? "" : input.url.slice(pathStart).split(/[?#]/, 1)[0] ?? ""; + if (decodeURIComponent(rawPath).split("/").some((part) => part === "..")) { + throw new Error("path traversal"); + } + url = new URL(input.url); + decodeURIComponent(url.pathname); + } catch { + return this.emitLinkError(resolutionId, new MeasurementInputError("Link is malformed", "url")); + } + const ruleApplications: Array<{ readonly id: string; readonly appended: ReadonlyArray; readonly missingPid: boolean }> = []; + for (const rule of this.linkConfiguration.parameterRules ?? []) { + if (!/^[a-zA-Z\d._-]{1,64}$/.test(rule.id)) { + return this.emitLinkError(resolutionId, new MeasurementConfigurationError("Invalid link parameter rule ID", "links.parameterRules.id")); + } + const domains = (rule.match.domains ?? []).map((domain) => domain.toLowerCase().replace(/\.$/, "")); + const matchesDomain = domains.length === 0 || domains.includes(url.hostname.toLowerCase().replace(/\.$/, "")); + const matchesSubstring = !rule.match.contains || url.toString().includes(rule.match.contains); + if (!matchesDomain || !matchesSubstring) continue; + const missingPid = !url.searchParams.has("pid"); + if (missingPid && rule.requiredPid === "reject") { + this.enqueue(MEASUREMENT_RECORD_TYPES.LINK_ROUTED, { resolutionId, ruleId: rule.id, outcome: "requiredPidMissing" }, "native"); + return this.emitLinkNotFound(resolutionId, "requiredPidMissing"); + } + const appended: string[] = []; + for (const [name, value] of Object.entries(rule.parameters ?? {})) { + if (!/^[a-zA-Z\d._-]{1,64}$/.test(name) || value.length > MAX_LINK_VALUE_LENGTH || /[\r\n]/.test(value)) { + return this.emitLinkError(resolutionId, new MeasurementConfigurationError("Invalid link rule parameter", `links.parameterRules.${rule.id}`)); + } + if (rule.overwrite || !url.searchParams.has(name)) { + url.searchParams.set(name, value); + appended.push(name); + } + } + if (rule.reengagement === true) { + url.searchParams.set("is_retargeting", "true"); + appended.push("is_retargeting"); + } + ruleApplications.push({ id: rule.id, appended: [...new Set(appended)].sort(), missingPid }); + } + const wrappedDomains = new Set( + (this.linkConfiguration.resolveWrappedDomains ?? []).map((item) => + item.toLowerCase().replace(/\.$/, ""), + ), + ); + if (wrappedDomains.has(url.hostname.toLowerCase().replace(/\.$/, ""))) { + const resolved = await this.resolveWrappedLink(url, resolutionId, protectedRef); + if (resolved instanceof MeasurementError) return this.emitLinkError(resolutionId, resolved); + url = resolved; + } + const scheme = url.protocol.slice(0, -1).toLowerCase(); + const allowedSchemes = new Set((this.linkConfiguration.allowedSchemes ?? ["https"]).map((item) => item.toLowerCase())); + if (!allowedSchemes.has(scheme) || scheme === "javascript" || scheme === "file") { + return this.emitLinkError(resolutionId, new MeasurementInputError("Link scheme is not allowed", "url")); + } + const normalizedHost = url.hostname.toLowerCase().replace(/\.$/, ""); + const allowedDomains = (this.linkConfiguration.allowedDomains ?? []).map((item) => item.toLowerCase().replace(/\.$/, "")); + if (scheme === "https" && allowedDomains.length > 0 && !allowedDomains.includes(normalizedHost)) { + return this.emitLinkNotFound(resolutionId, "domainNotAllowed"); + } + if (decodeURIComponent(url.pathname).split("/").some((part) => part === "..")) { + return this.emitLinkError(resolutionId, new MeasurementInputError("Link path traversal is not allowed", "url")); + } + const seen = new Set(); + for (const [key, value] of url.searchParams) { + if (seen.has(key)) return this.emitLinkError(resolutionId, new MeasurementInputError("Duplicate query fields are not allowed", key)); + if (value.length > MAX_LINK_VALUE_LENGTH) return this.emitLinkError(resolutionId, new MeasurementInputError("Link value exceeds the maximum length", key)); + seen.add(key); + } + const dedupeKey = `${scheme}://${normalizedHost}${url.pathname}?${[...url.searchParams.entries()].sort().map(([key, value]) => `${key}=${value}`).join("&")}`; + const previous = this.state.dedupe.get(`link:${dedupeKey}`); + const dedupeWindow = this.linkConfiguration.dedupeWindowMs ?? 5_000; + if (previous?.result && this.nowMs() - previous.at <= dedupeWindow) return clone(previous.result); + const routeValue = url.searchParams.get("deep_link_value") ?? url.pathname.split("/").filter(Boolean).at(-1); + if (!routeValue) return this.emitLinkNotFound(resolutionId, "routeMissing"); + const subvalues: Partial> = {}; + for (let index = 1; index <= 10; index += 1) { + const value = url.searchParams.get(`deep_link_sub${index}`); + if (value) subvalues[index as keyof typeof subvalues] = value; + } + const result: DeepLinkResult = { + status: "found", + resolutionId, + direct: input.source !== "deferred", + deferred: input.source === "deferred", + linkId: url.searchParams.get("link_id") ?? url.searchParams.get("click_id") ?? undefined, + route: { value: routeValue, subvalues }, + campaign: { + campaign: url.searchParams.get("campaign") ?? url.searchParams.get("c") ?? undefined, + channel: url.searchParams.get("channel") ?? undefined, + mediaSource: url.searchParams.get("media_source") ?? url.searchParams.get("pid") ?? undefined, + }, + receivedAt, + resolvedAt: this.now(), + }; + this.state.dedupe.set(`link:${dedupeKey}`, { at: this.nowMs(), result }); + this.enqueue(MEASUREMENT_RECORD_TYPES.LINK_RESOLVED, { ...result, ruleApplications }, "native"); + this.linkEvents.emit("deepLink", clone(result)); + return clone(result); + } + + private async resolveWrappedLink( + initialUrl: URL, + resolutionId: string, + protectedPayloadRef: string, + ): Promise { + const fetcher = this.adapter.fetch ?? globalThis.fetch; + if (!fetcher) return new MeasurementCapabilityUnavailable("links.wrappedResolution", "notConfigured"); + const maximumRedirects = this.linkConfiguration.maxRedirects ?? 5; + const timeoutMs = this.linkConfiguration.resolutionTimeoutMs ?? 5_000; + const seen = new Set(); + const hops: Array<{ + readonly durationMs: number; + readonly host: string; + readonly scheme: string; + readonly status: number; + }> = []; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + let current = initialUrl; + let outcome = "resolved"; + try { + for (let index = 0; index <= maximumRedirects; index += 1) { + if ( + current.protocol !== "https:" && + !(this.linkConfiguration.allowInsecureRedirects === true && current.protocol === "http:") + ) { + outcome = "insecureRedirect"; + return new MeasurementError({ + code: "transport", + message: "Wrapped link resolution refused an insecure redirect", + detail: { reason: outcome }, + }); + } + const normalized = this.normalizeRedirectIdentity(current); + if (seen.has(normalized)) { + outcome = "redirectLoop"; + return new MeasurementError({ + code: "transport", + message: "Wrapped link resolution detected a redirect loop", + detail: { reason: outcome }, + }); + } + seen.add(normalized); + const startedAt = this.nowMs(); + const isConfiguredWrappedDomain = (this.linkConfiguration.resolveWrappedDomains ?? []) + .map((item) => item.toLowerCase().replace(/\.$/, "")) + .includes(current.hostname.toLowerCase().replace(/\.$/, "")); + const supplied = isConfiguredWrappedDomain + ? await this.linkConfiguration.wrappedDomainHeaderProvider?.(current.origin) + : undefined; + const headers = new Headers(); + for (const [name, value] of Object.entries(supplied ?? {})) { + if (/\r|\n/.test(name) || /\r|\n/.test(value)) { + outcome = "invalidHeaders"; + return new MeasurementError({ + code: "invalidConfiguration", + message: "Wrapped-domain headers contain invalid characters", + detail: { reason: outcome }, + }); + } + headers.set(name, value); + } + let response: Response; + for (;;) { + try { + response = await fetcher(current.toString(), { + headers, + method: "GET", + redirect: "manual", + signal: controller.signal, + }); + break; + } catch (error) { + if (controller.signal.aborted) throw error; + await new Promise((resolve, reject) => { + const retry = setTimeout(resolve, this.linkConfiguration.wrappedRetryDelayMs ?? 100); + controller.signal.addEventListener("abort", () => { + clearTimeout(retry); + reject(new DOMException("Aborted", "AbortError")); + }, { once: true }); + }); + } + } + hops.push({ + durationMs: Math.max(0, this.nowMs() - startedAt), + host: current.hostname.toLowerCase().replace(/\.$/, ""), + scheme: current.protocol.slice(0, -1), + status: response.status, + }); + if (![301, 302, 303, 307, 308].includes(response.status)) return current; + if (index >= maximumRedirects) { + outcome = "redirectLimit"; + return new MeasurementError({ + code: "transport", + message: "Wrapped link resolution exceeded its redirect limit", + detail: { reason: outcome }, + }); + } + const location = response.headers.get("location"); + if (!location) { + outcome = "missingLocation"; + return new MeasurementError({ + code: "transport", + message: "Wrapped link redirect omitted its destination", + detail: { reason: outcome }, + }); + } + current = new URL(location, current); + if (current.protocol !== "http:" && current.protocol !== "https:") return current; + } + outcome = "redirectLimit"; + return new MeasurementError({ + code: "transport", + message: "Wrapped link resolution exceeded its redirect limit", + detail: { reason: outcome }, + }); + } catch (error) { + outcome = controller.signal.aborted ? "timeout" : "transport"; + return new MeasurementError({ + code: controller.signal.aborted ? "timeout" : "transport", + message: controller.signal.aborted + ? "Wrapped link resolution timed out" + : "Wrapped link resolution failed", + detail: { reason: outcome }, + }); + } finally { + clearTimeout(timeout); + this.enqueue("link.redirect_evidence.v1", { + hops, + outcome, + resolutionId, + }, "native", protectedPayloadRef); + } + } + + private normalizeRedirectIdentity(url: URL): string { + const normalized = new URL(url.toString()); + normalized.hostname = normalized.hostname.toLowerCase().replace(/\.$/, ""); + normalized.hash = ""; + normalized.search = [...normalized.searchParams.entries()] + .sort(([leftKey, leftValue], [rightKey, rightValue]) => + leftKey === rightKey ? leftValue.localeCompare(rightValue) : leftKey.localeCompare(rightKey), + ) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join("&"); + return normalized.toString(); + } + + private emitLinkNotFound(resolutionId: string, reason: string): DeepLinkResult { + const result: DeepLinkResult = { status: "notFound", reason, resolutionId }; + this.enqueue(MEASUREMENT_RECORD_TYPES.LINK_RESOLVED, result, "native"); + this.linkEvents.emit("deepLink", result); + return result; + } + + private emitLinkError(resolutionId: string, error: MeasurementError): DeepLinkResult { + const result: DeepLinkResult = { status: "error", resolutionId, error }; + this.enqueue(MEASUREMENT_RECORD_TYPES.LINK_RESOLVED, { + status: "error", + resolutionId, + error: { code: error.code, message: error.message, source: error.source }, + }, "native"); + this.linkEvents.emit("deepLink", result); + this.measurementEvents.emit("attributionError", error); + return result; + } + + private async trackAdRevenue(input: AdRevenueInput): Promise { + if (!input.impressionId.trim() || !input.monetizationNetwork.trim()) { + throw new MeasurementInputError("Ad revenue requires impressionId and monetizationNetwork"); + } + const allowedMediation = new Set([ + "ironsource", "applovin_max", "google_admob", "fyber", "appodeal", "admost", "topon", + "tradplus", "yandex", "chartboost", "unity", "topon_pte", "custom_mediation", + "direct_monetization_network", + ]); + if (!allowedMediation.has(input.mediationNetwork)) throw new MeasurementInputError("Unknown mediation network", "mediationNetwork"); + const currency = assertIsoCurrency(input.currency); + if (!/^-?(?:0|[1-9]\d{0,15})(?:\.\d{1,8})?$/.test(input.revenue)) { + throw new MeasurementInputError("Revenue must be a bounded decimal string with at most 8 fractional digits", "revenue"); + } + if (input.additionalParameters) assertSafePublicValue(input.additionalParameters, "additionalParameters"); + const dedupeKey = `ad-revenue:${input.impressionId}`; + if (this.state.dedupe.has(dedupeKey)) return; + this.state.dedupe.set(dedupeKey, { at: this.nowMs() }); + this.enqueue(MEASUREMENT_RECORD_TYPES.AD_REVENUE, { ...clone(input), currency }, "javascript"); + } + + private async validatePurchase(input: PurchaseValidationInput): Promise { + const allowedKeys = new Set(["transactionId", "platform", "protectedEvidenceId", "environment", "idempotencyKey"]); + for (const key of Object.keys(input)) if (!allowedKeys.has(key)) throw new MeasurementInputError(`Unknown purchase validation field '${key}'`, key); + if (!input.transactionId || !input.protectedEvidenceId) throw new MeasurementInputError("Purchase validation requires an opaque transaction and protected evidence reference"); + const environment = input.environment ?? this.state.configuration.configuration.purchases?.environment ?? "production"; + if (environment === "sandbox" && this.adapter.isReleaseBuild === true) { + throw new MeasurementConfigurationError("Sandbox purchase validation is forbidden in release builds", "purchases.environment"); + } + const requestId = this.makeId("purchase-validation"); + this.enqueue(MEASUREMENT_RECORD_TYPES.PURCHASE_VALIDATION_REQUESTED, { + requestId, + transactionId: input.transactionId, + environment, + idempotencyKey: input.idempotencyKey ?? requestId, + protectedEvidenceId: input.protectedEvidenceId, + }, "javascript"); + let result: PurchaseValidationResult; + let protectedResponseRef: string | undefined; + if (!this.adapter.validatePurchase) { + result = { + requestId, + transactionId: input.transactionId, + outcome: "indeterminate", + failure: { kind: "configuration", message: "No purchase validation transport is configured" }, + }; + } else { + try { + const response = await this.adapter.validatePurchase({ ...input, environment, requestId }); + if (response.outcome !== "valid" && response.outcome !== "invalid" && response.outcome !== "indeterminate") { + throw new TypeError("Invalid purchase validation response"); + } + if (response.outcome === "invalid" && response.failure) { + throw new TypeError("Store-invalid validation must not contain a transport failure"); + } + assertNormalizedStoreState(response.storeState); + protectedResponseRef = response.protectedResponse + ? await this.persistProtected(response.protectedResponse, "purchase-receipt", "transaction") + : undefined; + result = { + requestId, + transactionId: input.transactionId, + outcome: response.outcome, + storeState: response.storeState, + failure: response.failure, + }; + } catch (error) { + const failure = typeof error === "object" && error !== null && "kind" in error && + new Set(["network", "store", "configuration", "server"]).has(String(error.kind)) + ? String(error.kind) as NonNullable["kind"] + : "server"; + result = { + requestId, + transactionId: input.transactionId, + outcome: "indeterminate", + failure: { kind: failure, message: error instanceof Error ? error.message : "Purchase validation failed" }, + }; + } + } + this.enqueue(MEASUREMENT_RECORD_TYPES.PURCHASE_VALIDATION_RESULT, result, "server-correlation", protectedResponseRef); + this.measurementEvents.emit("purchaseValidation", clone(result)); + return result; + } + + private async createInviteLink(input: InviteLinkInput): Promise { + if (!input.deepLinkValue.trim()) throw new MeasurementInputError("deepLinkValue is required", "deepLinkValue"); + const limitedValues = [ + input.channel, input.campaign, input.referrerCustomerId, input.referrerUid, + input.referrerName, input.referrerImageUrl, input.baseDeepLink, input.brandedDomain, + input.appleAppId, ...Object.values(input.deepLinkSubvalues ?? {}), + ]; + if (limitedValues.some((value) => value !== undefined && value.length > 1_024)) { + throw new MeasurementInputError("Invite link fields must not exceed 1024 characters"); + } + const customParameters: Record = {}; + const allowed = new Set(this.linkConfiguration.allowedCustomParameters ?? []); + for (const [key, value] of Object.entries(input.customParameters ?? {})) { + if (!allowed.has(key)) throw new MeasurementInputError(`Invite parameter '${key}' is not allowlisted`, key); + if (typeof value !== "string" || value.length > 1_024) { + throw new MeasurementInputError("Invite custom parameters must be strings up to 1024 characters", key); + } + customParameters[key] = value; + } + const request: LinksCreateLinkRequest = { + brandedDomain: input.brandedDomain, + campaign: { campaign: input.campaign, channel: input.channel }, + customParameters, + destination: { + appleAppId: input.appleAppId, + baseDeepLink: input.baseDeepLink, + deepLinkValue: input.deepLinkValue, + subvalues: Object.fromEntries(Object.entries(input.deepLinkSubvalues ?? {})), + }, + referrerCustomerId: input.referrerCustomerId, + referrerImageUrl: input.referrerImageUrl, + referrerName: input.referrerName, + referrerUid: input.referrerUid, + templateId: this.linkConfiguration.templateId, + token: this.options.publishableKey, + }; + const fetcher = this.adapter.fetch ?? globalThis.fetch; + let response: Response; + try { + response = await fetcher( + `${(this.options.linksUrl ?? this.options.baseUrl).replace(/\/$/, "")}/l/v1/links`, + { body: JSON.stringify(request), headers: { "content-type": "application/json" }, method: "POST" }, + ); + } catch { + throw new MeasurementError({ code: "transport", message: "Invite link creation failed" }); + } + if (!response.ok) { + throw new MeasurementError({ code: "transport", message: `Invite link creation returned HTTP ${response.status}` }); + } + const generated = await response.json() as Partial; + if (typeof generated.linkId !== "string" || typeof generated.url !== "string") { + throw new MeasurementError({ code: "transport", message: "Invite link creation returned an invalid response" }); + } + return { linkId: generated.linkId, url: generated.url, expiresAt: generated.expiresAt }; + } + + private async trackInviteShare(input: { readonly linkId: string; readonly channel: string }): Promise { + if (!input.linkId.trim() || !input.channel.trim()) { + throw new MeasurementInputError("Invite sharing requires linkId and channel"); + } + this.capture(STANDARD_EVENTS.INVITE_SHARED, { channel: input.channel, link_id: input.linkId }); + } + + private async trackCrossPromotion(input: CrossPromotionInput): Promise { + if (!input.promotedAppId.trim()) throw new MeasurementInputError("promotedAppId is required", "promotedAppId"); + if (input.parameters) assertSafePublicValue(input.parameters, "parameters"); + const recordId = this.capture(`cross promotion ${input.action}`, { + campaign: input.campaign, + parameters: input.parameters, + promoted_app_id: input.promotedAppId, + }); + if (!recordId) throw new MeasurementPolicyBlocked("analytics"); + if (input.action === "impression") return { recordId }; + const link = await this.createInviteLink({ campaign: input.campaign, deepLinkValue: input.promotedAppId }); + if (!this.adapter.openUrl) throw new MeasurementCapabilityUnavailable("openUrl", "notConfigured"); + return { link, opened: await this.adapter.openUrl(link.url), recordId }; + } + + private async getPermissionStatus(): Promise { + this.state.permission = this.adapter.getPermissionStatus + ? await this.adapter.getPermissionStatus() + : this.state.permission; + return this.state.permission; + } + + private async requestPermission(options?: PushPermissionOptions): Promise { + if (!this.adapter.requestPermission) throw new MeasurementCapabilityUnavailable("notifications.permission", "notConfigured"); + this.state.permission = await this.adapter.requestPermission(options); + return this.state.permission; + } + + private async registerNotifications(): Promise { + if (this.state.registration) return clone(this.state.registration); + if (this.notificationRegistrationInFlight) return this.notificationRegistrationInFlight; + if (!this.adapter.getPushToken) throw new MeasurementCapabilityUnavailable("notifications.registration", "notConfigured"); + const operation = (async () => { + const token = await this.adapter.getPushToken!(); + const protectedRef = await this.persistProtected(token.token, "push-token", "installation"); + const response = await this.postPushDevice("register", { + bundleId: this.options.bundleId, + environment: token.provider === "apns" + ? token.environment === "development" ? "sandbox" : "production" + : undefined, + platform: this.options.platform, + platformToken: token.token, + provider: token.provider, + }); + const pushDeviceTokenId = (response as { pushDeviceTokenId?: unknown }).pushDeviceTokenId; + if (typeof pushDeviceTokenId !== "string" || !pushDeviceTokenId) { + throw new MeasurementError({ code: "transport", message: "Push registration returned an invalid response" }); + } + this.state.protectedEvidence.delete(protectedRef); + await this.adapter.deleteProtectedEvidence?.(protectedRef); + const registration: PushRegistration = { + pushDeviceTokenId, + provider: token.provider, + environment: token.environment, + registeredAt: this.now(), + }; + this.state.registration = registration; + await this.persistPushRegistration(registration); + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_TOKEN, registration, "push"); + this.notificationEvents.emit("tokenChanged", clone(registration)); + return clone(registration); + })(); + this.notificationRegistrationInFlight = operation; + try { + return await operation; + } catch (error) { + this.emitRegistrationError(error); + throw error; + } finally { + this.notificationRegistrationInFlight = undefined; + } + } + + private async unregisterNotifications(): Promise { + if (!this.state.registration) return; + await this.postPushDevice("unregister", { + pushDeviceTokenId: this.state.registration.pushDeviceTokenId, + }); + this.state.registration = undefined; + await this.adapter.clearPushRegistrationState?.(); + } + + private async relinkNotificationRegistration(): Promise { + const previous = this.state.registration; + if (!previous || !this.adapter.getPushToken) return; + const token = await this.adapter.getPushToken(); + const response = await this.postPushDevice("register", { + bundleId: this.options.bundleId, + environment: token.provider === "apns" + ? token.environment === "development" ? "sandbox" : "production" + : undefined, + platform: this.options.platform, + platformToken: token.token, + previousPushDeviceTokenId: previous.pushDeviceTokenId, + provider: token.provider, + }); + const pushDeviceTokenId = (response as { pushDeviceTokenId?: unknown }).pushDeviceTokenId; + if (typeof pushDeviceTokenId !== "string" || !pushDeviceTokenId) { + throw new MeasurementError({ code: "transport", message: "Push re-link returned an invalid response" }); + } + this.state.registration = { + ...previous, + pushDeviceTokenId, + registeredAt: this.now(), + }; + await this.persistPushRegistration(this.state.registration); + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_TOKEN, { + ...this.state.registration, + previousPushDeviceTokenId: previous.pushDeviceTokenId, + reason: "identityChanged", + }, "push"); + } + + private async refreshNotificationRegistration(): Promise { + const registration = this.state.registration; + if (!registration || !this.adapter.getPushToken) return; + const token = await this.adapter.getPushToken(); + const protectedRef = await this.persistProtected(token.token, "push-token", "installation"); + await this.postPushDevice("refresh", { + platformToken: token.token, + pushDeviceTokenId: registration.pushDeviceTokenId, + }); + this.state.protectedEvidence.delete(protectedRef); + await this.adapter.deleteProtectedEvidence?.(protectedRef); + const refreshed = { ...registration, environment: token.environment, provider: token.provider }; + this.state.registration = refreshed; + await this.persistPushRegistration(refreshed); + this.enqueue(MEASUREMENT_RECORD_TYPES.PUSH_TOKEN, { + ...refreshed, + reason: "tokenRotated", + }, "push"); + this.notificationEvents.emit("tokenChanged", clone(refreshed)); + } + + private async postPushDevice( + operation: "register" | "refresh" | "unregister", + payload: Readonly>, + ): Promise { + const fetcher = this.adapter.fetch ?? globalThis.fetch; + if (!fetcher) throw new MeasurementCapabilityUnavailable("notifications.registration", "notConfigured"); + const response = await fetcher( + `${this.options.baseUrl.replace(/\/$/, "")}/api/v1/sdk/push-devices/${operation}`, + { + body: JSON.stringify(payload), + headers: { + "content-type": "application/json", + "x-client-bundle-id": this.options.bundleId ?? "unknown", + "x-distinct-id": this.state.identity.distinctId, + "x-is-backgrounded": "false", + "x-is-debug-build": "false", + "x-nonce": getNonce(), + "x-observer-mode": "false", + "x-platform": this.options.platform, + "x-platform-flavor": "native", + "x-publishable-key": this.options.publishableKey, + "x-sdk": "react-native", + "x-sdk-version": SDK_VERSION, + }, + method: "POST", + }, + ); + if (!response.ok) { + throw new MeasurementError({ + code: "transport", + message: `Push device ${operation} failed`, + detail: { status: response.status }, + }); + } + if (response.status === 204 || response.headers.get("content-length") === "0") return undefined; + return response.json(); + } + + private async setBadgeCount(count: number): Promise { + if (!Number.isInteger(count) || count < 0) throw new MeasurementInputError("Badge count must be a non-negative integer", "count"); + if (!this.adapter.setBadgeCount) throw new MeasurementCapabilityUnavailable("notifications.badge", "notConfigured"); + await this.adapter.setBadgeCount(count); + this.state.badgeCount = count; + } + + private emitRegistrationError(error: unknown): void { + const mapped = error instanceof MeasurementError + ? error + : new MeasurementError({ code: "unknownNative", message: "Notification registration failed", cause: error }); + this.notificationEvents.emit("registrationError", mapped); + } + + private async deleteData(): Promise<{ readonly requestId: string; readonly status: "accepted" }> { + const requestId = this.makeId("deletion"); + this.state.deletion = { requested: true, completed: false }; + this.enqueue("measurement.deletion_requested.v1", { requestId }, "javascript"); + await this.adapter.waitForPendingWrites?.(); + await this.adapter.deleteProtectedData?.(requestId); + this.state.protectedEvidence.clear(); + this.state.protectedIdentityReferences.clear(); + this.state.deletion = { requested: true, completed: true }; + return { requestId, status: "accepted" }; + } + + private async setTestDevice(enabled: boolean): Promise { + await this.adapter.persistTestDeviceState?.(enabled); + this.state.testDevice = enabled; + } + + private async getState(): Promise { + const counts: Record = { critical: 0, high: 0, normal: 0, low: 0 }; + for (const item of this.state.outbox) counts[item.priority] += 1; + const oldest = this.state.outbox.reduce((value, item) => { + const queuedAt = Date.parse(item.envelope.queuedAt); + return value === undefined ? queuedAt : Math.min(value, queuedAt); + }, undefined); + const configuration = this.state.configuration.configuration; + return { + versions: { sdk: SDK_VERSION, native: "1", envelopeSchema: 1, configSchema: 1 }, + installation: { id: this.state.installationId, sequence: this.state.sequence, firstOpenedAt: this.state.firstOpenedAt }, + session: { current: this.state.session && clone(this.state.session), readiness: this.state.readiness, stopped: clone(this.state.stopped) }, + outbox: { + counts, + total: this.state.outbox.length, + oldestAgeMs: oldest === undefined ? undefined : Math.max(0, this.nowMs() - oldest), + lastDelivery: this.state.lastDelivery && clone(this.state.lastDelivery), + }, + consent: this.getConsentState(), + configuration: { + revision: this.state.configuration.revision, + startMode: configuration.startMode, + sessionTimeoutMs: configuration.sessionTimeoutMs, + defaultCurrency: configuration.defaultCurrency, + localeOverride: configuration.localeOverride, + contextKeys: Object.keys(configuration.context ?? {}).sort(), + endpoints: { + api: this.options.baseUrl, + ingest: this.options.ingestUrl ?? this.options.baseUrl, + links: this.options.linksUrl ?? this.options.baseUrl, + trustedConfigKeyIds: [...(this.options.trustedConfigKeyIds ?? [])], + }, + signed: this.state.signedConfiguration && clone(this.state.signedConfiguration), + lastSignedConfigurationRejection: this.state.lastSignedConfigurationRejection, + }, + collectors: this.collectorStates(), + manifest: { present: false }, + deletion: clone(this.state.deletion), + testDevice: this.state.testDevice, + }; + } + + private collectorStates(): MeasurementDebugState["collectors"] { + return { + links: this.linkConfiguration.allowedDomains?.length || this.linkConfiguration.allowedSchemes?.length ? "available" : "notConfigured", + referrer: "notConfigured", + push: this.adapter.getPushToken ? "available" : "notConfigured", + purchases: this.state.configuration.configuration.purchases?.enabled ? "available" : "notConfigured", + advertisingIdentifiers: this.collectionPolicy().advertisingIdentifiers === "denied" ? "disabled" : "notConfigured", + vendorIdentifiers: this.collectionPolicy().vendorIdentifiers === "denied" ? "disabled" : "notConfigured", + networkMetadata: this.collectionPolicy().networkMetadata === "denied" ? "disabled" : "notConfigured", + appleAds: this.state.configuration.configuration.ios?.collectAppleAds ? "available" : "notConfigured", + skan: this.state.configuration.configuration.ios?.disableSKAD ? "disabled" : "noRules", + adAttributionKit: this.state.configuration.configuration.ios?.disableSKAD ? "disabled" : "noRules", + }; + } + + private enqueue( + type: string, + publicPayload: unknown, + source: MeasurementEnvelopeV1["source"], + protectedPayloadRef?: string, + occurredAt = this.now(), + recordId?: string, + ): string { + this.state.sequence += 1; + const queuedAt = this.now(); + const envelope: MeasurementEnvelopeV1 = { + schemaVersion: 1, + recordId: recordId ?? this.makeId("record"), + type, + occurredAt, + queuedAt, + installationId: this.state.installationId, + installationSequence: this.state.sequence, + session: this.state.session && clone(this.state.session), + identity: clone(this.state.identity), + consent: clone(this.state.consent), + app: this.appSnapshot(), + device: this.deviceSnapshot(), + source, + publicPayload: clone(publicPayload), + protectedPayloadRef, + }; + const bytes = new TextEncoder().encode(JSON.stringify(envelope)).byteLength; + this.state.outbox.push({ envelope, priority: priorityForRecord(type), bytes, attempts: 0, eligibleAt: this.nowMs() }); + void this.adapter.enqueueMeasurement?.({ + commandId: envelope.recordId, + recordType: envelope.type, + occurredAt: envelope.occurredAt, + source: envelope.source, + priority: priorityForRecord(type), + envelope: clone(envelope), + protectedPayload: protectedPayloadRef, + identity: clone(envelope.identity), + consent: clone(envelope.consent), + session: envelope.session && clone(envelope.session), + }).catch((error) => { + const mapped = error instanceof MeasurementError + ? error + : new MeasurementError({ code: "unknownNative", message: "Native measurement enqueue failed", cause: error }); + this.measurementEvents.emit("error", mapped); + }); + this.enforceOutboxBounds(); + return envelope.recordId; + } + + private enforceOutboxBounds(): void { + let bytes = this.state.outbox.reduce((total, item) => total + item.bytes, 0); + while (this.state.outbox.length > MAX_OUTBOX_RECORDS || bytes > MAX_OUTBOX_BYTES) { + const index = this.state.outbox.findIndex( + (item) => item.priority === "low" && !NON_EVICTABLE_RECORD_TYPES.has(item.envelope.type), + ); + const fallback = this.state.outbox.findIndex( + (item) => item.priority === "normal" && !NON_EVICTABLE_RECORD_TYPES.has(item.envelope.type), + ); + const evictionIndex = index >= 0 ? index : fallback; + if (evictionIndex < 0) throw new MeasurementError({ code: "transport", message: "Measurement outbox is full of protected-priority evidence" }); + const [evicted] = this.state.outbox.splice(evictionIndex, 1); + bytes -= evicted?.bytes ?? 0; + } + } + + private emitDelivery(item: StoredEnvelope, outcome: DeliveryDiagnostic["outcome"], reason?: string): void { + const diagnostic: DeliveryDiagnostic = { + recordId: item.envelope.recordId, + requestId: this.makeId("request"), + outcome, + reason, + attemptCount: item.attempts + 1, + occurredAt: this.now(), + }; + this.state.lastDelivery = diagnostic; + this.measurementEvents.emit("delivery", clone(diagnostic)); + } + + private vault( + value: string, + purpose: "advertising-identifier" | "diagnostic-authorization" | "email" | "install-referrer" | "link-capture" | "partner-context" | "phone" | "purchase-receipt" | "push-token" = "diagnostic-authorization", + retentionClass: "ephemeral" | "installation" | "legal" | "transaction" = "installation", + ): string { + const reference = this.makeId("protected"); + this.state.protectedEvidence.set(reference, value); + void this.adapter.putProtectedEvidence?.({ + blobId: reference, + purpose, + consentRevision: this.state.consent.revision, + retentionClass, + value: new TextEncoder().encode(value), + }).catch((error) => { + const mapped = error instanceof MeasurementError + ? error + : new MeasurementError({ code: "unknownNative", message: "Protected evidence persistence failed", cause: error }); + this.measurementEvents.emit("error", mapped); + }); + return reference; + } + + private async persistProtected( + value: string, + purpose: "advertising-identifier" | "diagnostic-authorization" | "email" | "install-referrer" | "link-capture" | "partner-context" | "phone" | "purchase-receipt" | "push-token", + retentionClass: "ephemeral" | "installation" | "legal" | "transaction", + ): Promise { + const reference = this.makeId("protected"); + this.state.protectedEvidence.set(reference, value); + try { + await this.adapter.putProtectedEvidence?.({ + blobId: reference, + purpose, + consentRevision: this.state.consent.revision, + retentionClass, + value: new TextEncoder().encode(value), + }); + return reference; + } catch (error) { + this.state.protectedEvidence.delete(reference); + const mapped = error instanceof MeasurementError + ? error + : new MeasurementError({ code: "unknownNative", message: "Protected evidence persistence failed", cause: error }); + this.measurementEvents.emit("error", mapped); + throw mapped; + } + } + + private appSnapshot(): AppSnapshot { + return { bundleId: this.options.bundleId, build: this.options.appBuild, version: this.options.appVersion }; + } + + private deviceSnapshot(): DeviceSnapshot { + return { + platform: this.options.platform, + platformVersion: this.options.platformVersion, + locale: this.state.configuration.configuration.localeOverride ?? this.options.locale, + }; + } + + private now(): string { + return (this.adapter.now?.() ?? new Date()).toISOString(); + } + + private nowMs(): number { + return this.adapter.now?.().getTime() ?? Date.now(); + } + + private makeId(prefix: string): string { + return this.adapter.makeId?.(prefix) ?? `${prefix}_${getNonce()}`; + } +} diff --git a/libraries/react-native/src/core/measurement/signed-config.ts b/libraries/react-native/src/core/measurement/signed-config.ts new file mode 100644 index 000000000..e91ddf007 --- /dev/null +++ b/libraries/react-native/src/core/measurement/signed-config.ts @@ -0,0 +1,156 @@ +export interface SignedMeasurementConfiguration { + readonly expiresAt: string; + readonly keyId: string; + readonly payload: T; + readonly projectId: string; + readonly signature: string; + readonly version: number; +} + +export type SignedConfigurationRejectionCode = + | "expired" + | "invalid-signature" + | "malformed" + | "project-mismatch" + | "unknown-key" + | "version-replay"; + +export class SignedConfigurationRejected extends Error { + readonly code: SignedConfigurationRejectionCode; + + constructor(code: SignedConfigurationRejectionCode) { + super(`Signed measurement configuration rejected: ${code}`); + this.name = "SignedConfigurationRejected"; + this.code = code; + } +} + +export type SignedConfigurationKeyVerifier = ( + canonicalPayload: Uint8Array, + signature: string, +) => boolean | Promise; + +const canonicalize = (value: unknown): string => { + if (value === null || typeof value !== "object") { + const encoded = JSON.stringify(value); + if (encoded === undefined) throw new SignedConfigurationRejected("malformed"); + return encoded; + } + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + return `{${Object.entries(value as Record) + .filter(([, nested]) => nested !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nested]) => `${JSON.stringify(key)}:${canonicalize(nested)}`) + .join(",")}}`; +}; + +const decodeBase64 = (value: string): ArrayBuffer => { + const decoded = Uint8Array.from(atob(value), (character) => character.charCodeAt(0)); + return decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength) as ArrayBuffer; +}; + +/** Builds an Ed25519 verifier from a base64-encoded SPKI public key. */ +export const makeEd25519ConfigurationVerifier = async ( + publicKeySpki: string, +): Promise => { + const publicKey = await crypto.subtle.importKey( + "spki", + decodeBase64(publicKeySpki), + "Ed25519", + false, + ["verify"], + ); + return (canonicalPayload, signature) => + crypto.subtle.verify( + "Ed25519", + publicKey, + decodeBase64(signature), + canonicalPayload.buffer.slice( + canonicalPayload.byteOffset, + canonicalPayload.byteOffset + canonicalPayload.byteLength, + ) as ArrayBuffer, + ); +}; + +/** Fetches and verifies signed measurement configuration without accepting unsigned fallback data. */ +export const fetchSignedMeasurementConfiguration = async (input: { + readonly endpoint: string; + readonly expectedProjectId: string; + readonly fetch?: typeof globalThis.fetch; + readonly persistedVersion?: number; + readonly publishableKey: string; + readonly trustedKeys: ReadonlyArray<{ readonly keyId: string; readonly publicKeySpki: string }>; +}): Promise<{ readonly payload: T; readonly keyId: string; readonly version: number }> => { + const trusted = new Map(); + for (const key of input.trustedKeys) { + trusted.set(key.keyId, await makeEd25519ConfigurationVerifier(key.publicKeySpki)); + } + const response = await (input.fetch ?? globalThis.fetch)( + `${input.endpoint.replace(/\/$/, "")}/i/v1/measurement/config`, + { headers: { "x-publishable-key": input.publishableKey }, method: "GET" }, + ); + if (!response.ok) { + throw new SignedConfigurationRejected("malformed"); + } + const candidate = (await response.json()) as SignedMeasurementConfiguration; + const verifier = new SignedMeasurementConfigurationVerifier( + input.expectedProjectId, + trusted, + () => new Date(), + input.persistedVersion, + ); + const payload = await verifier.verify(candidate); + const state = verifier.getState(); + return { keyId: candidate.keyId, payload, version: state.version }; +}; + +/** Stateful verifier enforcing signature, project, expiry, and downgrade protections. */ +export class SignedMeasurementConfigurationVerifier { + private accepted?: SignedMeasurementConfiguration; + + constructor( + private readonly projectId: string, + private readonly trustedKeys: ReadonlyMap, + private readonly now: () => Date = () => new Date(), + persistedVersion = 0, + ) { + this.persistedVersion = persistedVersion; + } + + private persistedVersion: number; + + /** Verifies and persists a strictly newer signed configuration. */ + async verify(configuration: SignedMeasurementConfiguration): Promise { + if (!Number.isInteger(configuration.version) || configuration.version < 1) { + throw new SignedConfigurationRejected("malformed"); + } + if (configuration.projectId !== this.projectId) throw new SignedConfigurationRejected("project-mismatch"); + if (!Number.isFinite(Date.parse(configuration.expiresAt))) throw new SignedConfigurationRejected("malformed"); + if (Date.parse(configuration.expiresAt) <= this.now().getTime()) throw new SignedConfigurationRejected("expired"); + if (configuration.version <= this.persistedVersion) throw new SignedConfigurationRejected("version-replay"); + const verifier = this.trustedKeys.get(configuration.keyId); + if (!verifier) throw new SignedConfigurationRejected("unknown-key"); + const signed = new TextEncoder().encode(canonicalize({ + expiresAt: configuration.expiresAt, + keyId: configuration.keyId, + payload: configuration.payload, + projectId: configuration.projectId, + version: configuration.version, + })); + if (!(await verifier(signed, configuration.signature))) { + throw new SignedConfigurationRejected("invalid-signature"); + } + this.accepted = configuration; + this.persistedVersion = configuration.version; + return configuration.payload; + } + + /** Returns the last valid configuration without exposing key material. */ + getState(): { readonly keyId?: string; readonly version: number; readonly payload?: T } { + return { + keyId: this.accepted?.keyId, + version: this.persistedVersion, + payload: this.accepted?.payload, + }; + } +} diff --git a/libraries/react-native/src/core/measurement/support-bundle.ts b/libraries/react-native/src/core/measurement/support-bundle.ts new file mode 100644 index 000000000..3f59c7e67 --- /dev/null +++ b/libraries/react-native/src/core/measurement/support-bundle.ts @@ -0,0 +1,80 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { bytesToHex } from "@noble/hashes/utils.js"; + +import type { MeasurementDebugState } from "./types"; +import { assertSafePublicValue } from "./protected-fields"; + +export interface MeasurementSupportBundle { + readonly generatedAt: string; + readonly versions: MeasurementDebugState["versions"]; + readonly installationHash: string; + readonly sessionHash?: string; + readonly readiness: MeasurementDebugState["session"]["readiness"]; + readonly outbox: { + readonly counts: MeasurementDebugState["outbox"]["counts"]; + readonly total: number; + readonly oldestAgeMs?: number; + readonly lastDelivery?: { + readonly outcome: NonNullable["outcome"]; + readonly reason?: string; + readonly attemptCount: number; + }; + }; + readonly consent: MeasurementDebugState["consent"]["effective"] & { readonly revision: number }; + readonly configuration: { + readonly revision: number; + readonly startMode: MeasurementDebugState["configuration"]["startMode"]; + readonly trustedConfigKeyIds: ReadonlyArray; + readonly signed?: MeasurementDebugState["configuration"]["signed"]; + }; + readonly collectors: ReadonlyArray<{ + readonly capability: keyof MeasurementDebugState["collectors"]; + readonly state: MeasurementDebugState["collectors"][keyof MeasurementDebugState["collectors"]]; + }>; + readonly manifest: MeasurementDebugState["manifest"]; + readonly deletion: MeasurementDebugState["deletion"]; + readonly testDevice: boolean; +} + +const hashId = (value: string): string => + bytesToHex(sha256(new TextEncoder().encode(`voidhash-support:${value}`))).slice(0, 24); + +/** Builds the opt-in diagnostic document and enforces the public-field classifier on the result. */ +export const buildMeasurementSupportBundle = ( + state: MeasurementDebugState, + now: Date = new Date(), +): MeasurementSupportBundle => { + const bundle: MeasurementSupportBundle = { + generatedAt: now.toISOString(), + versions: state.versions, + installationHash: hashId(state.installation.id), + sessionHash: state.session.current ? hashId(state.session.current.id) : undefined, + readiness: state.session.readiness, + outbox: { + counts: state.outbox.counts, + total: state.outbox.total, + oldestAgeMs: state.outbox.oldestAgeMs, + lastDelivery: state.outbox.lastDelivery && { + outcome: state.outbox.lastDelivery.outcome, + reason: state.outbox.lastDelivery.reason, + attemptCount: state.outbox.lastDelivery.attemptCount, + }, + }, + consent: { ...state.consent.effective, revision: state.consent.snapshot.revision }, + configuration: { + revision: state.configuration.revision, + startMode: state.configuration.startMode, + trustedConfigKeyIds: state.configuration.endpoints.trustedConfigKeyIds, + signed: state.configuration.signed, + }, + collectors: Object.entries(state.collectors).map(([capability, collectorState]) => ({ + capability: capability as keyof MeasurementDebugState["collectors"], + state: collectorState, + })), + manifest: state.manifest, + deletion: state.deletion, + testDevice: state.testDevice, + }; + assertSafePublicValue(bundle, "supportBundle"); + return bundle; +}; diff --git a/libraries/react-native/src/core/measurement/types.ts b/libraries/react-native/src/core/measurement/types.ts new file mode 100644 index 000000000..905d67f4d --- /dev/null +++ b/libraries/react-native/src/core/measurement/types.ts @@ -0,0 +1,589 @@ +/** A value that can be represented losslessly as JSON. */ +export type JsonValue = + | string + | number + | boolean + | null + | ReadonlyArray + | { readonly [key: string]: JsonValue }; + +/** Platforms which can originate measurement evidence. */ +export type MeasurementPlatform = "ios" | "android" | "core"; + +/** Stable source categories for evidence records. */ +export type MeasurementSource = + | "native" + | "javascript" + | "store" + | "push" + | "server-correlation"; + +/** Priority used by the durable outbox. */ +export type MeasurementPriority = "critical" | "high" | "normal" | "low"; + +/** Immutable identity state captured with an evidence record. */ +export interface IdentitySnapshot { + readonly distinctId: string; + readonly anonymousId?: string; + readonly personId?: string; + readonly revision: number; +} + +/** Application state captured with an evidence record. */ +export interface AppSnapshot { + readonly bundleId?: string; + readonly build?: string; + readonly version?: string; +} + +/** Device state safe for ordinary event context. */ +export interface DeviceSnapshot { + readonly locale?: string; + readonly platform: "ios" | "android" | "unknown"; + readonly platformVersion?: string; +} + +/** Current ATT authorization state. */ +export type AttStatus = "notDetermined" | "restricted" | "denied" | "authorized"; + +/** A revisioned consent decision. Omitted fields remain unknown. */ +export interface ConsentSnapshot { + readonly revision: number; + readonly decidedAt: string; + readonly source: "cmp" | "system" | "application" | "unknown"; + readonly gdprApplies?: boolean; + readonly dataUsage?: boolean; + readonly adsPersonalization?: boolean; + readonly adStorage?: boolean; + readonly tcf?: { readonly string: string; readonly version: "2.2" | "2.3" | string }; + readonly att?: AttStatus; + readonly collectionOptOut?: boolean; + readonly partnerSharingOptOut?: boolean; +} + +/** Effective consent state returned by the consent namespace. */ +export interface ConsentState { + readonly snapshot: ConsentSnapshot; + readonly effective: { + readonly analytics: boolean; + readonly attribution: boolean; + readonly partnerSharing: boolean; + readonly upload: boolean; + }; +} + +/** Session captured at the time an evidence record is created. */ +export interface SessionSnapshot { + readonly id: string; + readonly sequence: number; + readonly startedAt: string; + readonly reason: SessionStartReason; +} + +/** Reason a measurement session began. */ +export type SessionStartReason = "coldStart" | "foreground" | "deepLink" | "push" | "manual"; + +/** The canonical immutable measurement envelope. */ +export interface MeasurementEnvelopeV1 { + readonly schemaVersion: 1; + readonly recordId: string; + readonly type: TType; + readonly occurredAt: string; + readonly monotonicTimeNs?: string; + readonly queuedAt: string; + readonly installationId: string; + readonly installationSequence: number; + readonly session?: SessionSnapshot; + readonly identity: IdentitySnapshot; + readonly consent: ConsentSnapshot; + readonly app: AppSnapshot; + readonly device: DeviceSnapshot; + readonly source: MeasurementSource; + readonly publicPayload: TPayload; + readonly protectedPayloadRef?: string; +} + +/** Collection controls evaluated independently for every input category. */ +export interface CollectionPolicy { + readonly analytics: "enabled" | "disabled"; + readonly attribution: "enabled" | "disabled"; + readonly advertisingIdentifiers: "allowed" | "denied" | "consent-dependent"; + readonly vendorIdentifiers: "allowed" | "denied" | "consent-dependent"; + readonly networkMetadata: "allowed" | "denied"; + readonly location: "manual-only" | "denied"; + readonly upload: "enabled" | "paused"; +} + +/** Partner sharing rules, evaluated at send time. */ +export interface PartnerSharingPolicy { + readonly mode: "enabled" | "disabled"; + readonly excludedPartners?: ReadonlyArray; + readonly excludedFields?: Readonly>>; +} + +/** Runtime purchase-observation controls. */ +export interface PurchaseMeasurementConfiguration { + readonly enabled?: boolean; + readonly subscriptions?: boolean; + readonly inAppPurchases?: boolean; + readonly environment?: "production" | "sandbox"; + readonly enrichment?: { + readonly ios?: (transaction: ObservedPurchaseTransaction) => Readonly> | Promise>>; + readonly android?: Partial Readonly> | Promise>>>>; + }; +} + +/** Normalized store transaction supplied to purchase observation and enrichment callbacks. */ +export interface ObservedPurchaseTransaction { + readonly appAccountToken?: string; + readonly expirationDate?: number; + readonly isAcknowledged: boolean; + readonly isAutoRenewing?: boolean; + readonly originalTransactionId?: string; + readonly platform: "ios" | "android"; + readonly productId: string; + readonly purchaseDate: number; + readonly purchaseState: "purchased" | "pending" | "unspecified"; + readonly purchaseToken?: string; + readonly quantity: number; + readonly receipt?: string; + readonly transactionId: string; +} + +/** Android-specific collection settings. */ +export interface AndroidMeasurementConfiguration { + readonly collectAppSetId?: boolean; + readonly collectAdvertisingId?: boolean; + readonly collectNetworkMetadata?: boolean; + readonly outOfStore?: string; + readonly collectOaid?: boolean; +} + +/** iOS-specific collection settings. */ +export interface IosMeasurementConfiguration { + readonly collectIdfv?: boolean; + readonly collectAppleAds?: boolean; + readonly disableSKAD?: boolean; +} + +/** Explicit opt-in configuration for protected email and phone identity traits. */ +export interface ProtectedIdentityConfiguration { + readonly enabled?: boolean; + readonly email?: boolean; + readonly phone?: boolean; +} + +/** Full measurement configuration accepted at client construction. */ +export interface MeasurementConfiguration { + readonly startMode?: "automatic" | "manual" | "consent-gated"; + readonly sessionTimeoutMs?: number; + readonly context?: Readonly>; + readonly defaultCurrency?: string; + readonly localeOverride?: string; + readonly collection?: Partial; + readonly partnerSharing?: PartnerSharingPolicy; + readonly partnerData?: Readonly>>>; + readonly purchases?: PurchaseMeasurementConfiguration; + readonly android?: AndroidMeasurementConfiguration; + readonly ios?: IosMeasurementConfiguration; + readonly protectedIdentity?: ProtectedIdentityConfiguration; +} + +/** Validated partial update accepted by `measurement.configure`. */ +export type MeasurementConfigurationPatch = MeasurementConfiguration; + +/** Current effective measurement configuration and revision. */ +export interface MeasurementState { + readonly revision: number; + readonly configuration: Readonly> & MeasurementConfiguration>; +} + +/** Explicit stop scopes. */ +export interface MeasurementStopOptions { + readonly collection?: boolean; + readonly upload?: boolean; + readonly partnerSharing?: boolean; + readonly persist?: boolean; +} + +/** Public manual measurement inputs. */ +export type MeasurementInput = + | { readonly type: "location"; readonly latitude: number; readonly longitude: number; readonly occurredAt?: string } + | { readonly type: "attStatus"; readonly status: AttStatus; readonly source: "system" | "application" } + | { readonly type: "identifier"; readonly kind: "oaid" | "amazonAaid" | "metaAttributionId"; readonly value: string }; + +/** Result of handling a manual input. */ +export interface MeasurementHandleResult { + readonly recordId: string; + readonly accepted: true; +} + +/** Per-record delivery result exposed by `flush` and diagnostics. */ +export interface DeliveryDiagnostic { + readonly recordId: string; + readonly requestId: string; + readonly outcome: "accepted" | "rejected" | "retryScheduled" | "quarantined" | "policyBlocked"; + readonly reason?: string; + readonly attemptCount: number; + readonly occurredAt: string; +} + +/** Aggregate drain result for the native outbox. */ +export interface MeasurementFlushResult { + readonly accepted: number; + readonly scheduled: number; + readonly quarantined: number; + readonly policyBlocked: number; +} + +/** Closed capability-state vocabulary used by diagnostics and build manifests. */ +export type CollectorCapabilityState = + | "available" + | "notConfigured" + | "notImplemented" + | "notInstalled" + | "unsupported" + | "timeout" + | "permissionDenied" + | "invalidSignature" + | "collected" + | "noRules" + | "disabled" + | "error"; + +/** Known collector keys always present in the state inspector. */ +export type CollectorCapability = + | "links" + | "referrer" + | "push" + | "purchases" + | "advertisingIdentifiers" + | "vendorIdentifiers" + | "networkMetadata" + | "appleAds" + | "skan" + | "adAttributionKit"; + +/** Redacted runtime state intended for support and integration tooling. */ +export interface MeasurementDebugState { + readonly versions: { + readonly sdk: string; + readonly native: string; + readonly envelopeSchema: 1; + readonly configSchema: 1; + }; + readonly installation: { readonly id: string; readonly sequence: number; readonly firstOpenedAt: string }; + readonly session: { + readonly current?: SessionSnapshot; + readonly readiness: "uninitialized" | "nativeInitialized" | "collectorsReady" | "sdkReady" | "appGatesReady" | "sessionStarted" | "backgrounded"; + readonly stopped: Readonly; + }; + readonly outbox: { + readonly counts: Readonly>; + readonly total: number; + readonly oldestAgeMs?: number; + readonly lastDelivery?: DeliveryDiagnostic; + }; + readonly consent: ConsentState; + readonly configuration: { + readonly revision: number; + readonly startMode: "automatic" | "manual" | "consent-gated"; + readonly sessionTimeoutMs: number; + readonly defaultCurrency?: string; + readonly localeOverride?: string; + readonly contextKeys: ReadonlyArray; + readonly endpoints: { + readonly api: string; + readonly ingest: string; + readonly links: string; + readonly trustedConfigKeyIds: ReadonlyArray; + }; + readonly signed?: { + readonly keyId: string; + readonly version: number; + readonly source: "network" | "persisted"; + }; + readonly lastSignedConfigurationRejection?: string; + }; + readonly collectors: Readonly>; + readonly manifest: { readonly present: boolean; readonly version?: number }; + readonly deletion: { readonly requested: boolean; readonly completed: boolean }; + readonly testDevice: boolean; +} + +/** Safe campaign fields that may cross the public bridge. */ +export interface SafeCampaignContext { + readonly campaign?: string; + readonly channel?: string; + readonly mediaSource?: string; + readonly ad?: string; + readonly adSet?: string; +} + +/** Sources accepted by the link pipeline. */ +export type UrlSource = "appLink" | "universalLink" | "customScheme" | "push" | "manual" | "deferred" | "esp"; + +/** Unified result emitted for direct and deferred links. */ +export type DeepLinkResult = + | { + readonly status: "found"; + readonly resolutionId: string; + readonly direct: boolean; + readonly deferred: boolean; + readonly linkId?: string; + readonly route: { readonly value: string; readonly subvalues: Partial> }; + readonly campaign?: SafeCampaignContext; + readonly receivedAt: string; + readonly resolvedAt: string; + } + | { readonly status: "notFound"; readonly resolutionId: string; readonly reason: string } + | { readonly status: "error"; readonly resolutionId: string; readonly error: import("./errors").MeasurementError }; + +/** Deep-link routing and redirect-resolution configuration. */ +export interface LinkConfiguration { + readonly templateId?: string; + readonly allowedCustomParameters?: ReadonlyArray; + readonly allowedDomains?: ReadonlyArray; + readonly allowedSchemes?: ReadonlyArray; + readonly allowedRouteParameters?: ReadonlyArray; + readonly resolveWrappedDomains?: ReadonlyArray; + readonly pushPayloadPaths?: ReadonlyArray>; + readonly resolutionTimeoutMs?: number; + readonly maxRedirects?: number; + readonly wrappedRetryDelayMs?: number; + readonly allowInsecureRedirects?: boolean; + readonly dedupeWindowMs?: number; + readonly parameterRules?: ReadonlyArray<{ + readonly id: string; + readonly match: { readonly domains?: ReadonlyArray; readonly contains?: string }; + readonly parameters?: Readonly>; + readonly overwrite?: boolean; + readonly requiredPid?: "reject" | "flag"; + readonly reengagement?: boolean; + }>; + /** Supplies sensitive request headers for one wrapped-domain origin at request time. */ + readonly wrappedDomainHeaderProvider?: ( + origin: string, + ) => Promise>>; +} + +/** Push-notification runtime configuration. */ +export interface NotificationsConfiguration { + readonly registration?: "automatic" | "manual"; + readonly iosForegroundPresentation?: ReadonlyArray<"banner" | "list" | "sound" | "badge">; +} + +/** Permission values shared across APNs and Android notification permission. */ +export type PushPermissionStatus = "notDetermined" | "denied" | "authorized" | "provisional" | "ephemeral" | "notRequired"; + +/** Options for a permission request. */ +export interface PushPermissionOptions { + readonly provisional?: boolean; +} + +/** Opaque server registration state; raw platform tokens are deliberately absent. */ +export interface PushRegistration { + readonly pushDeviceTokenId: string; + readonly provider: "apns" | "fcm"; + readonly environment: "development" | "production"; + readonly registeredAt: string; +} + +/** Safe notification projection emitted on foreground receipt. */ +export interface IncomingNotification { + readonly id: string; + readonly title?: string; + readonly body?: string; + readonly receivedAt: string; + readonly pushNotificationSendId?: string; +} + +/** Safe notification projection emitted when the user opens a notification. */ +export interface OpenedNotification extends IncomingNotification { + readonly openedAt: string; + readonly link?: string; +} + +/** Notification event stream values. */ +export interface NotificationEventMap { + readonly received: IncomingNotification; + readonly opened: OpenedNotification; + readonly tokenChanged: PushRegistration; + readonly registrationError: import("./errors").MeasurementError; +} + +/** Notification event names. */ +export type NotificationEventName = keyof NotificationEventMap; + +/** Ad-impression revenue accepted by the canonical revenue route. */ +export interface AdRevenueInput { + readonly impressionId: string; + readonly monetizationNetwork: string; + readonly mediationNetwork: + | "ironsource" | "applovin_max" | "google_admob" | "fyber" | "appodeal" + | "admost" | "topon" | "tradplus" | "yandex" | "chartboost" | "unity" + | "topon_pte" | "custom_mediation" | "direct_monetization_network"; + readonly currency: string; + readonly revenue: string; + readonly country?: string; + readonly adUnit?: string; + readonly adType?: string; + readonly placement?: string; + readonly additionalParameters?: Readonly>; +} + +/** Normalized reference used for explicit validation. */ +export interface PurchaseValidationInput { + readonly transactionId: string; + readonly platform: "ios" | "android"; + readonly protectedEvidenceId: string; + readonly environment?: "production" | "sandbox"; + readonly idempotencyKey?: string; +} + +/** Cross-store lifecycle projection returned by validation. */ +export interface NormalizedStorePurchaseState { + readonly state: "purchased" | "pending" | "cancelled" | "refunded" | "expired" | "paused" | "grace"; + readonly productId?: string; + readonly subscriptionState?: string; + readonly test?: boolean; + readonly lineItems?: ReadonlyArray<{ readonly productId: string; readonly quantity: number }>; + readonly cancellation?: { + readonly at?: string; + readonly reason: "customer" | "billing" | "developer" | "price-change" | "unknown"; + }; + readonly pause?: { readonly startsAt: string; readonly resumesAt?: string }; + readonly offer?: { readonly id?: string; readonly type: "introductory" | "promotional" | "offer-code" | "base-plan" }; + readonly replacement?: { + readonly mode: "immediate" | "deferred" | "prorated"; + readonly replacedProductId: string; + }; + readonly prepaid?: { readonly expiresAt?: string; readonly topUpEligible: boolean }; + readonly priceChange?: { + readonly currency?: string; + readonly price?: string; + readonly state: "pending" | "accepted" | "rejected"; + }; +} + +/** Correlated normalized validation result. */ +export interface PurchaseValidationResult { + readonly requestId: string; + readonly transactionId: string; + readonly outcome: "valid" | "invalid" | "indeterminate"; + readonly storeState?: NormalizedStorePurchaseState; + readonly failure?: { readonly kind: "network" | "store" | "configuration" | "server"; readonly message: string }; +} + +/** Input accepted by the invite-link service. */ +export interface InviteLinkInput { + readonly channel?: string; + readonly campaign?: string; + readonly referrerCustomerId?: string; + readonly referrerUid?: string; + readonly referrerName?: string; + readonly referrerImageUrl?: string; + readonly deepLinkValue: string; + readonly deepLinkSubvalues?: Partial>; + readonly baseDeepLink?: string; + readonly brandedDomain?: string; + readonly appleAppId?: string; + readonly customParameters?: Readonly>; +} + +/** Signed link returned to the application. */ +export interface GeneratedLink { + readonly linkId: string; + readonly url: string; + readonly expiresAt?: string; +} + +/** Cross-promotion tracking input. */ +export interface CrossPromotionInput { + readonly action: "impression" | "openStore"; + readonly promotedAppId: string; + readonly campaign?: string; + readonly parameters?: Readonly>; +} + +/** Result of a cross-promotion action. */ +export interface CrossPromotionResult { + readonly recordId: string; + readonly opened?: boolean; + readonly link?: GeneratedLink; +} + +/** Safe attribution decision exposed to an SDK client. */ +export interface AttributionDecision { + readonly decisionId: string; + readonly modelVersion: string; + readonly kind: "install" | "reengagement" | "organic"; + readonly campaign?: SafeCampaignContext; + readonly direct?: boolean; + readonly deterministic: boolean; + readonly reason: string; +} + +/** Initial client-side conversion projection. */ +export interface InstallConversionResult { + readonly installationId: string; + readonly attributed: boolean; + readonly campaign?: SafeCampaignContext; + readonly deferred: boolean; +} + +/** Events emitted by the measurement namespace. */ +export interface MeasurementEventMap { + readonly error: import("./errors").MeasurementError; + readonly attribution: AttributionDecision; + readonly attributionError: import("./errors").MeasurementError; + readonly conversion: InstallConversionResult; + readonly delivery: DeliveryDiagnostic; + readonly purchaseValidation: PurchaseValidationResult; + readonly session: SessionSnapshot; +} + +/** Measurement event names. */ +export type MeasurementEventName = keyof MeasurementEventMap; + +/** Public measurement namespace. */ +export interface MeasurementClient { + configure(patch: MeasurementConfigurationPatch): Promise; + start(options?: { readonly reason?: SessionStartReason }): Promise; + stop(options?: MeasurementStopOptions): Promise; + handle(input: MeasurementInput): Promise; + on(event: E, listener: (value: MeasurementEventMap[E]) => void): () => void; + getState(): Promise; + createSupportBundle(): Promise; + getInstallationId(): Promise; + createInviteLink(input: InviteLinkInput): Promise; + trackInviteShare(input: { readonly linkId: string; readonly channel: string }): Promise; + trackCrossPromotion(input: CrossPromotionInput): Promise; + trackAdRevenue(input: AdRevenueInput): Promise; + validatePurchase(input: PurchaseValidationInput): Promise; + deleteData(): Promise<{ readonly requestId: string; readonly status: "accepted" }>; + setTestDevice(enabled: boolean): Promise; +} + +/** Public link namespace. */ +export interface LinksClient { + handle(input: { readonly url: string; readonly source: UrlSource; readonly receivedAt?: string }): Promise; + on(event: "deepLink", listener: (value: DeepLinkResult) => void): () => void; +} + +/** Public consent namespace. */ +export interface ConsentClient { + set(consent: ConsentSnapshot): Promise; + get(): Promise; +} + +/** Public notification namespace. */ +export interface NotificationsClient { + getPermissionStatus(): Promise; + requestPermission(options?: PushPermissionOptions): Promise; + register(): Promise; + unregister(): Promise; + getRegistration(): Promise; + setBadgeCount(count: number): Promise; + on(event: E, listener: (value: NotificationEventMap[E]) => void): () => void; +} diff --git a/libraries/react-native/src/core/paywalls/paywall-runtime-config.ts b/libraries/react-native/src/core/paywalls/paywall-runtime-config.ts index ddf375796..93e2ef71a 100644 --- a/libraries/react-native/src/core/paywalls/paywall-runtime-config.ts +++ b/libraries/react-native/src/core/paywalls/paywall-runtime-config.ts @@ -6,6 +6,18 @@ import type { import type { Product, SubscriptionProduct } from "../entities/product"; import type { PaywallReleaseRuntime } from "./paywall-service"; +const toBridgeVariables = ( + variables: Readonly>, +): Readonly> => + Object.fromEntries( + Object.entries(variables).filter( + (entry): entry is [string, string | number | boolean] => + typeof entry[1] === "string" || + typeof entry[1] === "number" || + typeof entry[1] === "boolean", + ), + ); + const PERIOD_BY_NORMALIZED_INTERVAL: Readonly> = { // ISO-8601 billing periods (Play Billing `billingPeriod`). p1m: "month", @@ -81,7 +93,7 @@ export function buildPaywallRuntimeConfig(options: { return { products, - variables: options.runtime.variables, + variables: toBridgeVariables(options.runtime.variables), locale: options.locale, platform: options.platform === "unknown" ? undefined : options.platform, defaultSelectedProductId: products[0]?.id, diff --git a/libraries/react-native/src/core/sdk-configuration.ts b/libraries/react-native/src/core/sdk-configuration.ts index 1622cadb9..1e3ae517e 100644 --- a/libraries/react-native/src/core/sdk-configuration.ts +++ b/libraries/react-native/src/core/sdk-configuration.ts @@ -1,4 +1,5 @@ import { Context } from "effect"; +import type { UnifiedMeasurementRuntime } from "./measurement/runtime"; export class SdkConfiguration extends Context.Service< SdkConfiguration, @@ -8,5 +9,6 @@ export class SdkConfiguration extends Context.Service< readonly ingestUrl: string | undefined; readonly publishableKey: string; readonly readOnly: boolean; + readonly measurementRuntime: UnifiedMeasurementRuntime; } >()("rn-voidhash/SdkConfiguration") {} diff --git a/libraries/react-native/src/core/transactions/transaction-service.ts b/libraries/react-native/src/core/transactions/transaction-service.ts index 1531e5117..cbed131c5 100644 --- a/libraries/react-native/src/core/transactions/transaction-service.ts +++ b/libraries/react-native/src/core/transactions/transaction-service.ts @@ -1,6 +1,5 @@ import { Cause, Context, Deferred, Effect, Exit, Layer } from "effect"; -import { CacheManager } from "../caching/cache-manager"; import type { Product } from "../entities/product"; import type { Transaction } from "../entities/transaction"; import { PersonInfoManager } from "../identity/person-info-manager"; @@ -13,19 +12,9 @@ import { getCommonSdkHeaders } from "../utils/get-common-sdk-headers"; import { deriveAccountToken } from "../utils/account-token"; import { ReconcileTransactionsError } from "./errors"; -const PROCESSED_TRANSACTION_TTL_MS = 1000 * 60 * 30; - -interface TransactionProcessingState { - readonly backendAccepted: boolean; - readonly storeFinalized: boolean; -} - const buildTransactionProcessingKey = (transaction: Transaction) => `${transaction.platform}:${transaction.transactionId}:${transaction.purchaseDate}`; -const getProcessedTransactionCacheKey = (transactionProcessingKey: string) => - `processed-transaction:${transactionProcessingKey}`; - const resolveTransactionProductSlug = ( transaction: Transaction, productDefinitions: Readonly>, @@ -96,16 +85,14 @@ const mapTransactionToSyncPayload = ( /** * Owns the transaction lifecycle: deduplicated server-side sync, observation * reconciliation, purchase orchestration, restore-purchases, and the native - * transaction observer. Holds an in-memory `inFlightKeys` set per runtime to - * coalesce concurrent sync attempts for the same transaction (the cache TTL - * catches duplicate attempts across runtime restarts). + * transaction observer. Concurrent work is coalesced per runtime while the + * native measurement store remains the durable deduplication authority. */ export class TransactionService extends Context.Service()( "rn-voidhash/TransactionService", { make: Effect.gen(function* () { const apiClient = yield* ApiClient; - const cacheManager = yield* CacheManager; const personInfoManager = yield* PersonInfoManager; const identityManager = yield* IdentityManager; const paymentAdapter = yield* PaymentAdapter; @@ -137,20 +124,13 @@ export class TransactionService extends Context.Service()( inFlightTransactions.set(transactionProcessingKey, deferred); const execution = Effect.gen(function* () { - const processedCacheKey = getProcessedTransactionCacheKey(transactionProcessingKey); - const cachedTransaction = yield* cacheManager.get( - processedCacheKey, + const finalized = yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.hasDurableDedupe( + "transaction-finalized", + transactionProcessingKey, + ), ); - const cachedState = - cachedTransaction && !cachedTransaction.isExpired - ? cachedTransaction.value === true - ? { backendAccepted: true, storeFinalized: true } - : cachedTransaction.value === false - ? undefined - : cachedTransaction.value - : undefined; - - if (cachedState?.storeFinalized) { + if (finalized) { return; } @@ -164,7 +144,17 @@ export class TransactionService extends Context.Service()( return; } - if (!cachedState?.backendAccepted) { + yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.recordObservedPurchase(transaction), + ); + + const backendAccepted = yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.hasDurableDedupe( + "transaction-backend-accepted", + transactionProcessingKey, + ), + ); + if (!backendAccepted) { const commonHeaders = yield* getCommonSdkHeaders(); const distinctId = yield* identityManager.getDistinctId(); @@ -176,14 +166,21 @@ export class TransactionService extends Context.Service()( payload: mapTransactionToSyncPayload(transaction, schema.products), }); - yield* cacheManager.set( - processedCacheKey, - { backendAccepted: true, storeFinalized: transaction.isAcknowledged }, - { ttl: PROCESSED_TRANSACTION_TTL_MS }, + yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.checkAndSetDurableDedupe( + "transaction-backend-accepted", + transactionProcessingKey, + ), ); } if (sdkConfiguration.readOnly) { + yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.checkAndSetDurableDedupe( + "transaction-finalized", + transactionProcessingKey, + ), + ); return; } @@ -194,10 +191,11 @@ export class TransactionService extends Context.Service()( ); } - yield* cacheManager.set( - processedCacheKey, - { backendAccepted: true, storeFinalized: true }, - { ttl: PROCESSED_TRANSACTION_TTL_MS }, + yield* Effect.promise(() => + sdkConfiguration.measurementRuntime.checkAndSetDurableDedupe( + "transaction-finalized", + transactionProcessingKey, + ), ); }); diff --git a/libraries/react-native/src/index.ts b/libraries/react-native/src/index.ts index 1cbd9d098..01862279f 100644 --- a/libraries/react-native/src/index.ts +++ b/libraries/react-native/src/index.ts @@ -2,6 +2,7 @@ export * from "./client"; export * from "./client-react-native"; export * from "./core/schema"; export * from "./core/types"; +export * from "./core/measurement"; export * from "./core/utils"; export * from "./react/components/provider"; export * from "./react/hooks/use-paywall-by-location"; diff --git a/libraries/react-native/src/nitro.ts b/libraries/react-native/src/nitro.ts index 94082d2bc..b3130e4d9 100644 --- a/libraries/react-native/src/nitro.ts +++ b/libraries/react-native/src/nitro.ts @@ -4,6 +4,8 @@ import { NitroModules } from "react-native-nitro-modules"; import type { GoogleBilling as GoogleBillingSpec } from "./specs/android/GoogleBilling.nitro"; import type { PaywallPresenter as PaywallPresenterSpec } from "./specs/PaywallPresenter.nitro"; import type { Storekit as StorekitSpec } from "./specs/ios/Storekit.nitro"; +import type { Measurement as MeasurementSpec } from "./specs/measurement/Measurement.nitro"; +import type { Notifications as NotificationsSpec } from "./specs/notifications/Notifications.nitro"; export const Storekit: StorekitSpec | undefined = Platform.select({ android: undefined, @@ -19,3 +21,11 @@ export const PaywallPresenter: PaywallPresenterSpec | undefined = Platform.selec android: () => NitroModules.createHybridObject("PaywallPresenter"), ios: () => NitroModules.createHybridObject("PaywallPresenter"), })?.(); + +export const Measurement: MeasurementSpec = NitroModules.createHybridObject( + "Measurement", +); + +export const Notifications: NotificationsSpec = NitroModules.createHybridObject( + "Notifications", +); diff --git a/libraries/react-native/src/react/hooks/use-paywall-by-location.ts b/libraries/react-native/src/react/hooks/use-paywall-by-location.ts index 1a83b5300..3882ed100 100644 --- a/libraries/react-native/src/react/hooks/use-paywall-by-location.ts +++ b/libraries/react-native/src/react/hooks/use-paywall-by-location.ts @@ -154,7 +154,14 @@ async function sendConfigureMessage(options: { createPaywallBridgeConfigureMessage( { products: [], - variables: runtime.variables ?? {}, + variables: Object.fromEntries( + Object.entries(runtime.variables ?? {}).filter( + (entry): entry is [string, string | number | boolean] => + typeof entry[1] === "string" || + typeof entry[1] === "number" || + typeof entry[1] === "boolean", + ), + ), platform: getBridgePlatform(), }, requestId, diff --git a/libraries/react-native/src/specs/measurement/Measurement.nitro.ts b/libraries/react-native/src/specs/measurement/Measurement.nitro.ts new file mode 100644 index 000000000..26a905cd6 --- /dev/null +++ b/libraries/react-native/src/specs/measurement/Measurement.nitro.ts @@ -0,0 +1,39 @@ +import type { HybridObject } from "react-native-nitro-modules"; +import type { + MeasurementBridgeEvent, + MeasurementCommand, + MeasurementCommandResult, + MeasurementConfigurationStateBridge, + MeasurementFlushBridgeResult, + MeasurementInboxEntry, + MeasurementInitializeConfiguration, + MeasurementProtectedEvidenceInput, + MeasurementStateBridge, +} from "./MeasurementTypes.nitro"; + +export interface Measurement extends HybridObject<{ ios: "swift"; android: "kotlin" }> { + initialize(publishableKey: string, configuration: MeasurementInitializeConfiguration): Promise; + enqueue(command: MeasurementCommand): Promise; + flush(): Promise; + getInstallationId(): Promise; + getState(): Promise; + subscribe(subscriptionId: string, listener: (event: MeasurementBridgeEvent) => void): void; + unsubscribe(subscriptionId: string): void; + peekInbox(limit: number): Promise; + acknowledgeInbox(entryId: string): Promise; + readProtectedEvidence(blobId: string): Promise; + putProtectedEvidence(input: MeasurementProtectedEvidenceInput): Promise; + deleteProtectedEvidence(blobId: string): Promise; + deleteProtectedData(requestId: string): Promise; + getMeasurementConfigurationState(): Promise; + persistMeasurementConfigurationState(version: number, payload: ArrayBuffer): Promise; + applyMeasurementConfiguration(version: number, payload: ArrayBuffer): Promise; + applyMeasurementStorageLimits(maxOutboxRecords: number, maxOutboxBytes: number, maxProtectedBytes: number): Promise; + getPushRegistrationState(): Promise; + persistPushRegistrationState(payload: ArrayBuffer): Promise; + clearPushRegistrationState(): Promise; + getTestDeviceState(): Promise; + persistTestDeviceState(enabled: boolean): Promise; + hasDedupe(namespace: string, key: string): Promise; + checkAndSetDedupe(namespace: string, key: string, expiresAtMs: number): Promise; +} diff --git a/libraries/react-native/src/specs/measurement/MeasurementTypes.nitro.ts b/libraries/react-native/src/specs/measurement/MeasurementTypes.nitro.ts new file mode 100644 index 000000000..0fe7f3909 --- /dev/null +++ b/libraries/react-native/src/specs/measurement/MeasurementTypes.nitro.ts @@ -0,0 +1,197 @@ +export type MeasurementBridgeSource = "ios" | "android" | "core"; +export type MeasurementRecordSource = "native" | "javascript" | "store" | "push" | "server-correlation"; +export type MeasurementRecordPriority = "critical" | "high" | "normal" | "low"; +export type MeasurementCommandKind = + | "enqueueRecord" + | "identityTransition" + | "consentTransition" + | "sessionSignal" + | "coldLaunchInput" + | "transactionDedup" + | "linkInput" + | "pushInput" + | "purchaseInput" + | "identifierInput"; +export interface MeasurementBridgeError { + readonly code: string; + readonly message: string; + readonly source: MeasurementBridgeSource; + readonly capability?: string; + readonly reason?: string; +} + +export interface MeasurementIdentitySnapshot { + readonly distinctId: string; + readonly anonymousId?: string; + readonly personId?: string; + readonly revision: number; +} + +export interface MeasurementConsentSnapshot { + readonly revision: number; + readonly decidedAt: string; + readonly source: string; + readonly gdprApplies?: boolean; + readonly dataUsage?: boolean; + readonly adsPersonalization?: boolean; + readonly adStorage?: boolean; + readonly collectionOptOut?: boolean; + readonly partnerSharingOptOut?: boolean; +} + +export interface MeasurementSessionSnapshot { + readonly id: string; + readonly sequence: number; + readonly startedAt: string; + readonly reason: string; +} + +export interface MeasurementInitializeConfiguration { + readonly apiUrl: string; + readonly ingestUrl: string; + readonly linksUrl: string; + readonly trustedConfigKeyIds: string[]; +} + +export interface MeasurementConfigurationStateBridge { + readonly version: number; + readonly payload?: ArrayBuffer; +} + +interface MeasurementCommandBase { + readonly commandId: string; + readonly recordType: string; + readonly occurredAt: string; + readonly source: MeasurementRecordSource; + readonly priority: MeasurementRecordPriority; + readonly publicPayload: ArrayBuffer; + readonly protectedEvidenceRef?: string; + readonly identity?: MeasurementIdentitySnapshot; + readonly consent?: MeasurementConsentSnapshot; + readonly session?: MeasurementSessionSnapshot; +} + +export interface MeasurementEnqueueRecordCommand extends MeasurementCommandBase { + readonly kind: "enqueueRecord"; +} + +export interface MeasurementIdentityTransitionCommand extends MeasurementCommandBase { + readonly kind: "identityTransition"; +} + +export interface MeasurementConsentTransitionCommand extends MeasurementCommandBase { + readonly kind: "consentTransition"; +} + +export interface MeasurementSessionSignalCommand extends MeasurementCommandBase { + readonly kind: "sessionSignal"; +} + +export interface MeasurementColdLaunchInputCommand extends MeasurementCommandBase { + readonly kind: "coldLaunchInput"; +} + +export interface MeasurementTransactionDedupCommand extends MeasurementCommandBase { + readonly kind: "transactionDedup"; +} + +export interface MeasurementLinkInputCommand extends MeasurementCommandBase { + readonly kind: "linkInput"; +} + +export interface MeasurementPushInputCommand extends MeasurementCommandBase { + readonly kind: "pushInput"; +} + +export interface MeasurementPurchaseInputCommand extends MeasurementCommandBase { + readonly kind: "purchaseInput"; +} + +export interface MeasurementIdentifierInputCommand extends MeasurementCommandBase { + readonly kind: "identifierInput"; +} + +export type MeasurementCommandSchema = + | MeasurementEnqueueRecordCommand + | MeasurementIdentityTransitionCommand + | MeasurementConsentTransitionCommand + | MeasurementSessionSignalCommand + | MeasurementColdLaunchInputCommand + | MeasurementTransactionDedupCommand + | MeasurementLinkInputCommand + | MeasurementPushInputCommand + | MeasurementPurchaseInputCommand + | MeasurementIdentifierInputCommand; + +export interface MeasurementCommand extends MeasurementCommandBase { + readonly kind: MeasurementCommandKind; +} + +export interface MeasurementCommandResult { + readonly accepted: boolean; + readonly recordId?: string; + readonly installationSequence?: number; + readonly error?: MeasurementBridgeError; +} + +export interface MeasurementFlushBridgeResult { + readonly accepted: number; + readonly scheduled: number; + readonly quarantined: number; + readonly policyBlocked: number; +} + +export interface MeasurementStateBridge { + readonly installationId: string; + readonly firstOpenedAt: string; + readonly installationSequence: number; + readonly readiness: string; + readonly currentSessionId?: string; + readonly currentSessionSequence?: number; + readonly consentRevision: number; + readonly configurationRevision: number; + readonly outboxCritical: number; + readonly outboxHigh: number; + readonly outboxNormal: number; + readonly outboxLow: number; + readonly oldestRecordAgeMs?: number; +} + +export interface MeasurementBridgeEvent { + readonly subscriptionId: string; + readonly event: string; + readonly recordId?: string; + readonly requestId?: string; + readonly payload?: ArrayBuffer; + readonly error?: MeasurementBridgeError; +} + +export interface MeasurementInboxEntry { + readonly id: string; + readonly kind: string; + readonly source: string; + readonly appState: string; + readonly receivedAt: string; + readonly protectedEvidenceRef: string; +} + +export type MeasurementProtectedPurpose = + | "advertising-identifier" + | "diagnostic-authorization" + | "email" + | "install-referrer" + | "link-capture" + | "partner-context" + | "phone" + | "purchase-receipt" + | "push-token"; + +export type MeasurementProtectedRetention = "ephemeral" | "installation" | "legal" | "transaction"; + +export interface MeasurementProtectedEvidenceInput { + readonly blobId: string; + readonly purpose: MeasurementProtectedPurpose; + readonly consentRevision: number; + readonly retentionClass: MeasurementProtectedRetention; + readonly value: ArrayBuffer; +} diff --git a/libraries/react-native/src/specs/notifications/Notifications.nitro.ts b/libraries/react-native/src/specs/notifications/Notifications.nitro.ts new file mode 100644 index 000000000..a1dd6e1dc --- /dev/null +++ b/libraries/react-native/src/specs/notifications/Notifications.nitro.ts @@ -0,0 +1,34 @@ +import type { HybridObject } from "react-native-nitro-modules"; + +export type NativePushProvider = "apns" | "fcm"; +export type NativePushEnvironment = "development" | "production"; +export type NativeNotificationEventKind = + | "received" + | "opened" + | "tokenChanged" + | "registrationError"; + +export interface NativePushToken { + readonly token: string; + readonly provider: NativePushProvider; + readonly environment: NativePushEnvironment; +} + +export interface NativeNotificationEvent { + readonly id: string; + readonly kind: NativeNotificationEventKind; + readonly occurredAt: string; + readonly protectedPayloadRef?: string; + readonly pushNotificationSendId?: string; + readonly link?: string; + readonly errorCode?: string; +} + +export interface Notifications extends HybridObject<{ ios: "swift"; android: "kotlin" }> { + getPermissionStatus(): Promise; + requestPermission(provisional: boolean): Promise; + getToken(): Promise; + setBadgeCount(count: number): Promise; + subscribe(subscriptionId: string, listener: (event: NativeNotificationEvent) => void): void; + unsubscribe(subscriptionId: string): void; +} diff --git a/libraries/react-native/tests/core/client-effect.test.ts b/libraries/react-native/tests/core/client-effect.test.ts index fe65b4344..1e0dc2e83 100644 --- a/libraries/react-native/tests/core/client-effect.test.ts +++ b/libraries/react-native/tests/core/client-effect.test.ts @@ -833,6 +833,7 @@ describe("VoidhashEffectClient", () => { it("resumes native finalization from persistent state after a runtime restart", async () => { const schema = createTestSchema(); const cache = createInMemoryCacheAdapter(); + const dedupeStore = new Map(); const firstApi = createApiClientDouble(); const firstPayment = createPaymentAdapterDouble({ acknowledgePurchaseShouldFailTimes: 1, @@ -840,6 +841,7 @@ describe("VoidhashEffectClient", () => { const firstHarness = createEffectTestHarness({ apiClient: firstApi.apiClient, cacheAdapter: cache.adapter, + dedupeStore, paymentAdapter: firstPayment.paymentAdapter, }); const transaction = new Transaction( @@ -865,6 +867,7 @@ describe("VoidhashEffectClient", () => { const secondHarness = createEffectTestHarness({ apiClient: secondApi.apiClient, cacheAdapter: cache.adapter, + dedupeStore, paymentAdapter: secondPayment.paymentAdapter, }); @@ -1161,6 +1164,7 @@ describe("VoidhashEffectClient", () => { const analyticsEvents: ReadonlyArray = [ { context: {}, + distinct_id: "analytics-user", event_id: "evt_1", event_name: "cta-button-clicked", event_ts: "2026-01-01T00:00:00.000Z", diff --git a/libraries/react-native/tests/core/expo-plugin-validation.test.ts b/libraries/react-native/tests/core/expo-plugin-validation.test.ts new file mode 100644 index 000000000..5149444ba --- /dev/null +++ b/libraries/react-native/tests/core/expo-plugin-validation.test.ts @@ -0,0 +1,205 @@ +import { describe, expect, it } from "vitest"; + +import { + applyVoidhashAndroidPermissions, + applyVoidhashIosBuildSettings, + applyVoidhashIosInfoPlist, + validateVoidhashExpoPluginOptions, +} from "../../plugin/src/withVoidhashReactNative"; +import { diagnoseVoidhashIntegration } from "../../plugin/src/doctor"; +import { generateStoreDisclosureInputs } from "../../plugin/src/storeDisclosures"; + +describe("Voidhash Expo plugin validation", () => { + it("accepts a complete link and push configuration", () => { + expect(() => + validateVoidhashExpoPluginOptions({ + measurement: { + android: { + appLinks: [{ autoVerify: true, host: "links.example.com", pathPrefix: "/open" }], + urlSchemes: ["voidhash-demo"], + }, + ios: { + adAttributionKitPostbackEndpoint: "https://attribution.example.com", + associatedDomains: ["applinks:links.example.com"], + skAdNetworkPostbackEndpoint: "https://skan.example.com", + urlSchemes: ["voidhash-demo"], + }, + }, + notifications: { + android: { + defaultChannel: { id: "updates", name: "Updates" }, + googleServicesFile: "./google-services.json", + }, + enabled: true, + ios: { apsEnvironment: "development", backgroundRemoteNotifications: true }, + }, + }), + ).not.toThrow(); + }); + + it.each([ + [{ measurement: { ios: { associatedDomains: ["not a host"] } } }, "associated domain"], + [{ measurement: { android: { appLinks: [{ host: "localhost" }] } } }, "App Link host"], + [{ measurement: { android: { appLinks: [{ host: "example.com", pathPrefix: "open" }] } } }, "pathPrefix"], + [{ measurement: { ios: { urlSchemes: ["1invalid"] } } }, "URL scheme"], + [{ notifications: { android: {}, enabled: true } }, "googleServicesFile"], + [{ notifications: { android: { defaultChannel: { id: "", name: "Updates" } } } }, "requires id and name"], + [{ notifications: { enabled: true } }, "googleServicesFile"], + [{ notifications: { ios: { apsEnvironment: "invalid" as never } } }, "apsEnvironment"], + [{ measurement: { ios: { skAdNetworkPostbackEndpoint: "http://example.com" } } }, "HTTPS origin"], + [{ measurement: { ios: { adAttributionKitPostbackEndpoint: "https://example.com/path" } } }, "HTTPS origin"], + [{ measurement: { ios: { disableSKAD: true, skAdNetworkPostbackEndpoint: "https://example.com" } } }, "cannot be combined"], + [{ measurement: { ios: { privacyMode: "strict-no-idfa", requireAdvertisingId: true } } }, "strict-no-idfa"], + [{ measurement: { android: { advertisingIdPermission: "remove", requireAdvertisingId: true } } }, "advertisingIdPermission"], + [{ measurement: { buildMode: "production", purchaseValidationEnvironment: "sandbox" } }, "sandbox purchase validation"], + ] as const)("rejects contradictory configuration containing %s", (options, message) => { + expect(() => validateVoidhashExpoPluginOptions(options)).toThrow(message); + }); + + it("explicitly includes or removes Android AD_ID across manifest merging", () => { + const included = applyVoidhashAndroidPermissions( + { "uses-permission": [] }, + { measurement: { android: { advertisingIdPermission: "include" } } }, + ); + expect(included["uses-permission"]).toContainEqual({ + $: { "android:name": "com.google.android.gms.permission.AD_ID" }, + }); + + const removed = applyVoidhashAndroidPermissions( + { "uses-permission": [{ $: { "android:name": "com.google.android.gms.permission.AD_ID" } }] }, + { measurement: { android: { advertisingIdPermission: "remove" } } }, + ); + expect(removed.$?.["xmlns:tools"]).toBe("http://schemas.android.com/tools"); + expect(removed["uses-permission"]).toContainEqual({ + $: { + "android:name": "com.google.android.gms.permission.AD_ID", + "tools:node": "remove", + }, + }); + }); + + it("writes both Apple attribution endpoints and removes both when disabled", () => { + const plist: Record = {}; + applyVoidhashIosInfoPlist(plist, { + measurement: { + ios: { + adAttributionKitPostbackEndpoint: "https://aak.example.com", + skAdNetworkPostbackEndpoint: "https://skan.example.com", + }, + }, + }); + expect(plist).toMatchObject({ + AttributionCopyEndpoint: "https://aak.example.com", + NSAdvertisingAttributionReportEndpoint: "https://skan.example.com", + }); + applyVoidhashIosInfoPlist(plist, { measurement: { ios: { disableSKAD: true } } }); + expect(plist).not.toHaveProperty("AttributionCopyEndpoint"); + expect(plist).not.toHaveProperty("NSAdvertisingAttributionReportEndpoint"); + }); + + it("adds and removes the strict no-IDFA Swift compilation condition deterministically", () => { + const settings: Record = { + SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DEBUG VOIDHASH_STRICT_NO_IDFA", + }; + applyVoidhashIosBuildSettings(settings, { measurement: { ios: { privacyMode: "standard" } } }); + expect(settings.SWIFT_ACTIVE_COMPILATION_CONDITIONS).toBe("$(inherited) DEBUG"); + applyVoidhashIosBuildSettings(settings, { measurement: { ios: { privacyMode: "strict-no-idfa" } } }); + applyVoidhashIosBuildSettings(settings, { measurement: { ios: { privacyMode: "strict-no-idfa" } } }); + expect(settings.SWIFT_ACTIVE_COMPILATION_CONDITIONS).toBe( + "$(inherited) DEBUG VOIDHASH_STRICT_NO_IDFA", + ); + }); + + it("shares plugin validation and reports actionable doctor codes without secrets", () => { + const report = diagnoseVoidhashIntegration({ + options: { + measurement: { + android: { advertisingIdPermission: "remove", requireAdvertisingId: true }, + ios: { associatedDomains: ["links.example.com"] }, + }, + notifications: { enabled: true, ios: { apsEnvironment: "production" } }, + }, + androidManifest: "", + googleServicesPresent: false, + iosEntitlements: "", + }); + expect(report.ok).toBe(false); + expect(report.findings.map((finding) => finding.code)).toEqual(expect.arrayContaining([ + "VH_CFG_CONTRADICTION", + "VH_IOS_APS_ENTITLEMENT_MISSING", + "VH_ANDROID_GOOGLE_SERVICES_MISSING", + "VH_IOS_ASSOCIATED_DOMAINS_MISSING", + ])); + expect(JSON.stringify(report)).not.toMatch(/token|password|secret/i); + }); + + it("doctor reports every required broken-integration fixture and accepts a complete bare project", () => { + const baseOptions = { + measurement: { + android: { + appLinks: [{ autoVerify: true, host: "links.example.com" }], + backupPolicy: "voidhash-no-backup" as const, + }, + ios: { + adAttributionKitPostbackEndpoint: "https://aak.example.com", + associatedDomains: ["links.example.com"], + skAdNetworkPostbackEndpoint: "https://skan.example.com", + }, + }, + notifications: { + android: { googleServicesFile: "./google-services.json" }, + enabled: true, + ios: { apsEnvironment: "production" as const }, + }, + }; + const broken = diagnoseVoidhashIntegration({ + options: baseOptions, + androidManifest: "", + googleServicesPresent: false, + iosEntitlements: "", + iosInfoPlist: "", + }); + expect(broken.findings.map(({ code }) => code)).toEqual(expect.arrayContaining([ + "VH_ANDROID_APP_LINK_MISSING", + "VH_ANDROID_FCM_HOOK_MISSING", + "VH_ANDROID_GOOGLE_SERVICES_MISSING", + "VH_ANDROID_NO_BACKUP_UNVERIFIED", + "VH_IOS_ADATTRIBUTIONKIT_PLIST_MISSING", + "VH_IOS_APS_ENTITLEMENT_MISSING", + "VH_IOS_ASSOCIATED_DOMAINS_MISSING", + "VH_IOS_SKAN_PLIST_MISSING", + ])); + const complete = diagnoseVoidhashIntegration({ + options: baseOptions, + androidApplicationSource: "class VoidhashPushFirebaseMessagingService", + androidManifest: '', + googleServicesPresent: true, + iosEntitlements: "aps-environment com.apple.developer.associated-domains", + iosInfoPlist: "NSAdvertisingAttributionReportEndpoint AttributionCopyEndpoint", + }); + expect(complete).toMatchObject({ ok: true, findings: [] }); + }); + + it("derives store disclosures from strict privacy and notification capability toggles", () => { + const full = generateStoreDisclosureInputs({ + measurement: { + android: { advertisingIdPermission: "include" }, + ios: { privacyMode: "standard", requireAdvertisingId: true }, + }, + notifications: { enabled: true }, + }); + expect(full.apple.tracking).toBe(true); + expect(full.apple.collectedData).toContain("advertising-identifier"); + expect(full.googlePlay.collectedData).toContain("push-token"); + const strict = generateStoreDisclosureInputs({ + measurement: { + android: { advertisingIdPermission: "remove" }, + ios: { privacyMode: "strict-no-idfa" }, + }, + notifications: { enabled: false }, + }); + expect(strict.apple.tracking).toBe(false); + expect(strict.apple.collectedData).not.toContain("advertising-identifier"); + expect(strict.googlePlay.collectedData).not.toContain("push-token"); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-bridge-contract.test.ts b/libraries/react-native/tests/core/measurement-bridge-contract.test.ts new file mode 100644 index 000000000..4fcf5c124 --- /dev/null +++ b/libraries/react-native/tests/core/measurement-bridge-contract.test.ts @@ -0,0 +1,55 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +import { mapNativeMeasurementError } from "../../src/core/measurement"; + +const typesSource = readFileSync( + new URL("../../src/specs/measurement/MeasurementTypes.nitro.ts", import.meta.url), + "utf8", +); + +describe("measurement Nitro bridge contract", () => { + it("has a discriminated internal command schema for every durable command category", () => { + const schema = typesSource.slice( + typesSource.indexOf("export type MeasurementCommandSchema"), + typesSource.indexOf("export interface MeasurementCommand extends"), + ); + for (const variant of [ + "MeasurementEnqueueRecordCommand", + "MeasurementIdentityTransitionCommand", + "MeasurementConsentTransitionCommand", + "MeasurementSessionSignalCommand", + "MeasurementColdLaunchInputCommand", + "MeasurementTransactionDedupCommand", + ]) { + expect(schema).toContain(variant); + } + expect(typesSource).toMatch(/readonly kind: "enqueueRecord"/); + expect(typesSource).toMatch(/readonly kind: "transactionDedup"/); + }); + + it("does not permit JSON strings or unknown records as bridge payloads", () => { + const payloadDeclarations = [...typesSource.matchAll(/readonly \w*[Pp]ayload\w*\??:\s*([^;]+);/g)]; + expect(payloadDeclarations.length).toBeGreaterThan(0); + for (const declaration of payloadDeclarations) { + expect(declaration[1]).not.toContain("string"); + expect(declaration[1]).not.toContain("Record<"); + expect(declaration[1]).not.toContain("unknown"); + } + expect(typesSource).not.toContain("Record"); + }); + + it("maps native rejection objects to stable typed errors", () => { + const error = mapNativeMeasurementError({ + code: "transport", + message: "temporarily unavailable", + source: "android", + }); + expect(error).toMatchObject({ code: "transport", source: "android" }); + expect(mapNativeMeasurementError({ code: "future-code", source: "ios" })).toMatchObject({ + code: "unknownNative", + source: "ios", + }); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-diagnostics.test.ts b/libraries/react-native/tests/core/measurement-diagnostics.test.ts new file mode 100644 index 000000000..4cffe92d4 --- /dev/null +++ b/libraries/react-native/tests/core/measurement-diagnostics.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { + encodeDiagnosticAuthorizationPayload, + SecureDiagnosticLogger, + type DiagnosticAuthorization, + type RedactedDiagnosticEntry, +} from "../../src/core/measurement/diagnostics"; + +const base64 = (bytes: ArrayBuffer): string => + btoa(String.fromCharCode(...new Uint8Array(bytes))); + +describe("secure measurement diagnostics", () => { + it("forces release logging off until a valid signed session is active and always redacts", async () => { + const key = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + const entries: RedactedDiagnosticEntry[] = []; + let now = new Date("2026-01-01T00:00:00.000Z"); + const logger = new SecureDiagnosticLogger( + "project-1", + new Map([["support-1", key.publicKey]]), + true, + (entry) => entries.push(entry), + () => now, + ); + expect(logger.log("debug", "hidden", { safe: true })).toBe(false); + const unsigned = { + expiresAt: "2026-01-01T01:00:00.000Z", + keyId: "support-1", + projectId: "project-1", + sessionId: "diagnostic-1", + }; + const signature = await crypto.subtle.sign( + "Ed25519", + key.privateKey, + encodeDiagnosticAuthorizationPayload(unsigned), + ); + const authorization: DiagnosticAuthorization = { ...unsigned, signature: base64(signature) }; + await expect(logger.authorize({ ...authorization, projectId: "wrong" })).resolves.toBe(false); + await expect(logger.authorize(authorization)).resolves.toBe(true); + expect(logger.log("debug", "request to https://private.example/path", { + nested: { email: "person@example.com", safe: "retained" }, + pushToken: "private-token", + })).toBe(true); + expect(entries).toEqual([expect.objectContaining({ + fields: { nested: { email: "[redacted]", safe: "retained" }, pushToken: "[redacted]" }, + message: "request to [redacted]", + })]); + now = new Date("2026-01-01T01:00:00.000Z"); + expect(logger.log("debug", "expired")).toBe(false); + }); + + it("allows debug-build logging but still redacts protected material", () => { + const entries: RedactedDiagnosticEntry[] = []; + const logger = new SecureDiagnosticLogger("project-1", new Map(), false, (entry) => entries.push(entry)); + expect(logger.log("info", "token=secret", { url: "https://private.example" })).toBe(true); + expect(entries[0]).toMatchObject({ fields: { url: "[redacted]" }, message: "[redacted]" }); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-docs-coverage.test.ts b/libraries/react-native/tests/core/measurement-docs-coverage.test.ts new file mode 100644 index 000000000..b01ab767d --- /dev/null +++ b/libraries/react-native/tests/core/measurement-docs-coverage.test.ts @@ -0,0 +1,32 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { MEASUREMENT_RECORD_TYPES, STANDARD_EVENTS } from "../../src/core/measurement"; + +const docsRoot = resolve(process.cwd(), "../../docs/react-native-measurement"); + +describe("measurement documentation coverage", () => { + it("documents every unified public method", () => { + const reference = readFileSync(resolve(docsRoot, "api-reference.md"), "utf8"); + const methods = [ + "capture", "identify", "purchase", "restorePurchases", "flush", + "configure", "start", "stop", "handle", "on", "getState", "createSupportBundle", + "getInstallationId", "createInviteLink", "trackInviteShare", "trackCrossPromotion", + "trackAdRevenue", "validatePurchase", "deleteData", "setTestDevice", + "getPermissionStatus", "requestPermission", "register", "unregister", "getRegistration", + "setBadgeCount", "set", "get", + ]; + for (const method of methods) expect(reference, method).toContain(`\`${method}`); + }); + + it("documents every canonical record and standard event from source constants", () => { + const dictionary = readFileSync(resolve(docsRoot, "data-dictionary.md"), "utf8"); + for (const recordType of Object.values(MEASUREMENT_RECORD_TYPES)) { + expect(dictionary, recordType).toContain(`\`${recordType}\``); + } + for (const eventName of Object.values(STANDARD_EVENTS)) { + expect(dictionary, eventName).toContain(`\`${eventName}\``); + } + }); +}); diff --git a/libraries/react-native/tests/core/measurement-endpoints.test.ts b/libraries/react-native/tests/core/measurement-endpoints.test.ts new file mode 100644 index 000000000..68b63573e --- /dev/null +++ b/libraries/react-native/tests/core/measurement-endpoints.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; + +import { MeasurementConfigurationError, resolveMeasurementEndpoints } from "../../src/core/measurement"; + +describe("measurement endpoint overrides", () => { + it("uses cloud origins when no override is supplied", () => { + expect(resolveMeasurementEndpoints(undefined, false)).toEqual({ + api: "https://api.voidhash.com", + ingest: "https://api.voidhash.com", + links: "https://api.voidhash.com", + trustedConfigKeyIds: [], + }); + }); + + it("accepts partial self-host overrides and exposes key IDs without key material", () => { + const resolved = resolveMeasurementEndpoints( + { + ingest: "https://ingest.example.com", + configurationProjectId: "project-1", + trustedConfigKeys: [{ keyId: "rotation-2", publicKey: "secret-key-material" }], + }, + false, + ); + expect(resolved).toMatchObject({ + api: "https://api.voidhash.com", + ingest: "https://ingest.example.com", + links: "https://api.voidhash.com", + trustedConfigKeyIds: ["rotation-2"], + }); + expect(JSON.stringify(resolved)).not.toContain("secret-key-material"); + }); + + it.each([ + "not-a-url", + "ftp://example.com", + "https://user:password@example.com", + "https://example.com/path", + "https://example.com?query=yes", + ])("rejects unsafe endpoint %s", (api) => { + expect(() => resolveMeasurementEndpoints({ api }, false)).toThrow( + MeasurementConfigurationError, + ); + }); + + it("allows HTTP only under an explicit debug transport policy", () => { + expect(() => + resolveMeasurementEndpoints({ api: "http://localhost:8787" }, true), + ).toThrow(MeasurementConfigurationError); + expect( + resolveMeasurementEndpoints( + { allowInsecureDebugTransport: true, api: "http://localhost:8787" }, + true, + ).api, + ).toBe("http://localhost:8787"); + }); + + it("rejects duplicate trusted key IDs", () => { + expect(() => + resolveMeasurementEndpoints( + { + configurationProjectId: "project-1", + trustedConfigKeys: [ + { keyId: "same", publicKey: "a" }, + { keyId: "same", publicKey: "b" }, + ], + }, + false, + ), + ).toThrow(MeasurementConfigurationError); + }); + + it("requires a project binding with trusted configuration keys", () => { + expect(() => + resolveMeasurementEndpoints( + { trustedConfigKeys: [{ keyId: "key-1", publicKey: "spki" }] }, + false, + ), + ).toThrow(MeasurementConfigurationError); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-hardening.test.ts b/libraries/react-native/tests/core/measurement-hardening.test.ts new file mode 100644 index 000000000..a84d3cd13 --- /dev/null +++ b/libraries/react-native/tests/core/measurement-hardening.test.ts @@ -0,0 +1,69 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { UnifiedMeasurementRuntime } from "../../src/core/measurement"; + +const repositoryRoot = resolve(process.cwd(), "../.."); + +interface ParityTestMap { + readonly groups: ReadonlyArray<{ + readonly rows: ReadonlyArray; + readonly testCase: string; + readonly testFile: string; + }>; + readonly schemaVersion: number; + readonly wontDo: ReadonlyArray; +} + +describe("measurement hardening matrix", () => { + it("keeps the outbox bounded while retaining critical installation evidence", async () => { + let id = 0; + const runtime = new UnifiedMeasurementRuntime({ + adapter: { makeId: (prefix) => `${prefix}_${++id}` }, + baseUrl: "https://api.voidhash.test", + platform: "ios", + publishableKey: "pk_test", + }); + await runtime.initialize(); + await runtime.consent.set({ + dataUsage: true, + decidedAt: "2026-01-01T00:00:00.000Z", + revision: 1, + source: "application", + }); + for (let index = 0; index < 10_025; index += 1) runtime.capture("load fixture", { index }); + const records = runtime.inspectOutbox(); + expect(records).toHaveLength(10_000); + expect(records.some(({ type }) => type === "installation.created.v1")).toBe(true); + expect(records.some(({ type }) => type === "consent.changed.v1")).toBe(true); + expect(records.filter(({ type }) => type === "analytics.capture.v1")).toHaveLength(9_998); + }); + + it("keeps an executable source reference for every planned parity row", () => { + const manifest = JSON.parse(readFileSync( + resolve(repositoryRoot, "docs/react-native-measurement/parity-test-map.json"), + "utf8", + )) as ParityTestMap; + const rows = manifest.groups.flatMap(({ rows: groupRows }) => groupRows); + expect(new Set(rows).size).toBe(rows.length); + expect(rows).toHaveLength(82); + expect([...manifest.wontDo].sort()).toEqual( + ["CONS-03", "PRIV-03", "PRIV-09", "PRIV-10", "PUR-02", "PUR-03"], + ); + for (const group of manifest.groups) { + const source = readFileSync(resolve(repositoryRoot, group.testFile), "utf8"); + expect(source, `${group.testFile} must contain ${group.testCase}`).toContain(group.testCase); + } + }); + + it("enumerates all fourteen release scenarios without warning-only placeholders", () => { + const manifest = JSON.parse(readFileSync( + resolve(repositoryRoot, "docs/react-native-measurement/release-scenarios.json"), + "utf8", + )) as { readonly scenarios: ReadonlyArray<{ readonly automatedCase: string; readonly id: number }> }; + expect(manifest.scenarios.map(({ id }) => id)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1)); + expect(manifest.scenarios.every(({ automatedCase }) => automatedCase.length > 0)).toBe(true); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-policy.test.ts b/libraries/react-native/tests/core/measurement-policy.test.ts new file mode 100644 index 000000000..fa13bdcfa --- /dev/null +++ b/libraries/react-native/tests/core/measurement-policy.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + evaluateMeasurementCollection, + fetchSignedMeasurementConfiguration, + filterPartnerPayload, + SignedMeasurementConfigurationVerifier, + type CollectionPolicy, + type ConsentSnapshot, +} from "../../src/core/measurement"; + +const policy: CollectionPolicy = { + advertisingIdentifiers: "consent-dependent", + analytics: "enabled", + attribution: "enabled", + location: "manual-only", + networkMetadata: "denied", + upload: "enabled", + vendorIdentifiers: "consent-dependent", +}; + +const consent = (overrides: Partial = {}): ConsentSnapshot => ({ + decidedAt: "2026-07-20T00:00:00.000Z", + revision: 4, + source: "application", + ...overrides, +}); + +describe("measurement policy", () => { + it.each([ + ["analytics", {}, true, "allowed"], + ["advertisingIdentifier", {}, false, "consent-denied"], + ["advertisingIdentifier", { adStorage: true }, true, "allowed"], + ["vendorIdentifier", { dataUsage: false }, false, "consent-denied"], + ["vendorIdentifier", { dataUsage: true }, true, "allowed"], + ["networkMetadata", {}, false, "disabled"], + ["location", {}, false, "manual-only"], + ] as const)("evaluates %s independently", (category, overrides, allowed, reason) => { + expect(evaluateMeasurementCollection(category, policy, consent(overrides))).toMatchObject({ allowed, reason }); + }); + + it("gives collection opt-out precedence over all individual grants", () => { + expect(evaluateMeasurementCollection("analytics", policy, consent({ collectionOptOut: true }))).toMatchObject({ + allowed: false, + reason: "collection-opt-out", + }); + }); + + it("filters excluded partner fields at send time without mutating evidence", () => { + const payload = { campaign: "summer", emailHash: "abc", revenue: 10 }; + const decision = filterPartnerPayload("partner-a", payload, { + excludedFields: { "partner-a": ["emailHash"] }, + mode: "enabled", + }, consent()); + expect(decision).toEqual({ + allowed: true, + consentRevision: 4, + payload: { campaign: "summer", revenue: 10 }, + reason: "allowed", + }); + expect(payload).toEqual({ campaign: "summer", emailHash: "abc", revenue: 10 }); + }); + + it.each([ + [{ mode: "disabled" as const }, {}, "partner-sharing-disabled"], + [{ mode: "enabled" as const, excludedPartners: ["partner-a"] }, {}, "partner-excluded"], + [{ mode: "enabled" as const }, { partnerSharingOptOut: true }, "consent-denied"], + ])("denies partner sharing with an observable reason", (sharing, consentOverrides, reason) => { + expect(filterPartnerPayload("partner-a", { campaign: "summer" }, sharing, consent(consentOverrides))).toMatchObject({ + allowed: false, + reason, + }); + }); +}); + +describe("SignedMeasurementConfigurationVerifier", () => { + const configuration = { + expiresAt: "2026-08-01T00:00:00.000Z", + keyId: "key-1", + payload: { rules: [{ conversion: 2, name: "trial" }] }, + projectId: "project-1", + signature: "signed", + version: 2, + } as const; + + it("verifies canonical bytes and retains the last valid version", async () => { + const key = vi.fn((_bytes: Uint8Array, _signature: string) => true); + const verifier = new SignedMeasurementConfigurationVerifier( + "project-1", + new Map([["key-1", key]]), + () => new Date("2026-07-20T00:00:00.000Z"), + 1, + ); + await expect(verifier.verify(configuration)).resolves.toEqual(configuration.payload); + expect(new TextDecoder().decode(key.mock.calls[0]?.[0])).toBe( + '{"expiresAt":"2026-08-01T00:00:00.000Z","keyId":"key-1","payload":{"rules":[{"conversion":2,"name":"trial"}]},"projectId":"project-1","version":2}', + ); + expect(verifier.getState()).toEqual({ keyId: "key-1", payload: configuration.payload, version: 2 }); + }); + + it.each([ + [{ ...configuration, expiresAt: "2026-01-01T00:00:00.000Z" }, "expired"], + [{ ...configuration, keyId: "unknown" }, "unknown-key"], + [{ ...configuration, projectId: "project-2" }, "project-mismatch"], + [{ ...configuration, version: 1 }, "version-replay"], + ] as const)("rejects invalid signed configuration as %s", async (candidate, code) => { + const verifier = new SignedMeasurementConfigurationVerifier( + "project-1", + new Map([["key-1", () => true]]), + () => new Date("2026-07-20T00:00:00.000Z"), + 1, + ); + await expect(verifier.verify(candidate)).rejects.toMatchObject({ code }); + }); + + it("does not replace the accepted configuration after a signature failure", async () => { + const verifier = new SignedMeasurementConfigurationVerifier( + "project-1", + new Map([["key-1", (_bytes, signature) => signature === "signed"]]), + () => new Date("2026-07-20T00:00:00.000Z"), + ); + await verifier.verify(configuration); + await expect(verifier.verify({ ...configuration, signature: "bad", version: 3 })).rejects.toMatchObject({ + code: "invalid-signature", + }); + expect(verifier.getState().version).toBe(2); + }); + + it("fetches the config endpoint and verifies an Ed25519 response end to end", async () => { + const keys = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + const publicKey = await crypto.subtle.exportKey("spki", keys.publicKey); + const encode = (value: ArrayBuffer) => + btoa(String.fromCharCode(...new Uint8Array(value))); + const unsigned = { + expiresAt: "2099-08-01T00:00:00.000Z", + keyId: "key-1", + payload: { schemaVersion: 1 }, + projectId: "project-1", + version: 2, + }; + const canonical = + '{"expiresAt":"2099-08-01T00:00:00.000Z","keyId":"key-1","payload":{"schemaVersion":1},"projectId":"project-1","version":2}'; + const signature = encode( + await crypto.subtle.sign("Ed25519", keys.privateKey, new TextEncoder().encode(canonical)), + ); + const fetch = vi.fn(async () => + new Response(JSON.stringify({ ...unsigned, signature }), { + headers: { "content-type": "application/json" }, + status: 200, + }), + ); + + await expect( + fetchSignedMeasurementConfiguration<{ readonly schemaVersion: number }>({ + endpoint: "https://ingest.example", + expectedProjectId: "project-1", + fetch, + persistedVersion: 1, + publishableKey: "vh_pk_test", + trustedKeys: [{ keyId: "key-1", publicKeySpki: encode(publicKey) }], + }), + ).resolves.toEqual({ keyId: "key-1", payload: { schemaVersion: 1 }, version: 2 }); + expect(fetch).toHaveBeenCalledWith( + "https://ingest.example/i/v1/measurement/config", + expect.objectContaining({ headers: { "x-publishable-key": "vh_pk_test" } }), + ); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-runtime.test.ts b/libraries/react-native/tests/core/measurement-runtime.test.ts new file mode 100644 index 000000000..55a9dcf78 --- /dev/null +++ b/libraries/react-native/tests/core/measurement-runtime.test.ts @@ -0,0 +1,1074 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + MeasurementCapabilityUnavailable, + MeasurementConfigurationError, + MeasurementInputError, + MeasurementPolicyBlocked, + STANDARD_EVENTS, + UnifiedMeasurementRuntime, + type MeasurementEnvelopeV1, + type MeasurementRuntimeAdapter, +} from "../../src/core/measurement"; + +const makeHarness = (overrides: { + adapter?: MeasurementRuntimeAdapter; + links?: ConstructorParameters[0]["links"]; + measurement?: ConstructorParameters[0]["measurement"]; + consent?: ConstructorParameters[0]["consent"]; + trustedConfigKeys?: ConstructorParameters[0]["trustedConfigKeys"]; + configurationProjectId?: string; +} = {}) => { + let now = Date.parse("2026-01-01T00:00:00.000Z"); + let id = 0; + const baseAdapter: MeasurementRuntimeAdapter = { + makeId: (prefix) => `${prefix}_${++id}`, + now: () => new Date(now), + monotonicNowMs: () => now, + }; + const runtime = new UnifiedMeasurementRuntime({ + adapter: { ...baseAdapter, ...overrides.adapter }, + appBuild: "100", + appVersion: "1.0.0", + baseUrl: "https://api.voidhash.test", + bundleId: "com.voidhash.test", + consent: overrides.consent, + links: overrides.links, + measurement: overrides.measurement, + platform: "ios", + publishableKey: "pk_test", + trustedConfigKeys: overrides.trustedConfigKeys, + configurationProjectId: overrides.configurationProjectId, + }); + return { + advance: (milliseconds: number) => { + now += milliseconds; + }, + runtime, + }; +}; + +const byType = ( + records: ReadonlyArray>, + type: string, +) => records.filter((record) => record.type === type); + +describe("UnifiedMeasurementRuntime", () => { + it("hydrates native installation state and forwards stable envelope IDs to the durable bridge", async () => { + const commands: Array<{ commandId: string; recordType: string }> = []; + const { runtime } = makeHarness({ + adapter: { + initializeMeasurement: async () => ({ + installationId: "install_native", + firstOpenedAt: "2025-01-01T00:00:00.000Z", + installationSequence: 40, + }), + enqueueMeasurement: async (command) => { + commands.push({ commandId: command.commandId, recordType: command.recordType }); + }, + }, + }); + + await runtime.initialize(); + await Promise.resolve(); + + const records = runtime.inspectOutbox(); + expect(await runtime.measurement.getInstallationId()).toBe("install_native"); + expect((await runtime.measurement.getState()).installation.firstOpenedAt).toBe( + "2025-01-01T00:00:00.000Z", + ); + expect(records.map((record) => record.installationSequence)).toEqual([41, 42]); + expect(commands).toEqual( + records.map((record) => ({ commandId: record.recordId, recordType: record.type })), + ); + }); + + it("creates install evidence first and starts only one automatic session", async () => { + const { runtime } = makeHarness(); + const sessions: string[] = []; + runtime.measurement.on("session", (session) => sessions.push(session.id)); + + await runtime.initialize(); + const second = await runtime.measurement.start(); + + const records = runtime.inspectOutbox(); + expect(records.map((record) => record.type).slice(0, 2)).toEqual([ + "installation.created.v1", + "session.started.v1", + ]); + expect(sessions).toHaveLength(1); + expect(second.id).toBe(sessions[0]); + expect(records.map((record) => record.installationSequence)).toEqual([1, 2]); + }); + + it("persists signed configuration, reapplies storage limits, and rejects downgrade after restart", async () => { + const keys = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + const publicKey = await crypto.subtle.exportKey("spki", keys.publicKey); + const encode = (value: ArrayBuffer) => btoa(String.fromCharCode(...new Uint8Array(value))); + const payload = { + collectors: { appleAttributionEnabled: true, linkAllowedDomains: [] }, + conversionRules: [], + schemaVersion: 1 as const, + storage: { + maxOutboxBytes: 1_000_000, + maxOutboxRecords: 500, + maxProtectedBytes: 2_000_000, + }, + }; + const canonicalize = (value: unknown): string => { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + return `{${Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nested]) => `${JSON.stringify(key)}:${canonicalize(nested)}`) + .join(",")}}`; + }; + const signedResponse = async (version: number) => { + const unsigned = { + expiresAt: "2099-08-01T00:00:00.000Z", + keyId: "key-1", + payload, + projectId: "project-1", + version, + }; + const signature = encode( + await crypto.subtle.sign( + "Ed25519", + keys.privateKey, + new TextEncoder().encode(canonicalize(unsigned)), + ), + ); + return { ...unsigned, signature }; + }; + let persisted: { version: number; payload: Uint8Array } | undefined; + const applied = vi.fn(); + const initialFetch = vi.fn(async () => + new Response(JSON.stringify(await signedResponse(2)), { status: 200 }), + ); + const adapter: MeasurementRuntimeAdapter = { + applyMeasurementStorageLimits: applied, + fetch: initialFetch, + getMeasurementConfigurationState: async () => persisted ?? { version: 0 }, + persistMeasurementConfigurationState: async (version, storedPayload) => { + if (version <= (persisted?.version ?? 0)) return false; + persisted = { payload: storedPayload, version }; + return true; + }, + }; + const trustedConfigKeys = [{ keyId: "key-1", publicKeySpki: encode(publicKey) }]; + const first = makeHarness({ + adapter, + configurationProjectId: "project-1", + trustedConfigKeys, + }).runtime; + await first.initialize(); + expect((await first.measurement.getState()).configuration.signed).toEqual({ + keyId: "key-1", + source: "network", + version: 2, + }); + expect(persisted?.version).toBe(2); + + const restarted = makeHarness({ + adapter: { + ...adapter, + fetch: vi.fn(async () => + new Response(JSON.stringify(await signedResponse(1)), { status: 200 }), + ), + }, + configurationProjectId: "project-1", + trustedConfigKeys, + }).runtime; + await restarted.initialize(); + const restartedState = await restarted.measurement.getState(); + expect(restartedState.configuration.signed).toEqual({ + keyId: "key-1", + source: "persisted", + version: 2, + }); + expect(restartedState.configuration.lastSignedConfigurationRejection).toBe("version-replay"); + expect(applied).toHaveBeenLastCalledWith(payload.storage); + }); + + it("preserves consent tri-state values and snapshots revisions at capture time", async () => { + const { runtime } = makeHarness({ measurement: { startMode: "manual" } }); + await runtime.initialize(); + await runtime.consent.set({ + decidedAt: "2026-01-01T00:00:01.000Z", + revision: 1, + source: "application", + dataUsage: true, + }); + runtime.capture("first"); + await runtime.consent.set({ + decidedAt: "2026-01-01T00:00:02.000Z", + revision: 2, + source: "application", + dataUsage: false, + }); + runtime.capture("second"); + + const analytics = byType(runtime.inspectOutbox(), "analytics.capture.v1"); + expect(analytics.map((record) => record.consent.revision)).toEqual([1, 2]); + const consent = await runtime.consent.get(); + expect(consent.snapshot.dataUsage).toBe(false); + expect("gdprApplies" in consent.snapshot).toBe(false); + }); + + it("captures identity and configuration per item instead of reading globals at flush", async () => { + const { runtime } = makeHarness({ measurement: { startMode: "manual" } }); + await runtime.initialize(); + await runtime.measurement.configure({ context: { cohort: "a" }, defaultCurrency: "usd" }); + runtime.setIdentity("person-a"); + runtime.capture("first", { value: 1 }); + await runtime.measurement.configure({ context: { cohort: "b" } }); + runtime.setIdentity("person-b"); + runtime.capture("second", { value: 2 }); + + const analytics = byType(runtime.inspectOutbox(), "analytics.capture.v1"); + expect(analytics.map((record) => record.identity.distinctId)).toEqual(["person-a", "person-b"]); + expect(analytics.map((record) => (record.publicPayload as { measurementContext: unknown }).measurementContext)).toEqual([ + { cohort: "a" }, + { cohort: "b" }, + ]); + expect((analytics[0]?.publicPayload as { currency: string }).currency).toBe("USD"); + }); + + it("rejects unknown configuration, invalid currency, and protected context", async () => { + const { runtime } = makeHarness(); + await expect( + runtime.measurement.configure({ unknown: true } as never), + ).rejects.toBeInstanceOf(MeasurementConfigurationError); + await expect(runtime.measurement.configure({ defaultCurrency: "NOT" })).rejects.toBeInstanceOf( + MeasurementInputError, + ); + await expect( + runtime.measurement.configure({ context: { callbackUrl: "https://secret.test" } }), + ).rejects.toBeInstanceOf(MeasurementInputError); + }); + + it("prevents raw URLs, tokens, receipts, identifiers, email, and phone from public properties", () => { + const { runtime } = makeHarness(); + const cases = [ + { value: "https://private.test/path" }, + { pushToken: "secret" }, + { receipt: "secret" }, + { gaid: "secret" }, + { contact: "person@example.test" }, + { phone: "+420123456789" }, + ]; + for (const properties of cases) { + expect(() => runtime.capture("unsafe", properties)).toThrow(MeasurementInputError); + } + }); + + it("normalizes allowlisted links, projects routes, emits once, and dedupes", async () => { + const { runtime } = makeHarness({ + links: { + allowedDomains: ["links.example"], + allowedSchemes: ["https"], + dedupeWindowMs: 10_000, + }, + }); + const received = vi.fn(); + runtime.links.on("deepLink", received); + const url = "https://links.example/open?deep_link_value=checkout&deep_link_sub1=annual&campaign=winter&pid=owned&link_id=l_1"; + + const first = await runtime.links.handle({ source: "universalLink", url }); + const duplicate = await runtime.links.handle({ source: "manual", url }); + + expect(first).toMatchObject({ + campaign: { campaign: "winter", mediaSource: "owned" }, + direct: true, + linkId: "l_1", + route: { subvalues: { 1: "annual" }, value: "checkout" }, + status: "found", + }); + expect(duplicate).toEqual(first); + expect(received).toHaveBeenCalledTimes(1); + expect(byType(runtime.inspectOutbox(), "link.resolved.v1")).toHaveLength(1); + expect(JSON.stringify(runtime.inspectOutbox())).not.toContain(url); + }); + + it("drains native inbox links after initialization without duplicating protected evidence", async () => { + let inboxListener: Parameters>[0] | undefined; + const putProtectedEvidence = vi.fn(async ({ blobId }: { blobId: string }) => blobId); + const { runtime } = makeHarness({ + adapter: { + putProtectedEvidence, + subscribeNativeInbox: (listener) => { + inboxListener = listener; + return () => undefined; + }, + }, + links: { allowedDomains: ["links.example"], allowedSchemes: ["https"] }, + }); + const observed = vi.fn(); + runtime.links.on("deepLink", observed); + await runtime.initialize(); + + await inboxListener?.({ + appState: "cold", + id: "inbox-1", + kind: "link", + protectedEvidenceRef: "protected-native-link", + receivedAt: "2026-01-01T00:00:00.000Z", + source: "universalLink", + value: "https://links.example/open?deep_link_value=native", + }); + + expect(observed).toHaveBeenCalledWith(expect.objectContaining({ + route: { subvalues: {}, value: "native" }, + status: "found", + })); + expect(putProtectedEvidence).not.toHaveBeenCalled(); + expect(byType(runtime.inspectOutbox(), "link.received.v1")[0]?.protectedPayloadRef).toBe( + "protected-native-link", + ); + }); + + it.each([ + ["javascript:alert(1)", "error"], + ["https://evil.example/open?deep_link_value=x", "notFound"], + ["https://links.example/%E0%A4%A", "error"], + ["https://links.example/a/../..?deep_link_value=x", "error"], + ["https://links.example/open?deep_link_value=x&deep_link_value=y", "error"], + ])("rejects unsafe link %s deterministically", async (url, status) => { + const { runtime } = makeHarness({ + links: { allowedDomains: ["links.example"], allowedSchemes: ["https"] }, + }); + expect(await runtime.links.handle({ source: "manual", url })).toMatchObject({ status }); + }); + + it("resolves wrapped links with bounded redirects and domain-scoped headers", async () => { + const fetch = vi + .fn() + .mockResolvedValueOnce( + new Response(undefined, { + headers: { location: "https://links.example/open?deep_link_value=checkout" }, + status: 302, + }), + ) + .mockResolvedValueOnce(new Response(undefined, { status: 200 })); + const headerProvider = vi.fn(async () => ({ authorization: "Bearer secret" })); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["links.example"], + allowedSchemes: ["https"], + resolveWrappedDomains: ["wrap.example"], + wrappedDomainHeaderProvider: headerProvider, + }, + }); + + await expect( + runtime.links.handle({ source: "esp", url: "https://wrap.example/click?opaque=private" }), + ).resolves.toMatchObject({ route: { value: "checkout" }, status: "found" }); + expect(headerProvider).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledTimes(2); + expect((fetch.mock.calls[0]?.[1]?.headers as Headers).get("authorization")).toBe("Bearer secret"); + expect((fetch.mock.calls[1]?.[1]?.headers as Headers).get("authorization")).toBeNull(); + const serialized = JSON.stringify(runtime.inspectOutbox()); + expect(serialized).not.toContain("Bearer secret"); + expect(serialized).not.toContain("opaque=private"); + }); + + it.each([ + { + name: "normalized redirect loop", + responses: [new Response(undefined, { headers: { location: "https://WRAP.example.:443/click?a=1&b=2" }, status: 302 })], + start: "https://wrap.example/click?b=2&a=1", + expectedCode: "transport", + }, + { + name: "insecure redirect", + responses: [new Response(undefined, { headers: { location: "http://links.example/open?deep_link_value=x" }, status: 302 })], + start: "https://wrap.example/click", + expectedCode: "transport", + }, + ])("terminates a wrapped $name without exposing the chain", async ({ responses, start, expectedCode }) => { + const fetch = vi.fn(); + for (const response of responses) fetch.mockResolvedValueOnce(response); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["links.example"], + allowedSchemes: ["https"], + resolveWrappedDomains: ["wrap.example"], + }, + }); + const result = await runtime.links.handle({ source: "esp", url: start }); + expect(result).toMatchObject({ error: { code: expectedCode }, status: "error" }); + expect(JSON.stringify(runtime.inspectOutbox())).not.toContain(start); + }); + + it("times out stalled wrapped-link resolution within the configured bound", async () => { + const fetch = vi.fn((_input, init) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError"))); + }), + ); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["links.example"], + allowedSchemes: ["https"], + resolutionTimeoutMs: 5, + resolveWrappedDomains: ["wrap.example"], + }, + }); + await expect( + runtime.links.handle({ source: "esp", url: "https://wrap.example/stalled" }), + ).resolves.toMatchObject({ error: { code: "timeout" }, status: "error" }); + }); + + it("terminates a wrapped chain at the configured redirect bound", async () => { + const fetch = vi + .fn() + .mockResolvedValueOnce(new Response(undefined, { + headers: { location: "https://wrap.example/two" }, status: 302, + })) + .mockResolvedValueOnce(new Response(undefined, { + headers: { location: "https://wrap.example/three" }, status: 302, + })); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["links.example"], + maxRedirects: 1, + resolveWrappedDomains: ["wrap.example"], + }, + }); + await expect(runtime.links.handle({ source: "esp", url: "https://wrap.example/one" })) + .resolves.toMatchObject({ error: { code: "transport" }, status: "error" }); + expect(fetch).toHaveBeenCalledTimes(2); + const evidence = byType(runtime.inspectOutbox(), "link.redirect_evidence.v1"); + expect(evidence[0]?.publicPayload).toMatchObject({ outcome: "redirectLimit" }); + }); + + it("retries an offline wrapped link and emits only the eventual result", async () => { + const fetch = vi + .fn() + .mockRejectedValueOnce(new TypeError("offline")) + .mockResolvedValueOnce(new Response(undefined, { + headers: { location: "https://links.example/open?deep_link_value=recovered" }, status: 302, + })) + .mockResolvedValueOnce(new Response(undefined, { status: 200 })); + const received = vi.fn(); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["links.example"], + allowedSchemes: ["https"], + resolveWrappedDomains: ["wrap.example"], + wrappedRetryDelayMs: 1, + }, + }); + runtime.links.on("deepLink", received); + await expect(runtime.links.handle({ source: "esp", url: "https://wrap.example/offline" })) + .resolves.toMatchObject({ route: { value: "recovered" }, status: "found" }); + expect(received).toHaveBeenCalledTimes(1); + expect(byType(runtime.inspectOutbox(), "link.resolved.v1")).toHaveLength(1); + }); + + it("resumes within the timeout and rotates after it using monotonic time", async () => { + const { runtime, advance } = makeHarness({ measurement: { sessionTimeoutMs: 1_000 } }); + await runtime.initialize(); + const first = await runtime.measurement.start(); + runtime.background(); + advance(999); + expect((await runtime.foreground())?.id).toBe(first.id); + runtime.background(); + advance(1_001); + expect((await runtime.foreground())?.id).not.toBe(first.id); + expect(byType(runtime.inspectOutbox(), "session.ended.v1")).toHaveLength(1); + }); + + it("validates decimal ad revenue and dedupes by impression ID", async () => { + const { runtime } = makeHarness(); + const valid = { + currency: "eur", + impressionId: "imp-1", + mediationNetwork: "google_admob" as const, + monetizationNetwork: "network", + revenue: "0.12345678", + }; + await runtime.measurement.trackAdRevenue(valid); + await runtime.measurement.trackAdRevenue(valid); + expect(byType(runtime.inspectOutbox(), "revenue.ad_impression.v1")).toHaveLength(1); + await expect(runtime.measurement.trackAdRevenue({ ...valid, revenue: "NaN" })).rejects.toBeInstanceOf( + MeasurementInputError, + ); + await expect(runtime.measurement.trackAdRevenue({ ...valid, impressionId: "imp-2", revenue: "1.123456789" })).rejects.toBeInstanceOf( + MeasurementInputError, + ); + }); + + it("creates signed invite links through the generated contract and captures typed shares", async () => { + const fetch = vi.fn(async (_input, init) => + new Response(JSON.stringify({ + expiresAt: "2026-02-01T00:00:00.000Z", + linkId: "link_server_1", + url: "https://links.example/l/link_server_1?sig=signed", + }), { headers: { "content-type": "application/json" }, status: 201 }), + ); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedCustomParameters: ["coupon"], + templateId: "invite-template", + }, + }); + await runtime.initialize(); + const link = await runtime.measurement.createInviteLink({ + appleAppId: "123456789", + campaign: "winter", + channel: "share", + customParameters: { coupon: "WELCOME" }, + deepLinkSubvalues: { 1: "annual" }, + deepLinkValue: "checkout", + referrerCustomerId: "customer-1", + referrerImageUrl: "https://images.example/avatar.png", + referrerName: "Referrer", + referrerUid: "uid-1", + }); + expect(link.linkId).toBe("link_server_1"); + expect(String(fetch.mock.calls[0]?.[0])).toBe("https://api.voidhash.test/l/v1/links"); + expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toMatchObject({ + campaign: { campaign: "winter", channel: "share" }, + customParameters: { coupon: "WELCOME" }, + destination: { + appleAppId: "123456789", + deepLinkValue: "checkout", + subvalues: { 1: "annual" }, + }, + referrerCustomerId: "customer-1", + referrerImageUrl: "https://images.example/avatar.png", + referrerName: "Referrer", + referrerUid: "uid-1", + templateId: "invite-template", + token: "pk_test", + }); + await runtime.measurement.trackInviteShare({ channel: "messages", linkId: link.linkId }); + const invite = byType(runtime.inspectOutbox(), "analytics.capture.v1").find( + (record) => (record.publicPayload as { eventName?: string }).eventName === STANDARD_EVENTS.INVITE_SHARED, + ); + expect(invite?.publicPayload).toMatchObject({ + properties: { channel: "messages", link_id: "link_server_1" }, + }); + }); + + it("does not open cross-promotion until signed-link creation succeeds", async () => { + const order: string[] = []; + const { runtime } = makeHarness({ + adapter: { + fetch: vi.fn(async () => { + order.push("signed"); + return new Response(JSON.stringify({ + expiresAt: "2026-02-01T00:00:00.000Z", + linkId: "link-cross", + url: "https://links.example/l/link-cross?sig=signed", + }), { status: 201 }); + }), + openUrl: async () => { + order.push("opened"); + return true; + }, + }, + }); + await runtime.initialize(); + await expect(runtime.measurement.trackCrossPromotion({ + action: "openStore", + promotedAppId: "app.example", + })).resolves.toMatchObject({ opened: true, link: { linkId: "link-cross" } }); + expect(order).toEqual(["signed", "opened"]); + }); + + it("applies the manual-only location policy without owning location permissions", async () => { + const denied = makeHarness(); + await expect( + denied.runtime.measurement.handle({ latitude: 1, longitude: 2, type: "location" }), + ).rejects.toBeInstanceOf(MeasurementPolicyBlocked); + + const allowed = makeHarness({ measurement: { collection: { location: "manual-only" } } }); + await expect( + allowed.runtime.measurement.handle({ latitude: 1, longitude: 2, type: "location" }), + ).resolves.toMatchObject({ accepted: true }); + }); + + it("registers push without retaining the platform token and routes first-party opens", async () => { + const { runtime } = makeHarness({ + adapter: { + fetch: vi.fn(async (input, init) => { + expect(String(input)).toBe("https://api.voidhash.test/api/v1/sdk/push-devices/register"); + expect(JSON.parse(String(init?.body))).toMatchObject({ + platformToken: "raw-platform-token", + provider: "apns", + }); + return new Response(JSON.stringify({ pushDeviceTokenId: "push_tok_server_1" }), { + headers: { "content-type": "application/json" }, + status: 200, + }); + }), + getPermissionStatus: async () => "authorized", + getPushToken: async () => ({ + environment: "development", + provider: "apns", + token: "raw-platform-token", + }), + requestPermission: async () => "authorized", + setBadgeCount: async () => undefined, + }, + links: { allowedDomains: ["links.example"], allowedSchemes: ["https"] }, + }); + const opened = vi.fn(); + runtime.notifications.on("opened", opened); + const registration = await runtime.notifications.register(); + const incoming = runtime.internalReceiveNotification({ + pushNotificationSendId: "push_send_1", + rawPayload: { secret: "payload" }, + }); + await runtime.internalOpenNotification( + incoming, + "https://links.example/open?deep_link_value=inbox", + ); + + expect(registration.pushDeviceTokenId).toBe("push_tok_server_1"); + expect(opened).toHaveBeenCalledTimes(1); + const serialized = JSON.stringify(runtime.inspectOutbox()); + expect(serialized).not.toContain("raw-platform-token"); + expect(serialized).not.toContain('"secret":"payload"'); + expect( + byType(runtime.inspectOutbox(), "analytics.capture.v1").some( + (record) => (record.publicPayload as { eventName: string }).eventName === STANDARD_EVENTS.OPENED_FROM_PUSH_NOTIFICATION, + ), + ).toBe(true); + }); + + it("rejects notification capabilities when native hooks are absent", async () => { + const { runtime } = makeHarness(); + await expect(runtime.notifications.requestPermission()).rejects.toBeInstanceOf( + MeasurementCapabilityUnavailable, + ); + await expect(runtime.notifications.register()).rejects.toBeInstanceOf( + MeasurementCapabilityUnavailable, + ); + await expect(runtime.notifications.setBadgeCount(1)).rejects.toBeInstanceOf( + MeasurementCapabilityUnavailable, + ); + }); + + it("re-links push registration after identity changes and unregisters the opaque ID", async () => { + const requests: Array<{ path: string; body: Record; distinctId: string | null }> = []; + let registration = 0; + const fetch = vi.fn(async (input, init) => { + const path = new URL(String(input)).pathname; + requests.push({ + body: JSON.parse(String(init?.body)) as Record, + distinctId: new Headers(init?.headers).get("x-distinct-id"), + path, + }); + if (path.endsWith("/register")) { + registration += 1; + return new Response(JSON.stringify({ pushDeviceTokenId: `push_tok_${registration}` }), { + headers: { "content-type": "application/json" }, status: 200, + }); + } + return new Response(undefined, { status: 204 }); + }); + const { runtime } = makeHarness({ + adapter: { + fetch, + getPushToken: async () => ({ + environment: "production", provider: "fcm", token: "raw-fcm-token", + }), + }, + }); + expect((await runtime.notifications.register()).pushDeviceTokenId).toBe("push_tok_1"); + runtime.setIdentity("person-new"); + await vi.waitFor(() => expect(requests).toHaveLength(2)); + expect(requests[1]).toMatchObject({ + body: { previousPushDeviceTokenId: "push_tok_1" }, + distinctId: "person-new", + path: "/api/v1/sdk/push-devices/register", + }); + expect((await runtime.notifications.getRegistration())?.pushDeviceTokenId).toBe("push_tok_2"); + await runtime.notifications.unregister(); + expect(requests[2]).toMatchObject({ + body: { pushDeviceTokenId: "push_tok_2" }, + path: "/api/v1/sdk/push-devices/unregister", + }); + expect(await runtime.notifications.getRegistration()).toBeUndefined(); + expect(JSON.stringify(runtime.inspectOutbox())).not.toContain("raw-fcm-token"); + }); + + it("hydrates opaque push registration and refreshes it on a native token rotation", async () => { + let nativeListener: Parameters>[0] | undefined; + const persistedRegistration = { + environment: "production" as const, + provider: "fcm" as const, + pushDeviceTokenId: "push_tok_persisted", + registeredAt: "2026-01-01T00:00:00.000Z", + }; + const fetch = vi.fn(async (input, init) => { + expect(String(input)).toContain("/push-devices/refresh"); + expect(JSON.parse(String(init?.body))).toEqual({ + platformToken: "rotated-fcm-token", + pushDeviceTokenId: "push_tok_persisted", + }); + return new Response(undefined, { status: 204 }); + }); + const { runtime } = makeHarness({ + adapter: { + fetch, + getPushRegistrationState: async () => new TextEncoder().encode(JSON.stringify(persistedRegistration)), + getPushToken: async () => ({ + environment: "production", provider: "fcm", token: "rotated-fcm-token", + }), + persistPushRegistrationState: async () => true, + subscribeNotificationEvents: (listener) => { + nativeListener = listener; + return () => undefined; + }, + }, + }); + await runtime.initialize(); + expect(await runtime.notifications.getRegistration()).toEqual(persistedRegistration); + nativeListener?.({ + id: "notification-token", + kind: "tokenChanged", + occurredAt: "2026-01-01T00:00:01.000Z", + }); + await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(1)); + expect(byType(runtime.inspectOutbox(), "push.token.v1").at(-1)?.publicPayload).toMatchObject({ + pushDeviceTokenId: "push_tok_persisted", + reason: "tokenRotated", + }); + expect(JSON.stringify(runtime.inspectOutbox())).not.toContain("rotated-fcm-token"); + }); + + it("emits unsubscribe-safe delivery diagnostics and reports redacted state", async () => { + const { runtime } = makeHarness({ measurement: { startMode: "manual" } }); + const diagnostics = vi.fn(); + const unsubscribe = runtime.measurement.on("delivery", diagnostics); + await runtime.initialize(); + runtime.capture("safe", { ordinary: "value" }); + const before = await runtime.measurement.getState(); + expect(before.collectors).toEqual( + expect.objectContaining({ links: "notConfigured", push: "notConfigured", purchases: "notConfigured" }), + ); + await runtime.flush(); + unsubscribe(); + runtime.capture("second"); + await runtime.flush(); + expect(diagnostics).toHaveBeenCalledTimes(before.outbox.total); + expect(JSON.stringify(await runtime.measurement.getState())).not.toContain("ordinary"); + }); + + it("persists test-device mode across a cold runtime", async () => { + let persisted = false; + const adapter: MeasurementRuntimeAdapter = { + getTestDeviceState: async () => persisted, + persistTestDeviceState: async (enabled) => { + persisted = enabled; + return true; + }, + }; + const { runtime } = makeHarness({ adapter }); + await runtime.initialize(); + await runtime.measurement.setTestDevice(true); + expect((await runtime.measurement.getState()).testDevice).toBe(true); + const { runtime: coldRuntime } = makeHarness({ adapter }); + await coldRuntime.initialize(); + expect((await coldRuntime.measurement.getState()).testDevice).toBe(true); + }); + + it("distinguishes upload pause from deletion and keeps the installation identity", async () => { + const { runtime } = makeHarness(); + await runtime.initialize(); + const installationId = await runtime.measurement.getInstallationId(); + await runtime.measurement.stop({ upload: true }); + runtime.capture("retained"); + expect(await runtime.flush()).toMatchObject({ accepted: 0, policyBlocked: expect.any(Number) }); + expect((await runtime.measurement.getState()).deletion.completed).toBe(false); + + await runtime.measurement.deleteData(); + expect((await runtime.measurement.getState()).deletion.completed).toBe(true); + expect(await runtime.measurement.getInstallationId()).toBe(installationId); + }); + + it("durably enqueues deletion before atomically purging protected data", async () => { + let deletionPersisted = false; + const calls: string[] = []; + const { runtime } = makeHarness({ + adapter: { + deleteProtectedData: async (requestId) => { + expect(deletionPersisted).toBe(true); + calls.push(`purge:${requestId}`); + return true; + }, + enqueueMeasurement: async (command) => { + if (command.recordType === "measurement.deletion_requested.v1") { + deletionPersisted = true; + calls.push(`enqueue:${command.commandId}`); + } + }, + waitForPendingWrites: async () => { + expect(deletionPersisted).toBe(true); + calls.push("barrier"); + }, + }, + }); + await runtime.initialize(); + const result = await runtime.measurement.deleteData(); + expect(calls.map((call) => call.split(":")[0])).toEqual(["enqueue", "barrier", "purge"]); + expect(result.status).toBe("accepted"); + }); + + it("rejects legacy purchase validation fields at runtime and correlates each result", async () => { + const { runtime } = makeHarness(); + await expect( + runtime.measurement.validatePurchase({ + androidPublicKey: "forbidden", + platform: "android", + protectedEvidenceId: "protected_1", + transactionId: "tx-1", + } as never), + ).rejects.toBeInstanceOf(MeasurementInputError); + + const [first, second] = await Promise.all([ + runtime.measurement.validatePurchase({ platform: "ios", protectedEvidenceId: "protected_1", transactionId: "tx-1" }), + runtime.measurement.validatePurchase({ platform: "android", protectedEvidenceId: "protected_2", transactionId: "tx-2" }), + ]); + expect(first.requestId).not.toBe(second.requestId); + expect([first.transactionId, second.transactionId]).toEqual(["tx-1", "tx-2"]); + const { runtime: releaseRuntime } = makeHarness({ adapter: { isReleaseBuild: true } }); + await expect(releaseRuntime.measurement.validatePurchase({ + environment: "sandbox", + platform: "ios", + protectedEvidenceId: "protected_3", + transactionId: "tx-3", + })).rejects.toBeInstanceOf(MeasurementConfigurationError); + }); + + it("records purchase observation once with receipt material only in protected evidence", async () => { + const protectedWrites: Array<{ purpose: string; value: string }> = []; + const dedupe = new Set(); + const enrichment = { source: "first" }; + const { runtime } = makeHarness({ + measurement: { purchases: { enabled: true, enrichment: { android: { inApp: () => enrichment } } } }, + adapter: { + hasDedupe: async (namespace, key) => dedupe.has(`${namespace}:${key}`), + checkAndSetDedupe: async (namespace, key) => { + const value = `${namespace}:${key}`; + if (dedupe.has(value)) return false; + dedupe.add(value); + return true; + }, + putProtectedEvidence: async (input) => { + protectedWrites.push({ purpose: input.purpose, value: new TextDecoder().decode(input.value) }); + return input.blobId; + }, + }, + }); + const transaction = { + appAccountToken: "account-secret", + isAcknowledged: false, + platform: "android" as const, + productId: "annual", + purchaseDate: 123, + purchaseState: "purchased" as const, + purchaseToken: "purchase-secret", + quantity: 1, + receipt: "receipt-secret", + transactionId: "tx-1", + }; + await runtime.recordObservedPurchase(transaction); + enrichment.source = "mutated"; + await runtime.recordObservedPurchase(transaction); + const observed = byType(runtime.inspectOutbox(), "purchase.observed.v1"); + expect(observed).toHaveLength(1); + expect(observed[0]?.publicPayload).toMatchObject({ + enrichment: { source: "first" }, + enrichmentOutcome: "collected", + productId: "annual", + transactionId: "tx-1", + }); + expect(JSON.stringify(observed)).not.toMatch(/purchase-secret|receipt-secret|account-secret/); + expect(protectedWrites).toEqual([{ + purpose: "purchase-receipt", + value: JSON.stringify({ appAccountToken: "account-secret", purchaseToken: "purchase-secret", receipt: "receipt-secret" }), + }]); + }); + + it("keeps store-invalid validation distinct from transport failure and correlates concurrent responses", async () => { + const { runtime } = makeHarness({ + adapter: { + validatePurchase: async (input) => { + if (input.transactionId === "invalid") { + await Promise.resolve(); + return { outcome: "invalid", storeState: { state: "cancelled" } }; + } + throw Object.assign(new Error("offline"), { kind: "network" }); + }, + }, + }); + const [invalid, offline] = await Promise.all([ + runtime.measurement.validatePurchase({ platform: "ios", protectedEvidenceId: "p1", transactionId: "invalid" }), + runtime.measurement.validatePurchase({ platform: "android", protectedEvidenceId: "p2", transactionId: "offline" }), + ]); + expect(invalid).toMatchObject({ outcome: "invalid", transactionId: "invalid", storeState: { state: "cancelled" } }); + expect(invalid.failure).toBeUndefined(); + expect(offline).toMatchObject({ outcome: "indeterminate", transactionId: "offline", failure: { kind: "network" } }); + expect(invalid.requestId).not.toBe(offline.requestId); + }); + + it("classifies configuration, store, and server purchase validation failures", async () => { + const { runtime: unconfigured } = makeHarness(); + await expect(unconfigured.measurement.validatePurchase({ + platform: "ios", + protectedEvidenceId: "p-configuration", + transactionId: "configuration", + })).resolves.toMatchObject({ outcome: "indeterminate", failure: { kind: "configuration" } }); + + const { runtime } = makeHarness({ + adapter: { + validatePurchase: async ({ transactionId }) => { + if (transactionId === "store") throw Object.assign(new Error("store unavailable"), { kind: "store" }); + throw new Error("invalid server response"); + }, + }, + }); + const [store, server] = await Promise.all([ + runtime.measurement.validatePurchase({ platform: "ios", protectedEvidenceId: "p-store", transactionId: "store" }), + runtime.measurement.validatePurchase({ platform: "android", protectedEvidenceId: "p-server", transactionId: "server" }), + ]); + expect(store).toMatchObject({ outcome: "indeterminate", failure: { kind: "store" } }); + expect(server).toMatchObject({ outcome: "indeterminate", failure: { kind: "server" } }); + }); + + it("round-trips rich normalized subscription state and rejects unknown shapes", async () => { + const richState = { + cancellation: { at: "2026-01-01T00:00:00.000Z", reason: "price-change" as const }, + lineItems: [{ productId: "annual", quantity: 1 }], + offer: { id: "intro", type: "introductory" as const }, + pause: { startsAt: "2026-02-01T00:00:00.000Z", resumesAt: "2026-03-01T00:00:00.000Z" }, + prepaid: { expiresAt: "2026-04-01T00:00:00.000Z", topUpEligible: true }, + priceChange: { currency: "USD", price: "19.99", state: "pending" as const }, + productId: "annual", + replacement: { mode: "deferred" as const, replacedProductId: "monthly" }, + state: "paused" as const, + subscriptionState: "billing-retry", + test: true, + }; + const { runtime } = makeHarness({ adapter: { validatePurchase: async () => ({ outcome: "valid", storeState: richState }) } }); + await expect(runtime.measurement.validatePurchase({ + platform: "android", + protectedEvidenceId: "p-rich", + transactionId: "rich", + })).resolves.toMatchObject({ outcome: "valid", storeState: richState }); + + const { runtime: invalid } = makeHarness({ adapter: { validatePurchase: async () => ({ + outcome: "valid", + storeState: { state: "future-state" }, + } as never) } }); + await expect(invalid.measurement.validatePurchase({ + platform: "ios", + protectedEvidenceId: "p-invalid", + transactionId: "invalid-shape", + })).resolves.toMatchObject({ outcome: "indeterminate", failure: { kind: "server" } }); + }); + + it("vaults partner context and exposes only partner IDs and revision publicly", async () => { + const writes: string[] = []; + const { runtime } = makeHarness({ adapter: { + putProtectedEvidence: async (input) => { + writes.push(new TextDecoder().decode(input.value)); + expect(input.purpose).toBe("partner-context"); + return input.blobId; + }, + } }); + await runtime.measurement.configure({ + partnerData: { ads: { account: "partner-secret", callback: "https://partner.example/private" } }, + }); + expect(writes).toEqual([JSON.stringify({ ads: { account: "partner-secret", callback: "https://partner.example/private" } })]); + const record = byType(runtime.inspectOutbox(), "partner.context_changed.v1")[0]; + expect(record?.publicPayload).toMatchObject({ partners: ["ads"] }); + expect(JSON.stringify(runtime.inspectOutbox())).not.toMatch(/partner-secret|partner\.example/); + expect(JSON.stringify(await runtime.measurement.getState())).not.toMatch(/partner-secret|partner\.example/); + }); + + it("applies typed URL rules before wrapped resolution and enforces required pid", async () => { + const fetch = vi.fn(async (..._args: Parameters) => new Response(null, { status: 200 })); + const { runtime } = makeHarness({ + adapter: { fetch }, + links: { + allowedDomains: ["wrapped.example"], + parameterRules: [{ + id: "owned-media", + match: { domains: ["wrapped.example"] }, + parameters: { deep_link_value: "checkout" }, + reengagement: true, + requiredPid: "flag", + }], + resolveWrappedDomains: ["wrapped.example"], + }, + }); + const result = await runtime.links.handle({ source: "manual", url: "https://wrapped.example/open" }); + expect(result).toMatchObject({ status: "found", route: { value: "checkout" } }); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("deep_link_value=checkout"), + expect.objectContaining({ redirect: "manual" }), + ); + expect(fetch.mock.calls[0]?.[0]).toContain("is_retargeting=true"); + expect(byType(runtime.inspectOutbox(), "link.resolved.v1")[0]?.publicPayload).toMatchObject({ + ruleApplications: [{ appended: ["deep_link_value", "is_retargeting"], id: "owned-media", missingPid: true }], + }); + + const { runtime: rejecting } = makeHarness({ + links: { + allowedDomains: ["links.example"], + parameterRules: [{ id: "pid", match: { domains: ["links.example"] }, requiredPid: "reject" }], + }, + }); + await expect(rejecting.links.handle({ source: "manual", url: "https://links.example/open" })).resolves.toMatchObject({ + reason: "requiredPidMissing", + status: "notFound", + }); + }); + + it("never reads or exposes a manually supplied identifier unless policy permits it", async () => { + const writes: string[] = []; + const { runtime: denied } = makeHarness(); + await expect(denied.measurement.handle({ type: "identifier", kind: "oaid", value: "oaid-secret" })) + .rejects.toBeInstanceOf(MeasurementPolicyBlocked); + + const { runtime } = makeHarness({ + consent: { revision: 1, decidedAt: "2026-01-01T00:00:00.000Z", source: "application", adStorage: true }, + adapter: { putProtectedEvidence: async (input) => { + writes.push(new TextDecoder().decode(input.value)); + return input.blobId; + } }, + }); + await runtime.measurement.handle({ type: "identifier", kind: "oaid", value: "oaid-secret" }); + expect(writes).toEqual(["oaid-secret"]); + expect(JSON.stringify(byType(runtime.inspectOutbox(), "identifier.observed.v1"))).not.toContain("oaid-secret"); + }); + + it("deduplicates application and system observations of the same ATT transition", async () => { + const { runtime } = makeHarness(); + const application = await runtime.measurement.handle({ + source: "application", + status: "authorized", + type: "attStatus", + }); + const system = await runtime.measurement.handle({ + source: "system", + status: "authorized", + type: "attStatus", + }); + + expect(system.recordId).toBe(application.recordId); + expect(byType(runtime.inspectOutbox(), "ios.att.changed.v1")).toHaveLength(1); + }); +}); diff --git a/libraries/react-native/tests/core/measurement-support-bundle.test.ts b/libraries/react-native/tests/core/measurement-support-bundle.test.ts new file mode 100644 index 000000000..2421d5497 --- /dev/null +++ b/libraries/react-native/tests/core/measurement-support-bundle.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { + assertSafePublicValue, + buildMeasurementSupportBundle, + UnifiedMeasurementRuntime, +} from "../../src/core/measurement"; + +describe("measurement support bundle", () => { + it("hashes state identifiers, omits endpoints, and retains support categories", async () => { + const runtime = new UnifiedMeasurementRuntime({ + adapter: { makeId: (prefix) => `${prefix}_private`, now: () => new Date("2026-01-01T00:00:00.000Z") }, + baseUrl: "https://private.example", + platform: "ios", + publishableKey: "pk_secret", + }); + await runtime.initialize(); + const state = await runtime.measurement.getState(); + const bundle = buildMeasurementSupportBundle(state, new Date("2026-01-02T00:00:00.000Z")); + const serialized = JSON.stringify(bundle); + expect(serialized).not.toContain(state.installation.id); + expect(serialized).not.toContain("private.example"); + expect(serialized).not.toContain("pk_secret"); + expect(bundle).toMatchObject({ + generatedAt: "2026-01-02T00:00:00.000Z", + collectors: expect.any(Object), + configuration: expect.any(Object), + consent: expect.any(Object), + outbox: expect.any(Object), + versions: expect.any(Object), + }); + }); + + it.each([ + { receipt: "secret" }, + { nested: { pushToken: "secret" } }, + { ordinary: "https://private.example/path" }, + { email: "person@example.com" }, + ])("rejects protected diagnostic injection %#", (value) => { + expect(() => assertSafePublicValue(value, "supportBundle")).toThrow("Protected"); + }); +}); diff --git a/libraries/react-native/tests/core/protected-identity.test.ts b/libraries/react-native/tests/core/protected-identity.test.ts new file mode 100644 index 000000000..b2e79db8c --- /dev/null +++ b/libraries/react-native/tests/core/protected-identity.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; + +import { + hashProtectedIdentityValue, + normalizeProtectedEmail, + normalizeProtectedIdentityTraits, + normalizeProtectedPhone, + UnifiedMeasurementRuntime, + type MeasurementRuntimeAdapter, +} from "../../src/core/measurement"; + +const runtime = (adapter: MeasurementRuntimeAdapter = {}, dataUsage: boolean | undefined = true) => + new UnifiedMeasurementRuntime({ + adapter, + baseUrl: "https://api.voidhash.com", + consent: { + dataUsage, + decidedAt: "2026-07-20T00:00:00.000Z", + revision: 2, + source: "application", + }, + measurement: { protectedIdentity: { email: true, enabled: true, phone: true } }, + platform: "ios", + publishableKey: "vh_pk_test", + }); + +describe("protected identity", () => { + it("normalizes email and E.164 phone values deterministically", () => { + expect(normalizeProtectedEmail(" Test@EXAMPLE.com ")).toBe("test@example.com"); + expect(normalizeProtectedPhone("00 1 (415) 555-2671")).toBe("+14155552671"); + expect(hashProtectedIdentityValue("test@example.com")).toBe( + "973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b", + ); + }); + + it("marks caller-hashed input and agrees with SDK hashing", () => { + const hash = hashProtectedIdentityValue("test@example.com"); + const result = normalizeProtectedIdentityTraits({ + emails: [" Test@example.com ", { format: "sha256", value: hash }], + }); + expect(result.emails).toHaveLength(1); + expect(result.emails[0]?.hash).toBe(hash); + }); + + it("writes plaintext only through the protected adapter and exposes opaque references publicly", async () => { + const protectedWrites: Array<{ blobId: string; value: string }> = []; + const publicWrites: unknown[] = []; + const adapter: MeasurementRuntimeAdapter = { + enqueueMeasurement: async (command) => { publicWrites.push(command.envelope); }, + putProtectedEvidence: async (input) => { + protectedWrites.push({ blobId: input.blobId, value: new TextDecoder().decode(input.value) }); + return input.blobId; + }, + }; + const result = await runtime(adapter).setProtectedIdentityTraits({ + emails: ["Test@example.com"], + phones: ["+14155552671"], + }); + expect(result.status).toBe("stored"); + expect(protectedWrites).toHaveLength(2); + expect(protectedWrites.map(({ value }) => JSON.parse(value).normalized)).toEqual([ + "test@example.com", + "+14155552671", + ]); + const serializedPublic = JSON.stringify(publicWrites); + expect(serializedPublic).not.toContain("test@example.com"); + expect(serializedPublic).not.toContain("+14155552671"); + expect(result.references.every((reference) => serializedPublic.includes(reference))).toBe(true); + }); + + it("does not enqueue an opaque reference until its native vault write is durable", async () => { + let release!: () => void; + const gate = new Promise((resolve) => { release = resolve; }); + const publicWrites: string[] = []; + const update = runtime({ + enqueueMeasurement: async (command) => { publicWrites.push(command.recordType); }, + putProtectedEvidence: async (input) => { + await gate; + return input.blobId; + }, + }).setProtectedIdentityTraits({ emails: ["test@example.com"] }); + await Promise.resolve(); + expect(publicWrites).toEqual([]); + release(); + await update; + expect(publicWrites).toEqual(["protected_identity.updated.v1"]); + }); + + it("returns an observable policy block without writing protected values", async () => { + let writes = 0; + const result = await runtime({ putProtectedEvidence: async (input) => { writes += 1; return input.blobId; } }, false) + .setProtectedIdentityTraits({ emails: ["test@example.com"] }); + expect(result).toMatchObject({ status: "policyBlocked", references: [] }); + expect(writes).toBe(0); + }); + + it("is disabled by default and clears prior vault references explicitly", async () => { + const disabled = new UnifiedMeasurementRuntime({ + baseUrl: "https://api.voidhash.com", + platform: "android", + publishableKey: "vh_pk_test", + }); + await expect(disabled.setProtectedIdentityTraits({ emails: ["test@example.com"] })).resolves.toMatchObject({ + status: "disabled", + }); + + const deleted: string[] = []; + const enabled = runtime({ + deleteProtectedEvidence: async (reference) => { deleted.push(reference); return true; }, + putProtectedEvidence: async (input) => input.blobId, + }); + const stored = await enabled.setProtectedIdentityTraits({ emails: ["test@example.com"] }); + const cleared = await enabled.setProtectedIdentityTraits({ clearEmails: true }); + expect(deleted).toEqual(stored.references); + expect(cleared.cleared).toEqual(["email"]); + }); +}); diff --git a/libraries/react-native/tests/core/transaction-native-dedupe-contract.test.ts b/libraries/react-native/tests/core/transaction-native-dedupe-contract.test.ts new file mode 100644 index 000000000..a74c8c4b1 --- /dev/null +++ b/libraries/react-native/tests/core/transaction-native-dedupe-contract.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const source = readFileSync( + new URL("../../src/core/transactions/transaction-service.ts", import.meta.url), + "utf8", +); + +describe("transaction native dedupe contract", () => { + it("uses the native registry without cache or TTL source-of-truth state", () => { + expect(source).toContain("hasDurableDedupe"); + expect(source).toContain("checkAndSetDurableDedupe"); + expect(source).not.toContain("CacheManager"); + expect(source).not.toContain("PROCESSED_TRANSACTION_TTL"); + expect(source).not.toContain("processed-transaction:"); + }); +}); diff --git a/libraries/react-native/tests/helpers/effect-test-harness.ts b/libraries/react-native/tests/helpers/effect-test-harness.ts index 4363fdae2..b62905c93 100644 --- a/libraries/react-native/tests/helpers/effect-test-harness.ts +++ b/libraries/react-native/tests/helpers/effect-test-harness.ts @@ -23,6 +23,7 @@ import { ProductService } from "../../src/core/products/product-service"; import type { RuntimeProductDefinition } from "../../src/core/schema/runtime"; import { SchemaManager } from "../../src/core/schema/schema-manager"; import { SdkConfiguration } from "../../src/core/sdk-configuration"; +import { UnifiedMeasurementRuntime } from "../../src/core/measurement/runtime"; import { TransactionService } from "../../src/core/transactions/transaction-service"; import { createTestSchema } from "./test-schema"; @@ -273,6 +274,7 @@ export interface EffectTestHarnessOptions { baseUrl?: string; cacheAdapter: ReturnType["adapter"]; debug?: boolean; + dedupeStore?: Map; fetch?: typeof globalThis.fetch; ingestUrl?: string; lifecycleAdapter?: ReturnType; @@ -299,6 +301,22 @@ export function createEffectTestHarness(options: EffectTestHarnessOptions) { const atomRegistry = options.atomRegistry ?? AtomRegistry.make(); const lifecycle = options.lifecycleAdapter ?? createLifecycleAdapterDouble(); + const dedupeStore = options.dedupeStore ?? new Map(); + const measurementRuntime = new UnifiedMeasurementRuntime({ + adapter: { + checkAndSetDedupe: async (namespace, key) => { + const namespaced = `${namespace}:${key}`; + if (dedupeStore.has(namespaced)) return false; + dedupeStore.set(namespaced, true); + return true; + }, + hasDedupe: async (namespace, key) => dedupeStore.has(`${namespace}:${key}`), + }, + baseUrl: options.baseUrl ?? "https://api.voidhash.test", + ingestUrl: options.ingestUrl, + platform: options.platform?.platform === "android" ? "android" : "ios", + publishableKey: options.publishableKey ?? "pk_test", + }); const baseLayer = pipe( PersonAttributeManager.Default, @@ -333,6 +351,7 @@ export function createEffectTestHarness(options: EffectTestHarnessOptions) { ingestUrl: options.ingestUrl, publishableKey: options.publishableKey ?? "pk_test", readOnly: options.readOnly ?? false, + measurementRuntime, }), ), ); diff --git a/packages/api-contracts/package.json b/packages/api-contracts/package.json index d67adc35a..1ff65b028 100644 --- a/packages/api-contracts/package.json +++ b/packages/api-contracts/package.json @@ -12,7 +12,8 @@ "exports": { ".": "./src/index.ts", "./errors": "./src/errors/index.ts", - "./event-capture": "./src/EventCapture.ts" + "./event-capture": "./src/EventCapture.ts", + "./links": "./src/Links.ts" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/api-contracts/src/EventCapture.ts b/packages/api-contracts/src/EventCapture.ts index 324646732..e90c8ffb2 100644 --- a/packages/api-contracts/src/EventCapture.ts +++ b/packages/api-contracts/src/EventCapture.ts @@ -87,11 +87,33 @@ export const CaptureBatchRequest = Schema.Struct({ token: Schema.NonEmptyString, }); +/** Stable reasons a record can be permanently rejected by ingestion. */ +export const CaptureRecordRejectionReason = Schema.Literals([ + "malformed_envelope", + "unsupported_schema_version", + "payload_too_large", + "invalid_project_scope", + "duplicate", + "invalid_context", + "reserved_event", + "policy_rejected", +]); + +export type CaptureRecordRejectionReason = typeof CaptureRecordRejectionReason.Type; + +/** A single record that was not accepted by the capture pipeline. */ +export class CaptureRejectedRecord extends Schema.Class( + "CaptureRejectedRecord", +)({ + recordId: Schema.NonEmptyString, + reason: CaptureRecordRejectionReason, +}) {} + export class CaptureAcceptedResponse extends Schema.Class( "CaptureAcceptedResponse", )({ - accepted: Schema.Int, - rejected: Schema.Int, + accepted: Schema.Array(Schema.NonEmptyString), + rejected: Schema.Array(CaptureRejectedRecord), }) {} const CaptureAcceptedApiResponse = CaptureAcceptedResponse.pipe(HttpApiSchema.status(202)); @@ -105,6 +127,79 @@ export class CaptureInvalidRequestError extends Schema.TaggedErrorClass( + "ProtectedEvidenceAcceptedResponse", +)({ + accepted: Schema.Literal(true), + blobId: Schema.NonEmptyString, +}) {} + +export class ProtectedEvidenceConflictError extends Schema.TaggedErrorClass()( + "ProtectedEvidenceConflictError", + { + ...CaptureErrorResponseFields, + code: Schema.Literal("protected_evidence_conflict"), + }, + { httpApiStatus: 409 }, +) {} + +/** Project-scoped deletion request containing opaque subject identifiers only. */ +export const MeasurementDeletionRequest = Schema.Struct({ + installationId: Schema.NonEmptyString, + personId: Schema.optional(Schema.NonEmptyString), + requestId: Schema.NonEmptyString, + requestedAt: DateValidFromString, + token: Schema.NonEmptyString, +}); + +export class MeasurementDeletionAcceptedResponse extends Schema.Class( + "MeasurementDeletionAcceptedResponse", +)({ + accepted: Schema.Literal(true), + deletedProtectedEvidence: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), + requestId: Schema.NonEmptyString, + status: Schema.Literal("completed"), +}) {} + export class CaptureUnauthorizedError extends Schema.TaggedErrorClass()( "CaptureUnauthorizedError", { @@ -114,6 +209,46 @@ export class CaptureUnauthorizedError extends Schema.TaggedErrorClass( + "SignedMeasurementConfigurationResponse", +)({ + expiresAt: DateValidFromString, + keyId: Schema.NonEmptyString, + payload: MeasurementConfigurationPayload, + projectId: Schema.NonEmptyString, + signature: Schema.NonEmptyString, + version: Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0))), +}) {} + export class CapturePayloadTooLargeError extends Schema.TaggedErrorClass()( "CapturePayloadTooLargeError", { @@ -191,5 +326,43 @@ export const EventCaptureApi = HttpApi.make("EventCaptureApi").add( success: CaptureAcceptedApiResponse, }), ) + .add( + HttpApiEndpoint.post("protected", "/measurement/protected", { + error: [ + CaptureInvalidRequestError, + CaptureUnauthorizedError, + CapturePayloadTooLargeError, + ProtectedEvidenceConflictError, + CaptureDependencyUnavailableError, + CaptureInternalServerError, + ], + payload: ProtectedEvidenceRequest, + success: ProtectedEvidenceAcceptedResponse.pipe(HttpApiSchema.status(202)), + }), + ) + .add( + HttpApiEndpoint.post("deleteMeasurementData", "/measurement/delete", { + error: [ + CaptureInvalidRequestError, + CaptureUnauthorizedError, + CaptureRateLimitedError, + CaptureDependencyUnavailableError, + CaptureInternalServerError, + ], + payload: MeasurementDeletionRequest, + success: MeasurementDeletionAcceptedResponse.pipe(HttpApiSchema.status(202)), + }), + ) + .add( + HttpApiEndpoint.get("getMeasurementConfiguration", "/measurement/config", { + error: [ + CaptureUnauthorizedError, + CaptureDependencyUnavailableError, + CaptureInternalServerError, + ], + headers: Schema.Struct({ "x-publishable-key": Schema.NonEmptyString }), + success: SignedMeasurementConfigurationResponse, + }), + ) .prefix("/i/v1"), ); diff --git a/packages/api-contracts/src/Links.ts b/packages/api-contracts/src/Links.ts new file mode 100644 index 000000000..9e034703d --- /dev/null +++ b/packages/api-contracts/src/Links.ts @@ -0,0 +1,139 @@ +import { Schema, SchemaTransformation } from "effect"; +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"; + +const DateValidFromString = Schema.String.pipe( + Schema.decodeTo( + Schema.DateValid, + SchemaTransformation.transform({ + decode: (value: string) => new Date(value), + encode: (value: Date) => value.toISOString(), + }), + ), +); + +const SafeLinkValue = Schema.String.pipe( + Schema.check(Schema.isMinLength(1)), + Schema.check(Schema.isMaxLength(1_024)), +); + +export const LinkCampaign = Schema.Struct({ + ad: Schema.optional(SafeLinkValue), + adSet: Schema.optional(SafeLinkValue), + campaign: Schema.optional(SafeLinkValue), + channel: Schema.optional(SafeLinkValue), + mediaSource: Schema.optional(SafeLinkValue), +}); + +export const LinkDestination = Schema.Struct({ + androidStoreUrl: Schema.optional(SafeLinkValue), + appleAppId: Schema.optional(SafeLinkValue), + baseDeepLink: Schema.optional(SafeLinkValue), + deepLinkValue: SafeLinkValue, + iosStoreUrl: Schema.optional(SafeLinkValue), + subvalues: Schema.optional(Schema.Record(Schema.String, SafeLinkValue)), + webFallbackUrl: Schema.optional(SafeLinkValue), +}); + +/** Project-scoped request for an immutable signed short-link definition. */ +export const CreateLinkRequest = Schema.Struct({ + brandedDomain: Schema.optional(SafeLinkValue), + campaign: Schema.optional(LinkCampaign), + customParameters: Schema.optional(Schema.Record(Schema.String, SafeLinkValue)), + destination: LinkDestination, + expiresAt: Schema.optional(DateValidFromString), + idempotencyKey: Schema.optional(SafeLinkValue), + referrerCustomerId: Schema.optional(SafeLinkValue), + referrerImageUrl: Schema.optional(SafeLinkValue), + referrerName: Schema.optional(SafeLinkValue), + referrerUid: Schema.optional(SafeLinkValue), + templateId: Schema.optional(SafeLinkValue), + token: SafeLinkValue, +}); + +export class CreateLinkResponse extends Schema.Class("CreateLinkResponse")({ + expiresAt: DateValidFromString, + linkId: SafeLinkValue, + url: SafeLinkValue, +}) {} + +export const ResolveDeferredLinkRequest = Schema.Struct({ + deferredToken: SafeLinkValue, + installationId: SafeLinkValue, + platform: Schema.Literals(["ios", "android"]), + token: SafeLinkValue, +}); + +export const DeferredLinkFound = Schema.Struct({ + campaign: Schema.optional(LinkCampaign), + clickId: SafeLinkValue, + clickedAt: DateValidFromString, + deferred: Schema.Literal(true), + expiresAt: DateValidFromString, + linkId: SafeLinkValue, + route: Schema.Struct({ + subvalues: Schema.Record(Schema.String, SafeLinkValue), + value: SafeLinkValue, + }), + signature: SafeLinkValue, + status: Schema.Literal("found"), +}); + +export const DeferredLinkNotFound = Schema.Struct({ + reason: Schema.Literals(["expired", "not-found", "replayed", "invalid"]), + status: Schema.Literal("notFound"), +}); + +export const ResolveDeferredLinkResponse = Schema.Struct({ + campaign: Schema.optional(LinkCampaign), + clickId: Schema.optional(SafeLinkValue), + clickedAt: Schema.optional(DateValidFromString), + deferred: Schema.optional(Schema.Literal(true)), + expiresAt: Schema.optional(DateValidFromString), + linkId: Schema.optional(SafeLinkValue), + reason: Schema.optional(Schema.Literals(["expired", "not-found", "replayed", "invalid"])), + route: Schema.optional(Schema.Struct({ + subvalues: Schema.Record(Schema.String, SafeLinkValue), + value: SafeLinkValue, + })), + signature: Schema.optional(SafeLinkValue), + status: Schema.Literals(["found", "notFound"]), +}); + +export class LinkInvalidRequestError extends Schema.TaggedErrorClass()( + "LinkInvalidRequestError", + { code: Schema.Literal("invalid_link_request"), error: Schema.NonEmptyString }, + { httpApiStatus: 400 }, +) {} + +export class LinkUnauthorizedError extends Schema.TaggedErrorClass()( + "LinkUnauthorizedError", + { code: Schema.Literal("unauthorized"), error: Schema.NonEmptyString }, + { httpApiStatus: 401 }, +) {} + +export class LinkRateLimitedError extends Schema.TaggedErrorClass()( + "LinkRateLimitedError", + { code: Schema.Literal("rate_limited"), error: Schema.NonEmptyString }, + { httpApiStatus: 429 }, +) {} + +export class LinkServiceUnavailableError extends Schema.TaggedErrorClass()( + "LinkServiceUnavailableError", + { code: Schema.Literal("service_unavailable"), error: Schema.NonEmptyString }, + { httpApiStatus: 503 }, +) {} + +export const LinksApi = HttpApi.make("LinksApi").add( + HttpApiGroup.make("links") + .add(HttpApiEndpoint.post("createLink", "/links", { + error: [LinkInvalidRequestError, LinkUnauthorizedError, LinkRateLimitedError, LinkServiceUnavailableError], + payload: CreateLinkRequest, + success: CreateLinkResponse.pipe(HttpApiSchema.status(201)), + })) + .add(HttpApiEndpoint.post("resolveDeferredLink", "/deferred/resolve", { + error: [LinkInvalidRequestError, LinkUnauthorizedError, LinkRateLimitedError, LinkServiceUnavailableError], + payload: ResolveDeferredLinkRequest, + success: ResolveDeferredLinkResponse, + })) + .prefix("/l/v1"), +); diff --git a/packages/core/package.json b/packages/core/package.json index 13bd006e3..bb23bcd6c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -59,6 +59,10 @@ "./services/analyticsIngest/EventProcessorService": "./src/services/analyticsIngest/EventProcessorService.ts", "./services/analyticsIngest/PolicyCounterStore": "./src/services/analyticsIngest/PolicyCounterStore.ts", "./services/analyticsIngest/ProcessorOutputs": "./src/services/analyticsIngest/ProcessorOutputs.ts", + "./services/measurement/MeasurementDeletionService": "./src/services/measurement/MeasurementDeletionService.ts", + "./services/measurement/MeasurementConfigurationService": "./src/services/measurement/MeasurementConfigurationService.ts", + "./services/measurement/ProtectedEvidenceService": "./src/services/measurement/ProtectedEvidenceService.ts", + "./services/measurement/LinkRedirectService": "./src/services/measurement/LinkRedirectService.ts", "./services/apiKeys/ApiKeyService": "./src/services/apiKeys/ApiKeyService.ts", "./services/auditLog/AuditLogPort": "./src/services/auditLog/AuditLogPort.ts", "./services/auth/AuthTokenVerifier": "./src/services/auth/AuthTokenVerifier.ts", diff --git a/packages/core/src/domain/measurement/ApplePostback.ts b/packages/core/src/domain/measurement/ApplePostback.ts new file mode 100644 index 000000000..1d48130ce --- /dev/null +++ b/packages/core/src/domain/measurement/ApplePostback.ts @@ -0,0 +1,129 @@ +export interface NormalizedApplePostback { + readonly appId: string; + readonly campaignId?: string; + readonly coarseConversionValue?: "low" | "medium" | "high"; + readonly fidelityType?: number; + readonly fineConversionValue?: number; + readonly framework: "skan" | "ad-attribution-kit"; + readonly lockWindow?: boolean; + readonly postbackSequenceIndex?: 0 | 1 | 2; + readonly rawVersion: string; + readonly redownload?: boolean; + readonly sourceIdentifier?: string; +} + +export interface ApplePostbackEvidence { + readonly evidenceId: string; + readonly normalized?: NormalizedApplePostback; + readonly rawBody: Uint8Array; + readonly receivedAt: string; + readonly verification: "verified" | "failed" | "not-provided"; + readonly rejectionReason?: "invalid-json" | "invalid-shape" | "oversized"; +} + +export type ApplePostbackSignatureVerifier = ( + body: Uint8Array, + signature: string, +) => boolean | Promise; + +const optionalInt = (value: unknown): number | undefined => + typeof value === "number" && Number.isInteger(value) ? value : undefined; + +/** Retains immutable raw Apple postback evidence and normalizes supported framework shapes. */ +export const parseApplePostback = async (input: { + readonly body: Uint8Array; + readonly evidenceId: string; + readonly framework: NormalizedApplePostback["framework"]; + readonly receivedAt: string; + readonly signature?: string; + readonly verifySignature?: ApplePostbackSignatureVerifier; + readonly maximumBytes?: number; +}): Promise => { + const maximum = input.maximumBytes ?? 64 * 1024; + if (input.body.byteLength > maximum) { + return { evidenceId: input.evidenceId, rawBody: input.body.slice(), receivedAt: input.receivedAt, rejectionReason: "oversized", verification: "not-provided" }; + } + let candidate: unknown; + try { + candidate = JSON.parse(new TextDecoder().decode(input.body)); + } catch { + return { evidenceId: input.evidenceId, rawBody: input.body.slice(), receivedAt: input.receivedAt, rejectionReason: "invalid-json", verification: "not-provided" }; + } + if (candidate === null || Array.isArray(candidate) || typeof candidate !== "object") { + return { evidenceId: input.evidenceId, rawBody: input.body.slice(), receivedAt: input.receivedAt, rejectionReason: "invalid-shape", verification: "not-provided" }; + } + const decoded = candidate as Record; + const appId = decoded["app-id"] ?? decoded.appId ?? decoded.advertisedItemId; + const rawVersion = decoded.version; + if (typeof appId !== "string" || appId.length === 0 || typeof rawVersion !== "string") { + return { evidenceId: input.evidenceId, rawBody: input.body.slice(), receivedAt: input.receivedAt, rejectionReason: "invalid-shape", verification: "not-provided" }; + } + const sequence = optionalInt(decoded["postback-sequence-index"] ?? decoded.postbackSequenceIndex); + if (sequence !== undefined && ![0, 1, 2].includes(sequence)) { + return { evidenceId: input.evidenceId, rawBody: input.body.slice(), receivedAt: input.receivedAt, rejectionReason: "invalid-shape", verification: "not-provided" }; + } + const signature = input.signature ?? (typeof decoded.signature === "string" ? decoded.signature : undefined); + const verification = signature && input.verifySignature + ? await input.verifySignature(input.body, signature) ? "verified" : "failed" + : "not-provided"; + const normalized: NormalizedApplePostback = { + appId, + campaignId: typeof decoded["campaign-id"] === "number" ? String(decoded["campaign-id"]) : undefined, + coarseConversionValue: ["low", "medium", "high"].includes(String(decoded["coarse-conversion-value"])) + ? decoded["coarse-conversion-value"] as "low" | "medium" | "high" + : undefined, + fidelityType: optionalInt(decoded["fidelity-type"]), + fineConversionValue: optionalInt(decoded["conversion-value"] ?? decoded.fineConversionValue), + framework: input.framework, + lockWindow: typeof decoded["lock-window"] === "boolean" ? decoded["lock-window"] : undefined, + postbackSequenceIndex: sequence as 0 | 1 | 2 | undefined, + rawVersion, + redownload: typeof decoded.redownload === "boolean" ? decoded.redownload : undefined, + sourceIdentifier: typeof decoded["source-identifier"] === "string" + ? decoded["source-identifier"] + : typeof decoded.publisherItemId === "string" ? decoded.publisherItemId : undefined, + }; + return { evidenceId: input.evidenceId, normalized, rawBody: input.body.slice(), receivedAt: input.receivedAt, verification }; +}; + +export interface AppleConversionRuleVersion { + readonly activeFrom: string; + readonly activeTo?: string; + readonly appId: string; + readonly coarse: Readonly>>; + readonly fine: Readonly>; + readonly ruleVersion: string; +} + +/** Selects the conversion rule active for the postback window and decodes its modeled meaning. */ +export const decodeAppleConversion = ( + postback: NormalizedApplePostback, + conversionWindowAt: string, + rules: ReadonlyArray, +): { readonly ruleVersion?: string; readonly meaning?: string; readonly status: "decoded" | "unknown-rule" | "unknown-value" } => { + const at = Date.parse(conversionWindowAt); + const rule = rules.find((candidate) => + candidate.appId === postback.appId + && Date.parse(candidate.activeFrom) <= at + && (!candidate.activeTo || at < Date.parse(candidate.activeTo)), + ); + if (!rule) return { status: "unknown-rule" }; + const meaning = postback.fineConversionValue === undefined + ? postback.coarseConversionValue && rule.coarse[postback.coarseConversionValue] + : rule.fine[postback.fineConversionValue]; + return meaning + ? { meaning, ruleVersion: rule.ruleVersion, status: "decoded" } + : { ruleVersion: rule.ruleVersion, status: "unknown-value" }; +}; + +/** Builds an anonymous cohort key that cannot contain installation, person, or device identity. */ +export const applePostbackCohortKey = ( + postback: NormalizedApplePostback, + campaign: string, + ruleVersion: string, +): string => [ + postback.appId, + campaign, + ruleVersion, + postback.postbackSequenceIndex ?? 0, +].join(":"); diff --git a/packages/core/src/domain/measurement/Attribution.ts b/packages/core/src/domain/measurement/Attribution.ts new file mode 100644 index 000000000..991bc3512 --- /dev/null +++ b/packages/core/src/domain/measurement/Attribution.ts @@ -0,0 +1,142 @@ +export interface AttributionCampaign { + readonly campaign?: string; + readonly channel?: string; + readonly mediaSource?: string; + readonly ad?: string; + readonly adSet?: string; +} + +export type AttributionTouchpointKind = + | "install-referrer" + | "deferred-token" + | "cohort" + | "push-open" + | "deep-link"; + +export interface AttributionTouchpoint { + readonly campaign: AttributionCampaign; + readonly deterministic: boolean; + readonly evidenceId: string; + readonly kind: AttributionTouchpointKind; + readonly occurredAt: string; + readonly reengagement?: boolean; +} + +export interface AttributionRuleSet { + readonly lookbackMs: number; + readonly modelVersion: string; + readonly priorities: ReadonlyArray; + readonly ruleVersion: string; +} + +export interface AttributionReasonStep { + readonly evidenceId: string; + readonly outcome: "won" | "lower-priority" | "outside-lookback" | "not-eligible"; + readonly rule: AttributionTouchpointKind; +} + +export interface VersionedAttributionDecision { + readonly campaign?: AttributionCampaign; + readonly confidence: "deterministic" | "probabilistic" | "organic"; + readonly decidedAt: string; + readonly decisionId: string; + readonly deterministic: boolean; + readonly evidenceIds: ReadonlyArray; + readonly kind: "install" | "reengagement" | "organic"; + readonly lookbackMs: number; + readonly modelVersion: string; + readonly priorityRules: ReadonlyArray; + readonly reasonTrace: ReadonlyArray; + readonly ruleVersion: string; + readonly status: "active" | "fraud-flagged" | "superseded"; + readonly supersededDecisionId?: string; +} + +export interface AttributionEvaluationInput { + readonly decisionId: string; + readonly decisionTime: string; + readonly kind: "install" | "reengagement"; + readonly ruleSet: AttributionRuleSet; + readonly subjectOccurredAt: string; + readonly supersededDecisionId?: string; + readonly touchpoints: ReadonlyArray; +} + +const timestamp = (value: string): number => { + const parsed = Date.parse(value); + if (!Number.isFinite(parsed)) throw new TypeError(`Invalid attribution timestamp: ${value}`); + return parsed; +}; + +/** Evaluates attribution using only injected evidence, rule version, and decision time. */ +export const evaluateAttribution = (input: AttributionEvaluationInput): VersionedAttributionDecision => { + if (!Number.isSafeInteger(input.ruleSet.lookbackMs) || input.ruleSet.lookbackMs < 0) { + throw new RangeError("Attribution lookback must be a non-negative safe integer"); + } + const subjectAt = timestamp(input.subjectOccurredAt); + const priority = new Map(input.ruleSet.priorities.map((kind, index) => [kind, index])); + const evaluated = input.touchpoints.map((touchpoint) => { + const age = subjectAt - timestamp(touchpoint.occurredAt); + const eligibleKind = input.kind === "install" + ? !touchpoint.reengagement && !["push-open", "deep-link"].includes(touchpoint.kind) + : touchpoint.reengagement === true && ["push-open", "deep-link"].includes(touchpoint.kind); + const eligible = eligibleKind && age >= 0 && age <= input.ruleSet.lookbackMs && priority.has(touchpoint.kind); + return { age, eligible, priority: priority.get(touchpoint.kind) ?? Number.MAX_SAFE_INTEGER, touchpoint }; + }); + const candidates = evaluated.filter(({ eligible }) => eligible).sort((left, right) => + left.priority - right.priority + || right.touchpoint.occurredAt.localeCompare(left.touchpoint.occurredAt) + || left.touchpoint.evidenceId.localeCompare(right.touchpoint.evidenceId), + ); + const winner = candidates[0]?.touchpoint; + const reasonTrace: AttributionReasonStep[] = evaluated + .sort((left, right) => left.touchpoint.evidenceId.localeCompare(right.touchpoint.evidenceId)) + .map(({ age, eligible, touchpoint }) => ({ + evidenceId: touchpoint.evidenceId, + outcome: !eligible + ? age < 0 || age > input.ruleSet.lookbackMs + ? "outside-lookback" + : "not-eligible" + : touchpoint.evidenceId === winner?.evidenceId + ? "won" + : "lower-priority", + rule: touchpoint.kind, + })); + return { + campaign: winner?.campaign, + confidence: winner ? (winner.deterministic ? "deterministic" : "probabilistic") : "organic", + decidedAt: input.decisionTime, + decisionId: input.decisionId, + deterministic: winner?.deterministic ?? true, + evidenceIds: winner ? [winner.evidenceId] : [], + kind: winner ? input.kind : "organic", + lookbackMs: input.ruleSet.lookbackMs, + modelVersion: input.ruleSet.modelVersion, + priorityRules: [...input.ruleSet.priorities], + reasonTrace, + ruleVersion: input.ruleSet.ruleVersion, + status: "active", + supersededDecisionId: input.supersededDecisionId, + }; +}; + +/** Appends a deterministic recomputation and returns an immutable superseded projection. */ +export const recomputeAttribution = ( + previous: VersionedAttributionDecision, + input: Omit, +): { readonly previous: VersionedAttributionDecision; readonly current: VersionedAttributionDecision } => ({ + previous: { ...previous, status: "superseded" }, + current: evaluateAttribution({ ...input, supersededDecisionId: previous.decisionId }), +}); + +/** Projects a decision to the protected-field-free SDK correlation response. */ +export const toSafeAttributionResponse = (decision: VersionedAttributionDecision) => ({ + campaign: decision.campaign, + decisionId: decision.decisionId, + deferred: decision.kind === "install" && decision.evidenceIds.length > 0, + deterministic: decision.deterministic, + direct: decision.kind === "reengagement", + kind: decision.kind, + modelVersion: decision.modelVersion, + reason: decision.reasonTrace.find(({ outcome }) => outcome === "won")?.rule ?? "organic", +}); diff --git a/packages/core/src/domain/measurement/FraudReporting.ts b/packages/core/src/domain/measurement/FraudReporting.ts new file mode 100644 index 000000000..df46fbc24 --- /dev/null +++ b/packages/core/src/domain/measurement/FraudReporting.ts @@ -0,0 +1,119 @@ +export interface FraudEvidence { + readonly campaign?: string; + readonly clickId?: string; + readonly evidenceId: string; + readonly installReferrerClickId?: string; + readonly kind: "click" | "install" | "token-replay"; + readonly linkId?: string; + readonly occurredAt: string; +} + +export interface FraudRuleSet { + readonly maximumClicksPerWindow: number; + readonly maximumInstallDelayMs: number; + readonly minimumInstallDelayMs: number; + readonly ruleVersion: string; + readonly windowMs: number; +} + +export interface FraudFlag { + readonly evidenceIds: ReadonlyArray; + readonly flagId: string; + readonly reason: "click-flooding" | "click-to-install-anomaly" | "token-replay" | "referrer-click-mismatch"; + readonly ruleVersion: string; + readonly severity: "warning" | "block"; +} + +/** Appends deterministic fraud flags without changing source evidence. */ +export const evaluateFraud = ( + evidence: ReadonlyArray, + rules: FraudRuleSet, +): ReadonlyArray => { + const flags: FraudFlag[] = []; + const clicks = evidence.filter(({ kind }) => kind === "click").sort((left, right) => left.occurredAt.localeCompare(right.occurredAt)); + for (let index = 0; index < clicks.length; index += 1) { + const start = Date.parse(clicks[index]?.occurredAt ?? ""); + const window = clicks.filter((click) => { + const current = Date.parse(click.occurredAt); + return current >= start && current - start <= rules.windowMs; + }); + if (window.length > rules.maximumClicksPerWindow) { + const ids = window.map(({ evidenceId }) => evidenceId).sort(); + flags.push({ evidenceIds: ids, flagId: `${rules.ruleVersion}:click-flooding:${ids.join(",")}`, reason: "click-flooding", ruleVersion: rules.ruleVersion, severity: "block" }); + break; + } + } + for (const install of evidence.filter(({ kind }) => kind === "install")) { + const click = clicks + .filter((candidate) => candidate.clickId === install.clickId) + .sort((left, right) => right.occurredAt.localeCompare(left.occurredAt))[0]; + if (click) { + const delay = Date.parse(install.occurredAt) - Date.parse(click.occurredAt); + if (delay < rules.minimumInstallDelayMs || delay > rules.maximumInstallDelayMs) { + flags.push({ + evidenceIds: [click.evidenceId, install.evidenceId], + flagId: `${rules.ruleVersion}:ctit:${install.evidenceId}`, + reason: "click-to-install-anomaly", + ruleVersion: rules.ruleVersion, + severity: "block", + }); + } + } + if (install.clickId && install.installReferrerClickId && install.clickId !== install.installReferrerClickId) { + flags.push({ + evidenceIds: [install.evidenceId], + flagId: `${rules.ruleVersion}:mismatch:${install.evidenceId}`, + reason: "referrer-click-mismatch", + ruleVersion: rules.ruleVersion, + severity: "block", + }); + } + } + for (const replay of evidence.filter(({ kind }) => kind === "token-replay")) { + flags.push({ + evidenceIds: [replay.evidenceId], + flagId: `${rules.ruleVersion}:replay:${replay.evidenceId}`, + reason: "token-replay", + ruleVersion: rules.ruleVersion, + severity: "block", + }); + } + return [...new Map(flags.map((flag) => [flag.flagId, flag])).values()].sort((left, right) => left.flagId.localeCompare(right.flagId)); +}; + +export interface MeasurementExportRow { + readonly deleted?: boolean; + readonly payload: Readonly>; + readonly recordId: string; + readonly schemaVersion: number; + readonly type: string; +} + +const protectedKey = /(?:ciphertext|email|phone|receipt|token|rawUrl|advertisingId|idfa|gaid)/i; + +/** Produces a deletion-aware raw export with protected fields removed recursively. */ +export const buildMeasurementRawExport = ( + rows: ReadonlyArray, +): ReadonlyArray => rows + .filter(({ deleted }) => deleted !== true) + .map((row) => ({ ...row, payload: redact(row.payload) as Readonly> })); + +const redact = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map(redact); + if (value === null || typeof value !== "object") return value; + return Object.fromEntries(Object.entries(value).flatMap(([key, nested]) => + protectedKey.test(key) ? [] : [[key, redact(nested)]])); +}; + +/** Counts report rows by type and campaign using the same deletion semantics as raw export. */ +export const aggregateMeasurementReport = (rows: ReadonlyArray) => { + const exported = buildMeasurementRawExport(rows); + const byCampaign: Record = {}; + const byType: Record = {}; + for (const row of exported) { + byType[row.type] = (byType[row.type] ?? 0) + 1; + const campaign = row.payload.campaign; + if (typeof campaign === "string") byCampaign[campaign] = (byCampaign[campaign] ?? 0) + 1; + } + return { byCampaign, byType, total: exported.length }; +}; diff --git a/packages/core/src/domain/measurement/LinkRedirect.ts b/packages/core/src/domain/measurement/LinkRedirect.ts new file mode 100644 index 000000000..ee53bbbf6 --- /dev/null +++ b/packages/core/src/domain/measurement/LinkRedirect.ts @@ -0,0 +1,319 @@ +export interface LinkRoute { + readonly value: string; + readonly subvalues: Readonly>; +} + +export interface LinkDefinition { + readonly androidStoreUrl?: string; + readonly appleAppId?: string; + readonly baseDeepLink?: string; + readonly brandedDomain?: string; + readonly campaign?: Readonly>; + readonly createdAt: string; + readonly customParameters?: Readonly>; + readonly expiresAt: string; + readonly iosStoreUrl?: string; + readonly linkId: string; + readonly projectId: string; + readonly referrerCustomerId?: string; + readonly referrerImageUrl?: string; + readonly referrerName?: string; + readonly referrerUid?: string; + readonly route: LinkRoute; + readonly templateId?: string; + readonly webFallbackUrl?: string; +} + +export interface LinkClickEvidence { + readonly clickId: string; + readonly context: { + readonly platform: "ios" | "android" | "web"; + readonly refererOrigin?: string; + readonly userAgentFamily: "apple" | "android" | "browser" | "unknown"; + }; + readonly linkId: string; + readonly occurredAt: string; + readonly projectId: string; +} + +interface SignedLinkPayload { + readonly clickId?: string; + readonly expiresAt: string; + readonly issuedAt: string; + readonly kind: "link" | "deferred"; + readonly linkId: string; + readonly projectId: string; +} + +export interface LinkSigningKey { + readonly keyId: string; + readonly privateKey?: CryptoKey; + readonly publicKey: CryptoKey; +} + +export type DeferredResolution = + | { + readonly status: "found"; + readonly clickId: string; + readonly clickedAt: string; + readonly expiresAt: string; + readonly linkId: string; + readonly route: LinkRoute; + readonly campaign?: Readonly>; + } + | { readonly status: "notFound"; readonly reason: "expired" | "invalid" | "not-found" | "replayed" }; + +const canonicalize = (value: unknown): string => { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + return `{${Object.entries(value as Record) + .filter(([, nested]) => nested !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nested]) => `${JSON.stringify(key)}:${canonicalize(nested)}`) + .join(",")}}`; +}; + +const base64Url = (bytes: Uint8Array): string => { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +}; + +const fromBase64Url = (value: string): Uint8Array => { + if (!/^[A-Za-z\d_-]+$/.test(value)) throw new TypeError("invalid base64url"); + const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "="); + return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0)); +}; + +const bytesBuffer = (bytes: Uint8Array): ArrayBuffer => + bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; + +/** Creates an Ed25519 key suitable for tests and self-host development. */ +export const createLinkSigningKey = async (keyId: string): Promise => { + const pair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + return { keyId, privateKey: pair.privateKey, publicKey: pair.publicKey }; +}; + +/** Signs a bounded, canonical link or deferred-correlation payload. */ +export const signLinkPayload = async ( + payload: SignedLinkPayload, + key: LinkSigningKey, +): Promise => { + if (!key.privateKey) throw new TypeError("link signing key is verification-only"); + const encodedPayload = new TextEncoder().encode(canonicalize(payload)); + const signature = await crypto.subtle.sign("Ed25519", key.privateKey, bytesBuffer(encodedPayload)); + return `${key.keyId}.${base64Url(encodedPayload)}.${base64Url(new Uint8Array(signature))}`; +}; + +/** Verifies token signature, project scope, kind, expiry, and optional link binding. */ +export const verifyLinkPayload = async (input: { + readonly expectedKind: SignedLinkPayload["kind"]; + readonly expectedLinkId?: string; + readonly expectedProjectId: string; + readonly keys: ReadonlyMap; + readonly now: Date; + readonly token: string; + readonly allowExpired?: boolean; +}): Promise => { + try { + const [keyId, payloadPart, signaturePart, extra] = input.token.split("."); + if (!keyId || !payloadPart || !signaturePart || extra) return undefined; + const key = input.keys.get(keyId); + if (!key) return undefined; + const payloadBytes = fromBase64Url(payloadPart); + if (payloadBytes.byteLength > 4_096) return undefined; + const verified = await crypto.subtle.verify( + "Ed25519", + key.publicKey, + bytesBuffer(fromBase64Url(signaturePart)), + bytesBuffer(payloadBytes), + ); + if (!verified) return undefined; + const payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as SignedLinkPayload; + if ( + canonicalize(payload) !== new TextDecoder().decode(payloadBytes) || + payload.kind !== input.expectedKind || + payload.projectId !== input.expectedProjectId || + (input.expectedLinkId !== undefined && payload.linkId !== input.expectedLinkId) || + !Number.isFinite(Date.parse(payload.expiresAt)) || + (!input.allowExpired && Date.parse(payload.expiresAt) <= input.now.getTime()) + ) return undefined; + return payload; + } catch { + return undefined; + } +}; + +const safeUrl = (value: string): URL => { + const url = new URL(value); + const local = new Set(["localhost", "127.0.0.1", "[::1]"]).has(url.hostname); + if ((url.protocol !== "https:" && !(url.protocol === "http:" && local)) || url.username || url.password) { + throw new TypeError("unsafe link destination"); + } + return url; +}; + +const appleStoreUrl = (appleAppId: string): string => { + const normalized = appleAppId.replace(/^id/i, ""); + if (!/^\d{5,20}$/.test(normalized)) throw new TypeError("invalid Apple app ID"); + return `https://apps.apple.com/app/id${normalized}`; +}; + +const validateDefinition = (definition: LinkDefinition): void => { + for (const destination of [ + definition.androidStoreUrl, + definition.iosStoreUrl, + definition.webFallbackUrl, + definition.referrerImageUrl, + ]) { + if (destination) safeUrl(destination); + } + if (definition.appleAppId) appleStoreUrl(definition.appleAppId); + const customParameters = Object.entries(definition.customParameters ?? {}); + if (customParameters.length > 50) throw new TypeError("too many custom parameters"); + for (const [key, value] of customParameters) { + if (!/^[A-Za-z][A-Za-z\d_.-]{0,63}$/.test(key) || value.length > 1_024) { + throw new TypeError("invalid custom parameter"); + } + } +}; + +const classifyContext = (userAgent: string, referer?: string): LinkClickEvidence["context"] => { + const ios = /(?:iphone|ipad|ipod)/i.test(userAgent); + const android = /android/i.test(userAgent); + let refererOrigin: string | undefined; + try { + if (referer) refererOrigin = safeUrl(referer).origin; + } catch { + refererOrigin = undefined; + } + return { + platform: ios ? "ios" : android ? "android" : "web", + refererOrigin, + userAgentFamily: ios ? "apple" : android ? "android" : userAgent ? "browser" : "unknown", + }; +}; + +/** Immutable in-memory reference implementation for signed redirect and deferred-token semantics. */ +export class LinkRedirectEngine { + private readonly definitions = new Map(); + private readonly clicks = new Map(); + private readonly consumedDeferredTokens = new Set(); + private readonly signingKey: LinkSigningKey; + private readonly verificationKeys: ReadonlyMap; + private readonly now: () => Date; + + constructor( + signingKey: LinkSigningKey, + verificationKeys: ReadonlyMap, + now: () => Date = () => new Date(), + ) { + this.signingKey = signingKey; + this.verificationKeys = verificationKeys; + this.now = now; + } + + /** Persists a definition and returns a signed short URL. */ + async create(definition: LinkDefinition, origin: string): Promise<{ readonly token: string; readonly url: string }> { + if (this.definitions.has(definition.linkId)) throw new TypeError("duplicate link id"); + validateDefinition(definition); + this.definitions.set(definition.linkId, structuredClone(definition)); + const token = await signLinkPayload({ + expiresAt: definition.expiresAt, + issuedAt: definition.createdAt, + kind: "link", + linkId: definition.linkId, + projectId: definition.projectId, + }, this.signingKey); + return { token, url: `${safeUrl(origin).origin}/l/${encodeURIComponent(definition.linkId)}?token=${encodeURIComponent(token)}` }; + } + + /** Records immutable click evidence before returning any redirect destination. */ + async click(input: { + readonly clickId: string; + readonly linkId: string; + readonly referer?: string; + readonly token: string; + readonly userAgent: string; + }): Promise<{ readonly deferredToken: string; readonly destination: string } | undefined> { + const definition = this.definitions.get(input.linkId); + if (!definition) return undefined; + const verified = await verifyLinkPayload({ + expectedKind: "link", + expectedLinkId: input.linkId, + expectedProjectId: definition.projectId, + keys: this.verificationKeys, + now: this.now(), + token: input.token, + }); + if (!verified) return undefined; + if (this.clicks.has(input.clickId)) return undefined; + const context = classifyContext(input.userAgent, input.referer); + const occurredAt = this.now().toISOString(); + this.clicks.set(input.clickId, { + clickId: input.clickId, + context, + linkId: input.linkId, + occurredAt, + projectId: definition.projectId, + }); + const deferredToken = await signLinkPayload({ + clickId: input.clickId, + expiresAt: new Date(Math.min(Date.parse(definition.expiresAt), this.now().getTime() + 24 * 60 * 60 * 1_000)).toISOString(), + issuedAt: occurredAt, + kind: "deferred", + linkId: definition.linkId, + projectId: definition.projectId, + }, this.signingKey); + const destination = context.platform === "ios" + ? definition.iosStoreUrl ?? (definition.appleAppId ? appleStoreUrl(definition.appleAppId) : definition.webFallbackUrl) + : context.platform === "android" + ? definition.androidStoreUrl ?? definition.webFallbackUrl + : definition.webFallbackUrl; + if (!destination) return undefined; + const redirect = safeUrl(destination); + if (context.platform === "android") redirect.searchParams.set("referrer", deferredToken); + return { deferredToken, destination: redirect.toString() }; + } + + /** Resolves a signed deferred token once without probabilistic matching. */ + async resolveDeferred(projectId: string, token: string): Promise { + if (this.consumedDeferredTokens.has(token)) return { reason: "replayed", status: "notFound" }; + const verified = await verifyLinkPayload({ + expectedKind: "deferred", + expectedProjectId: projectId, + keys: this.verificationKeys, + now: this.now(), + token, + allowExpired: true, + }); + if (!verified) return { reason: "invalid", status: "notFound" }; + if (Date.parse(verified.expiresAt) <= this.now().getTime()) { + return { reason: "expired", status: "notFound" }; + } + const definition = this.definitions.get(verified.linkId); + const click = verified.clickId ? this.clicks.get(verified.clickId) : undefined; + if (!definition || !click) return { reason: "not-found", status: "notFound" }; + this.consumedDeferredTokens.add(token); + return { + campaign: definition.campaign, + clickId: click.clickId, + clickedAt: click.occurredAt, + expiresAt: verified.expiresAt, + linkId: definition.linkId, + route: definition.route, + status: "found", + }; + } + + /** Returns immutable click evidence for reporting tests. */ + evidence(): ReadonlyArray { + return [...this.clicks.values()].map((entry) => structuredClone(entry)); + } + + /** Returns an immutable link definition for management and reporting projections. */ + definition(linkId: string): LinkDefinition | undefined { + const definition = this.definitions.get(linkId); + return definition ? structuredClone(definition) : undefined; + } +} diff --git a/packages/core/src/domain/measurement/PartnerPostback.ts b/packages/core/src/domain/measurement/PartnerPostback.ts new file mode 100644 index 000000000..e2ef89e53 --- /dev/null +++ b/packages/core/src/domain/measurement/PartnerPostback.ts @@ -0,0 +1,105 @@ +export interface PartnerCatalogEntry { + readonly endpoint: string; + readonly fieldMapping: Readonly>; + readonly partner: string; + readonly requiredFields: ReadonlyArray; +} + +export interface PartnerPostbackPolicy { + readonly anonymize: boolean; + readonly consentRevision: number; + readonly deleted: boolean; + readonly excludedFields?: ReadonlyArray; + readonly excludedPartners?: ReadonlyArray; + readonly partnerSharing: boolean; +} + +export interface PartnerPostbackPlan { + readonly audit: { + readonly consentRevision: number; + readonly filteredFields: ReadonlyArray; + readonly partner: string; + readonly reason: string; + readonly result: "ready" | "suppressed"; + readonly triggerId: string; + }; + readonly idempotencyKey: string; + readonly request?: { + readonly endpoint: string; + readonly headers: Readonly>; + readonly payload: Readonly>; + }; +} + +const protectedField = /(?:email|phone|receipt|token|raw|url|idfa|gaid|advertising.?id)/i; + +/** Builds an allowlisted postback request after evaluating current send-time policy. */ +export const planPartnerPostback = ( + catalog: PartnerCatalogEntry, + triggerId: string, + trigger: Readonly>, + partnerData: Readonly>, + policy: PartnerPostbackPolicy, +): PartnerPostbackPlan => { + const idempotencyKey = `${catalog.partner}:${triggerId}`; + const suppress = (reason: string): PartnerPostbackPlan => ({ + audit: { + consentRevision: policy.consentRevision, + filteredFields: [], + partner: catalog.partner, + reason, + result: "suppressed", + triggerId, + }, + idempotencyKey, + }); + if (policy.deleted) return suppress("subject-deleted"); + if (!policy.partnerSharing) return suppress("partner-sharing-opt-out"); + if (policy.excludedPartners?.includes(catalog.partner)) return suppress("partner-excluded"); + + const excluded = new Set(policy.excludedFields ?? []); + const payload: Record = {}; + const filteredFields: string[] = []; + for (const [source, destination] of Object.entries(catalog.fieldMapping)) { + if (excluded.has(source) || protectedField.test(source) || protectedField.test(destination)) { + filteredFields.push(source); + continue; + } + const value = source.startsWith("partnerData.") + ? partnerData[source.slice("partnerData.".length)] + : trigger[source]; + if (value !== undefined) payload[destination] = value; + } + if (policy.anonymize && typeof payload.distinctId === "string") { + payload.distinctId = `anonymous:${idempotencyKey}`; + } + const missing = catalog.requiredFields.filter((field) => payload[field] === undefined); + if (missing.length > 0) return suppress(`missing-required:${missing.sort().join(",")}`); + return { + audit: { + consentRevision: policy.consentRevision, + filteredFields: filteredFields.sort(), + partner: catalog.partner, + reason: "allowed", + result: "ready", + triggerId, + }, + idempotencyKey, + request: { + endpoint: catalog.endpoint, + headers: { "content-type": "application/json", "idempotency-key": idempotencyKey }, + payload, + }, + }; +}; + +/** Computes a bounded deterministic retry delay while honoring Retry-After. */ +export const partnerPostbackRetryDelay = ( + attempt: number, + retryAfterMs: number | undefined, + maximumAttempts = 8, +): number | undefined => { + if (!Number.isInteger(attempt) || attempt < 0 || attempt >= maximumAttempts) return undefined; + const exponential = Math.min(60 * 60 * 1_000, 1_000 * 2 ** attempt); + return Math.max(exponential, retryAfterMs ?? 0); +}; diff --git a/packages/core/src/domain/measurement/StoreLifecycle.ts b/packages/core/src/domain/measurement/StoreLifecycle.ts new file mode 100644 index 000000000..2c213adbd --- /dev/null +++ b/packages/core/src/domain/measurement/StoreLifecycle.ts @@ -0,0 +1,154 @@ +export interface PurchaseCorrelationEvidence { + readonly accountToken?: string; + readonly environment: "sandbox" | "production"; + readonly installationId: string; + readonly originalTransactionId?: string; + readonly personId?: string; + readonly purchaseToken?: string; + readonly transactionId?: string; +} + +export interface StoreNotificationEvidence { + readonly accountToken?: string; + readonly environment: "sandbox" | "production"; + readonly linkedPurchaseToken?: string; + readonly notificationId: string; + readonly originalTransactionId?: string; + readonly provider: "apple" | "google"; + readonly purchaseToken?: string; + readonly transactionId?: string; + readonly type: StoreNotificationType; +} + +export type StoreNotificationType = + | "purchased" + | "renewed" + | "canceled" + | "grace-period" + | "billing-retry" + | "paused" + | "resumed" + | "replaced" + | "refunded" + | "revoked" + | "expired" + | "prepaid-top-up"; + +export type NormalizedStoreLifecycleState = + | "active" + | "renewed" + | "canceled" + | "grace" + | "billing-retry" + | "paused" + | "resumed" + | "replaced" + | "refunded" + | "revoked" + | "expired" + | "prepaid"; + +export type StoreNotificationCorrelation = + | { + readonly status: "matched"; + readonly key: "account-token" | "lineage" | "transaction"; + readonly installationId: string; + readonly personId?: string; + } + | { readonly status: "unmatched"; readonly reason: "purchase-evidence-not-found" | "environment-mismatch" }; + +/** Correlates a decoded store notification using the documented key precedence. */ +export const correlateStoreNotification = ( + notification: StoreNotificationEvidence, + purchases: ReadonlyArray, +): StoreNotificationCorrelation => { + const sameEnvironment = purchases.filter(({ environment }) => environment === notification.environment); + const match = ( + predicate: (purchase: PurchaseCorrelationEvidence) => boolean, + key: "account-token" | "lineage" | "transaction", + ): StoreNotificationCorrelation | undefined => { + const purchase = sameEnvironment.find(predicate); + return purchase && { + status: "matched", + key, + installationId: purchase.installationId, + personId: purchase.personId, + }; + }; + if (notification.accountToken) { + const correlated = match(({ accountToken }) => accountToken === notification.accountToken, "account-token"); + if (correlated) return correlated; + } + const lineage = notification.originalTransactionId ?? notification.linkedPurchaseToken; + if (lineage) { + const correlated = match( + (purchase) => purchase.originalTransactionId === lineage || purchase.purchaseToken === lineage, + "lineage", + ); + if (correlated) return correlated; + } + const transaction = notification.transactionId ?? notification.purchaseToken; + if (transaction) { + const correlated = match( + (purchase) => purchase.transactionId === transaction || purchase.purchaseToken === transaction, + "transaction", + ); + if (correlated) return correlated; + } + const crossEnvironment = purchases.some((purchase) => + (notification.accountToken && purchase.accountToken === notification.accountToken) + || (lineage && (purchase.originalTransactionId === lineage || purchase.purchaseToken === lineage)) + || (transaction && (purchase.transactionId === transaction || purchase.purchaseToken === transaction)), + ); + return { status: "unmatched", reason: crossEnvironment ? "environment-mismatch" : "purchase-evidence-not-found" }; +}; + +/** Normalizes Apple and Google lifecycle notification types into one model. */ +export const normalizeStoreLifecycleState = (type: StoreNotificationType): NormalizedStoreLifecycleState => { + const states: Record = { + purchased: "active", + renewed: "renewed", + canceled: "canceled", + "grace-period": "grace", + "billing-retry": "billing-retry", + paused: "paused", + resumed: "resumed", + replaced: "replaced", + refunded: "refunded", + revoked: "revoked", + expired: "expired", + "prepaid-top-up": "prepaid", + }; + return states[type]; +}; + +export interface StoreLifecycleProjection { + readonly environment: "sandbox" | "production"; + readonly installationId: string; + readonly notificationId: string; + readonly personId?: string; + readonly source: "server-correlation"; + readonly state: NormalizedStoreLifecycleState; +} + +/** Projects a matched notification idempotently, or parks it until purchase evidence arrives. */ +export const projectStoreNotification = ( + notification: StoreNotificationEvidence, + purchases: ReadonlyArray, + processedNotificationIds: ReadonlySet, +): { readonly status: "projected" | "parked" | "duplicate"; readonly projection?: StoreLifecycleProjection } => { + if (processedNotificationIds.has(notification.notificationId)) return { status: "duplicate" }; + const correlation = correlateStoreNotification(notification, purchases); + if (correlation.status === "unmatched") return { status: "parked" }; + return { + status: "projected", + projection: { + environment: notification.environment, + installationId: correlation.installationId, + notificationId: notification.notificationId, + personId: correlation.personId, + source: "server-correlation", + state: normalizeStoreLifecycleState(notification.type), + }, + }; +}; diff --git a/packages/core/src/domain/measurement/UninstallInference.ts b/packages/core/src/domain/measurement/UninstallInference.ts new file mode 100644 index 000000000..82bc9f410 --- /dev/null +++ b/packages/core/src/domain/measurement/UninstallInference.ts @@ -0,0 +1,86 @@ +export interface PushRegistrationEvidence { + readonly environment: "development" | "production"; + readonly installationId: string; + readonly personId?: string; + readonly previousPushDeviceTokenId?: string; + readonly pushDeviceTokenId: string; + readonly registeredAt: string; + readonly unregisteredAt?: string; +} + +export interface PushDeliveryAttemptEvidence { + readonly attemptId: string; + readonly occurredAt: string; + readonly provider: "apns" | "fcm"; + readonly providerInvalidAt?: string; + readonly pushDeviceTokenId: string; + readonly result: "success" | "unregistered" | "bad-token" | "invalid-argument" | "throttled" | "server-error"; +} + +export interface UninstallInferenceRecord { + readonly confidence: "high" | "medium"; + readonly contributingAttemptIds: ReadonlyArray; + readonly environment: "development" | "production"; + readonly inferredAfter: string; + readonly inferredBefore: string; + readonly installationId: string; + readonly personId?: string; + readonly pushDeviceTokenId: string; + readonly status: "active" | "superseded"; + readonly supersededAt?: string; +} + +const time = (value: string): number => { + const parsed = Date.parse(value); + if (!Number.isFinite(parsed)) throw new TypeError(`Invalid push evidence timestamp: ${value}`); + return parsed; +}; + +/** Derives deterministic uninstall windows from existing registration and delivery evidence. */ +export const inferUninstalls = ( + registrations: ReadonlyArray, + attempts: ReadonlyArray, +): ReadonlyArray => { + const registrationsByToken = new Map(registrations.map((registration) => [registration.pushDeviceTokenId, registration])); + return attempts + .filter(({ result }) => result === "unregistered" || result === "bad-token") + .sort((left, right) => left.occurredAt.localeCompare(right.occurredAt) || left.attemptId.localeCompare(right.attemptId)) + .flatMap((attempt) => { + const registration = registrationsByToken.get(attempt.pushDeviceTokenId); + if (!registration || registration.unregisteredAt) return []; + const successorAtFailure = registrations.find((candidate) => + candidate.installationId === registration.installationId + && candidate.pushDeviceTokenId !== registration.pushDeviceTokenId + && time(candidate.registeredAt) <= time(attempt.occurredAt) + && time(candidate.registeredAt) > time(registration.registeredAt), + ); + if (successorAtFailure) return []; + const invalidAt = attempt.providerInvalidAt ?? attempt.occurredAt; + if (time(invalidAt) < time(registration.registeredAt)) return []; + const successes = attempts.filter((candidate) => + candidate.pushDeviceTokenId === attempt.pushDeviceTokenId + && candidate.result === "success" + && time(candidate.occurredAt) <= time(attempt.occurredAt), + ); + const lastSuccess = successes.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt))[0]; + const laterRegistration = registrations.find((candidate) => + candidate.installationId === registration.installationId + && time(candidate.registeredAt) > time(attempt.occurredAt), + ); + return [{ + confidence: attempt.provider === "apns" && attempt.providerInvalidAt ? "high" : "medium", + contributingAttemptIds: [attempt.attemptId], + environment: registration.environment, + inferredAfter: lastSuccess?.occurredAt ?? registration.registeredAt, + inferredBefore: invalidAt, + installationId: registration.installationId, + personId: registration.personId, + pushDeviceTokenId: registration.pushDeviceTokenId, + status: laterRegistration ? "superseded" : "active", + supersededAt: laterRegistration?.registeredAt, + } satisfies UninstallInferenceRecord]; + }) + .filter((inference, index, all) => + all.findIndex((candidate) => candidate.pushDeviceTokenId === inference.pushDeviceTokenId) === index, + ); +}; diff --git a/packages/core/src/services/analyticsIngest/EventCaptureService.ts b/packages/core/src/services/analyticsIngest/EventCaptureService.ts index c17f69850..4f33a681a 100644 --- a/packages/core/src/services/analyticsIngest/EventCaptureService.ts +++ b/packages/core/src/services/analyticsIngest/EventCaptureService.ts @@ -13,6 +13,7 @@ import { CaptureRateLimitedError, CaptureUnauthorizedError, type CaptureEvent, + type CaptureRecordRejectionReason, } from "@voidhash/api-contracts/event-capture"; import { ANONYMOUS_USER_ID_PREFIX } from "@voidhash/lib"; import { Context, Effect, Layer, Schema } from "effect"; @@ -53,8 +54,11 @@ export interface CaptureRequest { } export interface CaptureResult { - readonly accepted: number; - readonly rejected: number; + readonly accepted: ReadonlyArray; + readonly rejected: ReadonlyArray<{ + readonly recordId: string; + readonly reason: CaptureRecordRejectionReason; + }>; } interface ResolvedCaptureProject { @@ -343,12 +347,30 @@ export class EventCaptureService extends Context.Service()( envelope: ReturnType; routeClass: RouteClass; }> = []; - let accepted = 0; - let rejected = 0; + const accepted: Array = []; + const rejected: Array<{ + recordId: string; + reason: CaptureRecordRejectionReason; + }> = []; for (const event of input.events) { + const schemaVersion = event.context.schemaVersion; + if (schemaVersion !== 1) { + rejected.push({ + recordId: event.uuid, + reason: + typeof schemaVersion === "number" + ? "unsupported_schema_version" + : "invalid_context", + }); + continue; + } + if (new TextEncoder().encode(JSON.stringify(event)).byteLength > 256 * 1024) { + rejected.push({ recordId: event.uuid, reason: "payload_too_large" }); + continue; + } if (isReservedRevenueEventName(event.event)) { - rejected += 1; + rejected.push({ recordId: event.uuid, reason: "reserved_event" }); yield* Effect.logWarning( "rejected reserved revenue event from publishable-key capture", { @@ -400,21 +422,21 @@ export class EventCaptureService extends Context.Service()( ); if (outcome._tag === "Failure") { - rejected += 1; + rejected.push({ recordId: event.uuid, reason: "policy_rejected" }); continue; } publishableEvents.push(outcome.success); - accepted += 1; + accepted.push(event.uuid); } yield* ingress.enqueueBatch(publishableEvents); - yield* Effect.annotateCurrentSpan("voidhash.capture.accepted_count", accepted); - yield* Effect.annotateCurrentSpan("voidhash.capture.rejected_count", rejected); + yield* Effect.annotateCurrentSpan("voidhash.capture.accepted_count", accepted.length); + yield* Effect.annotateCurrentSpan("voidhash.capture.rejected_count", rejected.length); yield* Effect.logInfo("capture request processed", { - accepted, + accepted: accepted.length, projectId: project.projectId, - rejected, + rejected: rejected.length, requestId: input.request.requestId, tokenSuffix: tokenSuffix(token), }); diff --git a/packages/core/src/services/measurement/LinkRedirectService.ts b/packages/core/src/services/measurement/LinkRedirectService.ts new file mode 100644 index 000000000..04924a35d --- /dev/null +++ b/packages/core/src/services/measurement/LinkRedirectService.ts @@ -0,0 +1,275 @@ +import { + type CreateLinkRequest, + LinkInvalidRequestError, + LinkUnauthorizedError, + type ResolveDeferredLinkRequest, +} from "@voidhash/api-contracts/links"; +import { + and, + apiKeys, + Db, + eq, + gt, + isNull, + measurementLinkClicks, + measurementLinks, + projects, +} from "@voidhash/db"; +import { Context, Effect, Layer } from "effect"; + +import { validateCaptureToken } from "../analyticsIngest/EventCaptureService.ts"; +import { + canonicalizeMeasurementConfig, + MeasurementConfigSigner, +} from "./MeasurementConfigurationService.ts"; + +type CreateInput = typeof CreateLinkRequest.Type; +type ResolveInput = typeof ResolveDeferredLinkRequest.Type; + +type StoredLinkDefinition = Omit; + +const encodeBase64Url = (bytes: Uint8Array): string => { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +}; + +const tokenPart = (value: unknown): string => + encodeBase64Url(new TextEncoder().encode(canonicalizeMeasurementConfig(value))); + +const hashToken = async (token: string): Promise => { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)); + return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join(""); +}; + +const safeWebUrl = (value: string): URL => { + const url = new URL(value); + const local = new Set(["localhost", "127.0.0.1", "[::1]"]).has(url.hostname); + if ((url.protocol !== "https:" && !(url.protocol === "http:" && local)) || url.username || url.password) { + throw new TypeError("link destinations must use HTTPS"); + } + return url; +}; + +const validateCustomParameters = (parameters: CreateInput["customParameters"]): void => { + const entries = Object.entries(parameters ?? {}); + if (entries.length > 50) throw new TypeError("customParameters cannot contain more than 50 entries"); + let encodedBytes = 0; + for (const [key, value] of entries) { + if (!/^[A-Za-z][A-Za-z\d_.-]{0,63}$/.test(key) || /^(?:token|authorization|password|secret|receipt)$/i.test(key)) { + throw new TypeError(`custom parameter '${key}' is not allowed`); + } + encodedBytes += new TextEncoder().encode(`${key}=${value}`).byteLength; + } + if (encodedBytes > 16_384) throw new TypeError("customParameters exceed the encoded size limit"); +}; + +const appleStoreUrl = (appleAppId: string): string => { + const normalized = appleAppId.replace(/^id/i, ""); + if (!/^\d{5,20}$/.test(normalized)) throw new TypeError("appleAppId must be a numeric App Store ID"); + return `https://apps.apple.com/app/id${normalized}`; +}; + +const linkOrigin = (origin: string, brandedDomain?: string): string => { + if (!brandedDomain) return safeWebUrl(origin).origin; + if (!/^(?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?\.)+[a-z]{2,63}$/i.test(brandedDomain)) { + throw new TypeError("brandedDomain must be a DNS hostname"); + } + return `https://${brandedDomain}`; +}; + +const signedToken = ( + keyId: string, + payload: unknown, + signature: string, +): string => `${keyId}.${tokenPart(payload)}.${signature.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`; + +/** Persistent signed-link creation, click evidence, and one-time deferred correlation. */ +export class LinkRedirectService extends Context.Service()( + "LinkRedirectService", + { + make: Effect.gen(function* () { + const db = yield* Db; + const signer = yield* MeasurementConfigSigner; + + const resolveProject = Effect.fn("LinkRedirectService.resolveProject")(function* (rawToken: string) { + const token = yield* validateCaptureToken(rawToken).pipe( + Effect.mapError(() => new LinkUnauthorizedError({ code: "unauthorized", error: "invalid token" })), + ); + const [project] = yield* db + .select({ projectId: apiKeys.projectId }) + .from(apiKeys) + .innerJoin(projects, eq(projects.id, apiKeys.projectId)) + .where(and(eq(apiKeys.isPublic, true), eq(apiKeys.key, token))) + .limit(1); + if (!project) { + return yield* Effect.fail(new LinkUnauthorizedError({ code: "unauthorized", error: "invalid token" })); + } + return project.projectId; + }); + + const existingCreateResult = Effect.fn("LinkRedirectService.existingCreateResult")(function* ( + projectId: string, + idempotencyKey: string, + origin: string, + brandedDomain?: string, + ) { + const [existing] = yield* db + .select({ + definition: measurementLinks.definition, + expiresAt: measurementLinks.expiresAt, + id: measurementLinks.id, + signedToken: measurementLinks.signedToken, + }) + .from(measurementLinks) + .where(and(eq(measurementLinks.projectId, projectId), eq(measurementLinks.idempotencyKey, idempotencyKey))) + .limit(1); + if (!existing) return undefined; + const definition = existing.definition as unknown as StoredLinkDefinition; + return { + expiresAt: existing.expiresAt, + linkId: existing.id, + url: `${linkOrigin(origin, definition.brandedDomain ?? brandedDomain)}/l/${encodeURIComponent(existing.id)}?token=${encodeURIComponent(existing.signedToken)}`, + }; + }); + + const create = Effect.fn("LinkRedirectService.create")(function* (input: CreateInput, publicOrigin: string) { + const projectId = yield* resolveProject(input.token); + const now = new Date(); + const expiresAt = input.expiresAt ?? new Date(now.getTime() + 30 * 24 * 60 * 60 * 1_000); + if (expiresAt <= now) { + return yield* Effect.fail(new LinkInvalidRequestError({ code: "invalid_link_request", error: "expiresAt must be in the future" })); + } + try { + linkOrigin(publicOrigin, input.brandedDomain); + validateCustomParameters(input.customParameters); + for (const value of [input.destination.androidStoreUrl, input.destination.iosStoreUrl, input.destination.webFallbackUrl, input.referrerImageUrl]) { + if (value) safeWebUrl(value); + } + if (input.destination.appleAppId) appleStoreUrl(input.destination.appleAppId); + } catch (cause) { + return yield* Effect.fail(new LinkInvalidRequestError({ code: "invalid_link_request", error: String(cause) })); + } + if (input.idempotencyKey) { + const existing = yield* existingCreateResult(projectId, input.idempotencyKey, publicOrigin, input.brandedDomain); + if (existing) return existing; + } + const linkId = `link_${crypto.randomUUID()}`; + const unsigned = { expiresAt: expiresAt.toISOString(), issuedAt: now.toISOString(), kind: "link", linkId, projectId } as const; + const signature = yield* signer.sign(new TextEncoder().encode(canonicalizeMeasurementConfig(unsigned))); + const token = signedToken(signer.keyId, unsigned, signature); + const { idempotencyKey: _, token: __, ...definition } = input; + const rows = yield* db.insert(measurementLinks).values({ + definition: definition as unknown as Record, + expiresAt, + id: linkId, + idempotencyKey: input.idempotencyKey, + projectId, + signedToken: token, + }).onConflictDoNothing().returning({ id: measurementLinks.id }); + if (rows.length === 0 && input.idempotencyKey) { + const existing = yield* existingCreateResult(projectId, input.idempotencyKey, publicOrigin, input.brandedDomain); + if (existing) return existing; + } + const origin = linkOrigin(publicOrigin, input.brandedDomain); + return { expiresAt, linkId, url: `${origin}/l/${encodeURIComponent(linkId)}?token=${encodeURIComponent(token)}` }; + }); + + const click = Effect.fn("LinkRedirectService.click")(function* (input: { + readonly clickId: string; + readonly linkId: string; + readonly referer?: string; + readonly token: string; + readonly userAgent: string; + }) { + const [link] = yield* db.select().from(measurementLinks) + .where(and(eq(measurementLinks.id, input.linkId), eq(measurementLinks.signedToken, input.token), gt(measurementLinks.expiresAt, new Date()))) + .limit(1); + if (!link) return undefined; + const definition = link.definition as unknown as StoredLinkDefinition; + const ios = /(?:iphone|ipad|ipod)/i.test(input.userAgent); + const android = /android/i.test(input.userAgent); + let refererOrigin: string | undefined; + try { + if (input.referer) refererOrigin = safeWebUrl(input.referer).origin; + } catch { + refererOrigin = undefined; + } + const occurredAt = new Date(); + const deferredExpiresAt = new Date(Math.min(link.expiresAt.getTime(), occurredAt.getTime() + 24 * 60 * 60 * 1_000)); + const payload = { clickId: input.clickId, expiresAt: deferredExpiresAt.toISOString(), issuedAt: occurredAt.toISOString(), kind: "deferred", linkId: link.id, projectId: link.projectId } as const; + const signature = yield* signer.sign(new TextEncoder().encode(canonicalizeMeasurementConfig(payload))); + const deferredToken = signedToken(signer.keyId, payload, signature); + const deferredTokenHash = yield* Effect.promise(() => hashToken(deferredToken)); + const context = { + platform: ios ? "ios" : android ? "android" : "web", + refererOrigin, + userAgentFamily: ios ? "apple" : android ? "android" : input.userAgent ? "browser" : "unknown", + }; + const rows = yield* db.insert(measurementLinkClicks).values({ + context, + deferredExpiresAt, + deferredTokenHash, + id: input.clickId, + linkId: link.id, + occurredAt, + projectId: link.projectId, + }).onConflictDoNothing().returning({ id: measurementLinkClicks.id }); + if (rows.length === 0) return undefined; + const destination = ios + ? definition.destination.iosStoreUrl ?? (definition.destination.appleAppId + ? appleStoreUrl(definition.destination.appleAppId) + : definition.destination.webFallbackUrl) + : android + ? definition.destination.androidStoreUrl ?? definition.destination.webFallbackUrl + : definition.destination.webFallbackUrl; + if (!destination) return undefined; + const redirect = safeWebUrl(destination); + if (android) redirect.searchParams.set("referrer", deferredToken); + return { deferredToken, destination: redirect.toString() }; + }); + + const resolveDeferred = Effect.fn("LinkRedirectService.resolveDeferred")(function* (input: ResolveInput) { + const projectId = yield* resolveProject(input.token); + const deferredTokenHash = yield* Effect.promise(() => hashToken(input.deferredToken)); + const now = new Date(); + const [claimed] = yield* db.update(measurementLinkClicks) + .set({ consumedAt: now, installationId: input.installationId }) + .where(and( + eq(measurementLinkClicks.projectId, projectId), + eq(measurementLinkClicks.deferredTokenHash, deferredTokenHash), + isNull(measurementLinkClicks.consumedAt), + gt(measurementLinkClicks.deferredExpiresAt, now), + )) + .returning(); + if (!claimed) { + const [existing] = yield* db.select({ consumedAt: measurementLinkClicks.consumedAt, expiresAt: measurementLinkClicks.deferredExpiresAt }) + .from(measurementLinkClicks) + .where(and(eq(measurementLinkClicks.projectId, projectId), eq(measurementLinkClicks.deferredTokenHash, deferredTokenHash))) + .limit(1); + return { reason: existing?.consumedAt ? "replayed" as const : existing && existing.expiresAt <= now ? "expired" as const : "not-found" as const, status: "notFound" as const }; + } + const [link] = yield* db.select().from(measurementLinks) + .where(and(eq(measurementLinks.projectId, projectId), eq(measurementLinks.id, claimed.linkId))) + .limit(1); + if (!link) return { reason: "not-found" as const, status: "notFound" as const }; + const definition = link.definition as unknown as StoredLinkDefinition; + return { + campaign: definition.campaign, + clickId: claimed.id, + clickedAt: claimed.occurredAt, + deferred: true as const, + expiresAt: claimed.deferredExpiresAt, + linkId: link.id, + route: { subvalues: definition.destination.subvalues ?? {}, value: definition.destination.deepLinkValue }, + signature: input.deferredToken, + status: "found" as const, + }; + }); + + return { click, create, resolveDeferred } as const; + }), + }, +) { + static readonly layer = Layer.effect(LinkRedirectService)(LinkRedirectService.make); +} diff --git a/packages/core/src/services/measurement/MeasurementConfigurationService.ts b/packages/core/src/services/measurement/MeasurementConfigurationService.ts new file mode 100644 index 000000000..e81e6d469 --- /dev/null +++ b/packages/core/src/services/measurement/MeasurementConfigurationService.ts @@ -0,0 +1,158 @@ +import { + CaptureUnauthorizedError, + SignedMeasurementConfigurationResponse, +} from "@voidhash/api-contracts/event-capture"; +import { and, apiKeys, Db, eq, projects } from "@voidhash/db"; +import { Context, Effect, Layer, Schema } from "effect"; + +import { validateCaptureToken } from "../analyticsIngest/EventCaptureService.ts"; + +export class MeasurementConfigSigningError extends Schema.TaggedErrorClass( + "MeasurementConfigSigningError", +)("MeasurementConfigSigningError", { cause: Schema.String }) {} + +export interface MeasurementConfigSignerShape { + readonly keyId: string; + readonly version: number; + readonly sign: (bytes: Uint8Array) => Effect.Effect; +} + +export class MeasurementConfigSigner extends Context.Service< + MeasurementConfigSigner, + MeasurementConfigSignerShape +>()("@voidhash/core/MeasurementConfigSigner") {} + +const decodeBase64 = (value: string): Uint8Array => + Uint8Array.from(atob(value), (character) => character.charCodeAt(0)); + +const encodeBase64 = (value: ArrayBuffer): string => { + const bytes = new Uint8Array(value); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary); +}; + +const asArrayBuffer = (value: Uint8Array): ArrayBuffer => + value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer; + +/** Canonicalizes JSON-compatible signed data by recursively sorting object keys. */ +export const canonicalizeMeasurementConfig = (value: unknown): string => { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalizeMeasurementConfig).join(",")}]`; + return `{${Object.entries(value as Record) + .filter(([, nested]) => nested !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map( + ([key, nested]) => + `${JSON.stringify(key)}:${canonicalizeMeasurementConfig(nested)}`, + ) + .join(",")}}`; +}; + +/** Creates an Ed25519 signer from PKCS#8 base64, or an ephemeral development key. */ +export const createMeasurementConfigSigner = async ( + keyId: string, + privateKeyPkcs8?: string, + version = 1, +): Promise => { + if (!Number.isSafeInteger(version) || version < 1) { + throw new RangeError("Measurement configuration version must be a positive safe integer"); + } + const keyPair = privateKeyPkcs8 + ? { + privateKey: await crypto.subtle.importKey( + "pkcs8", + asArrayBuffer(decodeBase64(privateKeyPkcs8)), + "Ed25519", + false, + ["sign"], + ), + publicKey: undefined, + } + : await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + const publicKeySpki = keyPair.publicKey + ? encodeBase64(await crypto.subtle.exportKey("spki", keyPair.publicKey)) + : "configured-separately"; + return { + keyId, + publicKeySpki, + version, + sign: (bytes) => + Effect.tryPromise({ + try: () => crypto.subtle.sign("Ed25519", keyPair.privateKey, asArrayBuffer(bytes)).then(encodeBase64), + catch: (cause) => new MeasurementConfigSigningError({ cause: String(cause) }), + }), + }; +}; + +/** Builds the signing layer used by the measurement configuration endpoint. */ +export const makeMeasurementConfigSignerLayer = ( + keyId: string, + privateKeyPkcs8?: string, + version = 1, +): Layer.Layer => + Layer.effect( + MeasurementConfigSigner, + Effect.tryPromise({ + try: () => createMeasurementConfigSigner(keyId, privateKeyPkcs8, version), + catch: (cause) => new MeasurementConfigSigningError({ cause: String(cause) }), + }), + ); + +/** Resolves project scope and serves a signed, expiring collector configuration. */ +export class MeasurementConfigurationService extends Context.Service()( + "MeasurementConfigurationService", + { + make: Effect.gen(function* () { + const db = yield* Db; + const signer = yield* MeasurementConfigSigner; + + const get = Effect.fn("MeasurementConfigurationService.get")(function* (rawToken: string) { + const token = yield* validateCaptureToken(rawToken); + const [project] = yield* db + .select({ projectId: apiKeys.projectId }) + .from(apiKeys) + .innerJoin(projects, eq(projects.id, apiKeys.projectId)) + .where(and(eq(apiKeys.isPublic, true), eq(apiKeys.key, token))) + .limit(1); + if (!project) { + return yield* Effect.fail( + new CaptureUnauthorizedError({ code: "unauthorized", error: "invalid token" }), + ); + } + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1_000); + const payload = { + collectors: { appleAttributionEnabled: true, linkAllowedDomains: [] }, + conversionRules: [], + schemaVersion: 1 as const, + storage: { + maxOutboxBytes: 20 * 1024 * 1024, + maxOutboxRecords: 10_000, + maxProtectedBytes: 20 * 1024 * 1024, + }, + }; + const unsigned = { + expiresAt: expiresAt.toISOString(), + keyId: signer.keyId, + payload, + projectId: project.projectId, + version: signer.version, + }; + const signature = yield* signer.sign( + new TextEncoder().encode(canonicalizeMeasurementConfig(unsigned)), + ); + return new SignedMeasurementConfigurationResponse({ + ...unsigned, + expiresAt, + signature, + }); + }); + + return { get } as const; + }), + }, +) { + static readonly layer = Layer.effect( + MeasurementConfigurationService, + )(MeasurementConfigurationService.make); +} diff --git a/packages/core/src/services/measurement/MeasurementDeletionService.ts b/packages/core/src/services/measurement/MeasurementDeletionService.ts new file mode 100644 index 000000000..0dd4a75f9 --- /dev/null +++ b/packages/core/src/services/measurement/MeasurementDeletionService.ts @@ -0,0 +1,145 @@ +import { + CaptureInvalidRequestError, + CaptureUnauthorizedError, + type MeasurementDeletionRequest, +} from "@voidhash/api-contracts/event-capture"; +import { + and, + apiKeys, + Db, + eq, + lt, + measurementDeletionRequests, + projects, + protectedMeasurementEvidence, +} from "@voidhash/db"; +import { Context, Effect, Layer } from "effect"; + +import { validateCaptureToken } from "../analyticsIngest/EventCaptureService.ts"; + +export interface MeasurementDeletionResult { + readonly accepted: true; + readonly deletedProtectedEvidence: number; + readonly requestId: string; + readonly status: "completed"; +} + +/** Coordinates idempotent, project-scoped protected measurement deletion. */ +export class MeasurementDeletionService extends Context.Service()( + "MeasurementDeletionService", + { + make: Effect.gen(function* () { + const db = yield* Db; + + const resolveProject = Effect.fn("MeasurementDeletionService.resolveProject")(function* ( + rawToken: string, + ) { + const token = yield* validateCaptureToken(rawToken); + const [project] = yield* db + .select({ projectId: apiKeys.projectId }) + .from(apiKeys) + .innerJoin(projects, eq(projects.id, apiKeys.projectId)) + .where(and(eq(apiKeys.isPublic, true), eq(apiKeys.key, token))) + .limit(1); + if (!project) { + return yield* Effect.fail( + new CaptureUnauthorizedError({ code: "unauthorized", error: "invalid token" }), + ); + } + return project.projectId; + }); + + const request = Effect.fn("MeasurementDeletionService.request")(function* ( + input: typeof MeasurementDeletionRequest.Type, + ) { + const projectId = yield* resolveProject(input.token); + const [existing] = yield* db + .select() + .from(measurementDeletionRequests) + .where( + and( + eq(measurementDeletionRequests.projectId, projectId), + eq(measurementDeletionRequests.requestId, input.requestId), + ), + ) + .limit(1); + if (existing) { + if ( + existing.installationId !== input.installationId || + (existing.personId ?? undefined) !== input.personId + ) { + return yield* Effect.fail( + new CaptureInvalidRequestError({ + code: "invalid_request", + error: "requestId is already bound to another deletion subject", + }), + ); + } + return { + accepted: true, + deletedProtectedEvidence: existing.deletedProtectedEvidence, + requestId: existing.requestId, + status: "completed", + } as const; + } + + const completedAt = new Date(); + const deleted = yield* db.transaction((tx) => + Effect.gen(function* () { + const purged = yield* tx + .update(protectedMeasurementEvidence) + .set({ ciphertext: null, deletionState: "deleted", updatedAt: completedAt }) + .where( + and( + eq(protectedMeasurementEvidence.projectId, projectId), + eq(protectedMeasurementEvidence.installationId, input.installationId), + ), + ) + .returning({ id: protectedMeasurementEvidence.id }); + yield* tx.insert(measurementDeletionRequests).values({ + completedAt, + deletedProtectedEvidence: purged.length, + id: `measurement_deletion_${crypto.randomUUID()}`, + installationId: input.installationId, + personId: input.personId, + projectId, + requestedAt: input.requestedAt, + requestId: input.requestId, + status: "completed", + }); + return purged.length; + }), + ); + return { + accepted: true, + deletedProtectedEvidence: deleted, + requestId: input.requestId, + status: "completed", + } as const; + }); + + const purgeExpiredEphemeral = Effect.fn( + "MeasurementDeletionService.purgeExpiredEphemeral", + )(function* (cutoff: Date) { + const purged = yield* db + .update(protectedMeasurementEvidence) + .set({ ciphertext: null, deletionState: "deleted", updatedAt: new Date() }) + .where( + and( + eq(protectedMeasurementEvidence.retentionClass, "ephemeral"), + eq(protectedMeasurementEvidence.deletionState, "active"), + lt(protectedMeasurementEvidence.createdAt, cutoff), + ), + ) + .returning({ id: protectedMeasurementEvidence.id }); + return purged.length; + }); + + return { purgeExpiredEphemeral, request } as const; + }), + }, +) { + static readonly layer: Layer.Layer = Layer.effect( + MeasurementDeletionService, + )(MeasurementDeletionService.make); +} diff --git a/packages/core/src/services/measurement/ProtectedEvidenceService.ts b/packages/core/src/services/measurement/ProtectedEvidenceService.ts new file mode 100644 index 000000000..58c23bdfe --- /dev/null +++ b/packages/core/src/services/measurement/ProtectedEvidenceService.ts @@ -0,0 +1,175 @@ +import { + CapturePayloadTooLargeError, + CaptureInvalidRequestError, + CaptureUnauthorizedError, + ProtectedEvidenceConflictError, + type ProtectedEvidenceRequest, +} from "@voidhash/api-contracts/event-capture"; +import { + and, + apiKeys, + Db, + eq, + measurementDeletionRequests, + projects, + protectedMeasurementEvidence, +} from "@voidhash/db"; +import { Context, Effect, Layer, Schema } from "effect"; + +import { validateCaptureToken } from "../analyticsIngest/EventCaptureService.ts"; + +const MAX_PROTECTED_EVIDENCE_BYTES = 512 * 1024; + +export class ProtectedEvidenceServiceError extends Schema.TaggedErrorClass( + "ProtectedEvidenceServiceError", +)("ProtectedEvidenceServiceError", { + cause: Schema.String, + message: Schema.String, +}) {} + +export interface PutProtectedEvidenceResult { + readonly accepted: true; + readonly blobId: string; +} + +/** Strictly decodes canonical base64 without accepting ignored characters. */ +export const decodeProtectedCiphertext = ( + ciphertext: string, +): Effect.Effect => + Effect.gen(function* () { + if (!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(ciphertext)) { + return yield* Effect.fail( + new CaptureInvalidRequestError({ + code: "invalid_request", + error: "ciphertext must be canonical base64", + }), + ); + } + const estimatedBytes = Math.floor((ciphertext.length * 3) / 4) - (ciphertext.endsWith("==") ? 2 : ciphertext.endsWith("=") ? 1 : 0); + if (estimatedBytes > MAX_PROTECTED_EVIDENCE_BYTES) { + return yield* Effect.fail( + new CapturePayloadTooLargeError({ + code: "payload_too_large", + error: "protected evidence exceeds the per-blob bound", + }), + ); + } + return yield* Effect.try({ + try: () => Uint8Array.from(atob(ciphertext), (value) => value.charCodeAt(0)), + catch: (cause) => + new CaptureInvalidRequestError({ + code: "invalid_request", + error: `ciphertext must be canonical base64: ${String(cause)}`, + }), + }); + }); + +const equalBytes = (left: Uint8Array, right: Uint8Array): boolean => { + if (left.byteLength !== right.byteLength) return false; + let difference = 0; + for (let index = 0; index < left.byteLength; index += 1) { + difference |= (left[index] ?? 0) ^ (right[index] ?? 0); + } + return difference === 0; +}; + +export class ProtectedEvidenceService extends Context.Service()( + "ProtectedEvidenceService", + { + make: Effect.gen(function* () { + const db = yield* Db; + + const put = Effect.fn("ProtectedEvidenceService.put")(function* ( + input: typeof ProtectedEvidenceRequest.Type, + ) { + const token = yield* validateCaptureToken(input.token); + const ciphertext = yield* decodeProtectedCiphertext(input.ciphertext); + const [project] = yield* db + .select({ projectId: apiKeys.projectId }) + .from(apiKeys) + .innerJoin(projects, eq(projects.id, apiKeys.projectId)) + .where(and(eq(apiKeys.isPublic, true), eq(apiKeys.key, token))) + .limit(1); + if (!project) { + return yield* Effect.fail( + new CaptureUnauthorizedError({ code: "unauthorized", error: "invalid token" }), + ); + } + + const [deletion] = yield* db + .select({ id: measurementDeletionRequests.id }) + .from(measurementDeletionRequests) + .where( + and( + eq(measurementDeletionRequests.projectId, project.projectId), + eq(measurementDeletionRequests.installationId, input.installationId), + eq(measurementDeletionRequests.status, "completed"), + ), + ) + .limit(1); + if (deletion) { + return yield* Effect.fail( + new CaptureInvalidRequestError({ + code: "invalid_request", + error: "protected evidence cannot be recreated for a deleted installation", + }), + ); + } + + const [existing] = yield* db + .select() + .from(protectedMeasurementEvidence) + .where( + and( + eq(protectedMeasurementEvidence.projectId, project.projectId), + eq(protectedMeasurementEvidence.blobId, input.blobId), + ), + ) + .limit(1); + + if (existing) { + const metadataMatches = + existing.purpose === input.purpose && + existing.consentRevision === input.consentRevision && + existing.retentionClass === input.retentionClass && + existing.encryptionKeyVersion === input.encryptionKeyVersion && + existing.deletionState === input.deletionState; + if ( + !metadataMatches || + existing.installationId !== input.installationId || + existing.ciphertext === null || + !equalBytes(existing.ciphertext, ciphertext) + ) { + return yield* Effect.fail( + new ProtectedEvidenceConflictError({ + code: "protected_evidence_conflict", + error: "blobId already exists with different protected evidence", + }), + ); + } + return { accepted: true, blobId: input.blobId } as const; + } + + yield* db.insert(protectedMeasurementEvidence).values({ + blobId: input.blobId, + ciphertext: Buffer.from(ciphertext), + consentRevision: input.consentRevision, + deletionState: input.deletionState, + encryptionKeyVersion: input.encryptionKeyVersion, + id: `protected_${crypto.randomUUID()}`, + installationId: input.installationId, + projectId: project.projectId, + purpose: input.purpose, + retentionClass: input.retentionClass, + }); + return { accepted: true, blobId: input.blobId } as const; + }); + + return { put } as const; + }), + }, +) { + static readonly layer: Layer.Layer = Layer.effect( + ProtectedEvidenceService, + )(ProtectedEvidenceService.make); +} diff --git a/packages/core/src/services/measurement/index.ts b/packages/core/src/services/measurement/index.ts new file mode 100644 index 000000000..04629fb1b --- /dev/null +++ b/packages/core/src/services/measurement/index.ts @@ -0,0 +1,4 @@ +export * from "./MeasurementDeletionService.ts"; +export * from "./MeasurementConfigurationService.ts"; +export * from "./LinkRedirectService.ts"; +export * from "./ProtectedEvidenceService.ts"; diff --git a/packages/core/test/domain/measurement/ApplePostback.test.ts b/packages/core/test/domain/measurement/ApplePostback.test.ts new file mode 100644 index 000000000..6ef12c37a --- /dev/null +++ b/packages/core/test/domain/measurement/ApplePostback.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it, vi } from "vitest"; + +import { applePostbackCohortKey, decodeAppleConversion, parseApplePostback } from "../../../src/domain/measurement/ApplePostback"; + +const encode = (value: unknown) => new TextEncoder().encode(JSON.stringify(value)); + +describe("Apple postback ingest domain", () => { + it.each([ + ["skan", { "app-id": "123", "coarse-conversion-value": "high", "conversion-value": 7, "postback-sequence-index": 2, "source-identifier": "0042", version: "4.0" }], + ["ad-attribution-kit", { advertisedItemId: "123", fineConversionValue: 4, postbackSequenceIndex: 0, publisherItemId: "publisher", version: "1.0" }], + ] as const)("normalizes %s fixtures", async (framework, body) => { + const result = await parseApplePostback({ body: encode(body), evidenceId: "evidence-1", framework, receivedAt: "2026-07-20T00:00:00.000Z" }); + expect(result.normalized).toMatchObject({ appId: "123", framework }); + expect(result.rawBody).toEqual(encode(body)); + }); + + it("records verification failure without discarding raw evidence", async () => { + const body = encode({ "app-id": "123", signature: "tampered", version: "4.0" }); + const verify = vi.fn(() => false); + const result = await parseApplePostback({ body, evidenceId: "evidence-1", framework: "skan", receivedAt: "2026-07-20T00:00:00.000Z", verifySignature: verify }); + expect(result).toMatchObject({ verification: "failed" }); + expect(result.rawBody).toEqual(body); + expect(verify).toHaveBeenCalledOnce(); + }); + + it.each([ + [encode("not-an-object"), "invalid-shape"], + [new TextEncoder().encode("{"), "invalid-json"], + [new Uint8Array(100), "oversized"], + ])("retains rejected input with typed reason", async (body, reason) => { + const result = await parseApplePostback({ + body, + evidenceId: "evidence-1", + framework: "skan", + maximumBytes: reason === "oversized" ? 10 : undefined, + receivedAt: "2026-07-20T00:00:00.000Z", + }); + expect(result.rejectionReason).toBe(reason); + expect(result.rawBody).toEqual(body); + }); + + it("correlates the active rule version and decodes fine or coarse meaning", () => { + const rules = [{ activeFrom: "2026-07-01T00:00:00.000Z", appId: "123", coarse: { high: "payer" }, fine: { 7: "trial-start" }, ruleVersion: "rules-2" }]; + const postback = { appId: "123", fineConversionValue: 7, framework: "skan" as const, rawVersion: "4.0" }; + expect(decodeAppleConversion(postback, "2026-07-20T00:00:00.000Z", rules)).toEqual({ meaning: "trial-start", ruleVersion: "rules-2", status: "decoded" }); + expect(decodeAppleConversion({ ...postback, appId: "unknown" }, "2026-07-20T00:00:00.000Z", rules)).toEqual({ status: "unknown-rule" }); + }); + + it("creates an anonymous cohort key with no individual identity dimension", () => { + const key = applePostbackCohortKey({ appId: "123", framework: "skan", postbackSequenceIndex: 1, rawVersion: "4.0" }, "campaign-1", "rules-2"); + expect(key).toBe("123:campaign-1:rules-2:1"); + expect(key).not.toMatch(/installation|person|device/); + }); +}); diff --git a/packages/core/test/domain/measurement/Attribution.test.ts b/packages/core/test/domain/measurement/Attribution.test.ts new file mode 100644 index 000000000..179807082 --- /dev/null +++ b/packages/core/test/domain/measurement/Attribution.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; + +import { + evaluateAttribution, + recomputeAttribution, + toSafeAttributionResponse, + type AttributionRuleSet, + type AttributionTouchpoint, +} from "../../../src/domain/measurement/Attribution"; + +const rules: AttributionRuleSet = { + lookbackMs: 7 * 24 * 60 * 60 * 1_000, + modelVersion: "last-touch-v1", + priorities: ["install-referrer", "deferred-token", "cohort", "push-open", "deep-link"], + ruleVersion: "rules-1", +}; + +const touch = ( + evidenceId: string, + kind: AttributionTouchpoint["kind"], + occurredAt = "2026-07-19T00:00:00.000Z", + extra: Partial = {}, +): AttributionTouchpoint => ({ + campaign: { campaign: evidenceId, mediaSource: kind }, + deterministic: kind !== "cohort", + evidenceId, + kind, + occurredAt, + ...extra, +}); + +const evaluate = (touchpoints: ReadonlyArray, overrides = {}) => evaluateAttribution({ + decisionId: "decision-1", + decisionTime: "2026-07-20T00:00:01.000Z", + kind: "install", + ruleSet: rules, + subjectOccurredAt: "2026-07-20T00:00:00.000Z", + touchpoints, + ...overrides, +}); + +describe("attribution engine", () => { + it("applies configured priority and stable tie breaking", () => { + expect(evaluate([touch("cohort", "cohort"), touch("deferred", "deferred-token"), touch("referrer", "install-referrer")]).campaign?.campaign).toBe("referrer"); + expect(evaluate([touch("b", "deferred-token"), touch("a", "deferred-token")]).campaign?.campaign).toBe("a"); + }); + + it("excludes evidence outside lookback and emits an explicit organic decision", () => { + const decision = evaluate([touch("old", "install-referrer", "2026-01-01T00:00:00.000Z")]); + expect(decision).toMatchObject({ confidence: "organic", evidenceIds: [], kind: "organic" }); + expect(decision.reasonTrace).toEqual([{ evidenceId: "old", outcome: "outside-lookback", rule: "install-referrer" }]); + }); + + it("attributes push and deep-link re-engagement without creating an install decision", () => { + const decision = evaluate([ + touch("push", "push-open", undefined, { reengagement: true }), + touch("link", "deep-link", undefined, { reengagement: true }), + ], { kind: "reengagement" }); + expect(decision).toMatchObject({ kind: "reengagement", campaign: { campaign: "push" } }); + }); + + it("is byte deterministic apart from injected decision identity and time", () => { + const first = evaluate([touch("cohort", "cohort")]); + const second = evaluate([touch("cohort", "cohort")], { decisionId: "decision-2", decisionTime: "2026-07-21T00:00:00.000Z" }); + const strip = ({ decisionId: _id, decidedAt: _time, ...value }: typeof first) => value; + expect(JSON.stringify(strip(first))).toBe(JSON.stringify(strip(second))); + }); + + it("appends a new version and preserves source evidence", () => { + const evidence = [touch("cohort", "cohort")]; + const checksum = JSON.stringify(evidence); + const previous = evaluate(evidence); + const recomputed = recomputeAttribution(previous, { + decisionId: "decision-2", + decisionTime: "2026-07-21T00:00:00.000Z", + kind: "install", + ruleSet: { ...rules, priorities: ["cohort"], ruleVersion: "rules-2" }, + subjectOccurredAt: "2026-07-20T00:00:00.000Z", + touchpoints: evidence, + }); + expect(recomputed.previous).toMatchObject({ decisionId: "decision-1", status: "superseded" }); + expect(recomputed.current).toMatchObject({ ruleVersion: "rules-2", supersededDecisionId: "decision-1" }); + expect(JSON.stringify(evidence)).toBe(checksum); + }); + + it("exposes only safe campaign and decision fields", () => { + const serialized = JSON.stringify(toSafeAttributionResponse(evaluate([touch("referrer", "install-referrer")]))); + expect(serialized).not.toMatch(/url|token|referrerString|identifier/i); + }); +}); diff --git a/packages/core/test/domain/measurement/FraudReporting.test.ts b/packages/core/test/domain/measurement/FraudReporting.test.ts new file mode 100644 index 000000000..be6f9b8c7 --- /dev/null +++ b/packages/core/test/domain/measurement/FraudReporting.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { aggregateMeasurementReport, buildMeasurementRawExport, evaluateFraud, type FraudEvidence } from "../../../src/domain/measurement/FraudReporting"; + +const rules = { + maximumClicksPerWindow: 2, + maximumInstallDelayMs: 7 * 24 * 60 * 60 * 1_000, + minimumInstallDelayMs: 1_000, + ruleVersion: "fraud-1", + windowMs: 60_000, +}; + +const evidence: FraudEvidence[] = [ + { clickId: "click-1", evidenceId: "click-a", kind: "click", occurredAt: "2026-07-20T00:00:00.000Z" }, + { clickId: "click-2", evidenceId: "click-b", kind: "click", occurredAt: "2026-07-20T00:00:10.000Z" }, + { clickId: "click-3", evidenceId: "click-c", kind: "click", occurredAt: "2026-07-20T00:00:20.000Z" }, + { clickId: "click-1", evidenceId: "install-a", installReferrerClickId: "different", kind: "install", occurredAt: "2026-07-20T00:00:00.100Z" }, + { evidenceId: "replay-a", kind: "token-replay", occurredAt: "2026-07-20T00:00:30.000Z" }, +]; + +describe("fraud and reporting", () => { + it("flags every shipped heuristic deterministically without mutating evidence", () => { + const checksum = JSON.stringify(evidence); + const flags = evaluateFraud(evidence, rules); + expect(flags.map(({ reason }) => reason).sort()).toEqual([ + "click-flooding", + "click-to-install-anomaly", + "referrer-click-mismatch", + "token-replay", + ]); + expect(evaluateFraud([...evidence].reverse(), rules)).toEqual(flags); + expect(JSON.stringify(evidence)).toBe(checksum); + expect(flags.every(({ severity }) => severity === "block")).toBe(true); + }); + + it("exports all non-deleted layers while recursively removing protected fields", () => { + const rows = [ + { payload: { campaign: "summer", nested: { ciphertext: "secret", safe: true }, rawUrl: "private" }, recordId: "raw-1", schemaVersion: 1, type: "click" }, + { payload: { campaign: "summer" }, recordId: "derived-1", schemaVersion: 1, type: "touchpoint" }, + { payload: { campaign: "winter" }, recordId: "decision-1", schemaVersion: 1, type: "decision" }, + { deleted: true, payload: { campaign: "deleted" }, recordId: "deleted-1", schemaVersion: 1, type: "click" }, + ]; + const exported = buildMeasurementRawExport(rows); + expect(exported.map(({ recordId }) => recordId)).toEqual(["raw-1", "derived-1", "decision-1"]); + expect(JSON.stringify(exported)).not.toContain("secret"); + expect(JSON.stringify(exported)).not.toContain("private"); + expect(aggregateMeasurementReport(rows)).toEqual({ + byCampaign: { summer: 2, winter: 1 }, + byType: { click: 1, decision: 1, touchpoint: 1 }, + total: 3, + }); + }); +}); diff --git a/packages/core/test/domain/measurement/LinkRedirect.test.ts b/packages/core/test/domain/measurement/LinkRedirect.test.ts new file mode 100644 index 000000000..a3b71826e --- /dev/null +++ b/packages/core/test/domain/measurement/LinkRedirect.test.ts @@ -0,0 +1,142 @@ +import { + createLinkSigningKey, + LinkRedirectEngine, + verifyLinkPayload, +} from "../../../src/domain/measurement/LinkRedirect.ts"; +import { describe, expect, it } from "vitest"; + +const definition = { + androidStoreUrl: "https://play.google.com/store/apps/details?id=com.example", + appleAppId: "123456789", + baseDeepLink: "https://example.com/open", + brandedDomain: "links.example", + campaign: { campaign: "winter", mediaSource: "owned" }, + createdAt: "2026-01-01T00:00:00.000Z", + customParameters: { coupon: "winter-25" }, + expiresAt: "2026-01-03T00:00:00.000Z", + iosStoreUrl: "https://apps.apple.com/app/id123456789", + linkId: "link-1", + projectId: "project-1", + referrerCustomerId: "customer-1", + referrerImageUrl: "https://example.com/referrer.png", + referrerName: "Example customer", + referrerUid: "referrer-1", + route: { subvalues: { 1: "annual" }, value: "checkout" }, + templateId: "invite-template", + webFallbackUrl: "https://example.com/download", +} as const; + +describe("LinkRedirectEngine", () => { + it("signs, records before redirect, stamps Android referrer, and resolves once", async () => { + const key = await createLinkSigningKey("key-1"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => new Date("2026-01-01T01:00:00.000Z")); + const created = await engine.create(definition, "https://links.example"); + expect(created.url).toContain("/l/link-1?token="); + const clicked = await engine.click({ + clickId: "click-1", + linkId: "link-1", + referer: "https://publisher.example/private/path?secret=yes", + token: created.token, + userAgent: "Mozilla/5.0 (Linux; Android 15)", + }); + expect(engine.evidence()).toEqual([expect.objectContaining({ + clickId: "click-1", + context: expect.objectContaining({ refererOrigin: "https://publisher.example" }), + })]); + expect(new URL(clicked?.destination ?? "").searchParams.get("referrer")).toBe(clicked?.deferredToken); + await expect(engine.resolveDeferred("project-1", clicked?.deferredToken ?? "")).resolves.toMatchObject({ + clickId: "click-1", + route: definition.route, + status: "found", + }); + await expect(engine.resolveDeferred("project-1", clicked?.deferredToken ?? "")).resolves.toEqual({ + reason: "replayed", + status: "notFound", + }); + }); + + it("rejects tampering, wrong project, unknown key, expiry, and destination injection", async () => { + const key = await createLinkSigningKey("key-1"); + const now = new Date("2026-01-01T01:00:00.000Z"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => now); + const created = await engine.create(definition, "https://links.example"); + const [tamperedKeyId, tamperedPayload, tamperedSignature] = created.token.split("."); + const tamperedPayloadBytes = `${tamperedPayload?.startsWith("a") ? "b" : "a"}${tamperedPayload?.slice(1)}`; + const tampered = `${tamperedKeyId}.${tamperedPayloadBytes}.${tamperedSignature}`; + await expect(engine.click({ clickId: "c", linkId: "link-1", token: tampered, userAgent: "" })).resolves.toBeUndefined(); + const [keyId, payload, signature] = created.token.split("."); + await expect(verifyLinkPayload({ + expectedKind: "link", + expectedProjectId: "wrong-project", + keys: new Map([[key.keyId, key]]), + now, + token: created.token, + })).resolves.toBeUndefined(); + await expect(verifyLinkPayload({ + expectedKind: "link", + expectedProjectId: "project-1", + keys: new Map(), + now, + token: `${keyId}.${payload}.${signature}`, + })).resolves.toBeUndefined(); + const expired = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => new Date("2026-01-04T00:00:00.000Z")); + await expect(expired.create(definition, "javascript:alert(1)")).rejects.toThrow("unsafe"); + }); + + it("cannot be turned into an open redirect by request context", async () => { + const key = await createLinkSigningKey("key-1"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => new Date("2026-01-01T01:00:00.000Z")); + const created = await engine.create(definition, "https://links.example"); + const clicked = await engine.click({ + clickId: "click-safe", + linkId: "link-1", + referer: "https://evil.example/?redirect=https://evil.example", + token: created.token, + userAgent: "iPhone redirect=https://evil.example", + }); + expect(new URL(clicked?.destination ?? "").origin).toBe("https://apps.apple.com"); + }); + + it("retains every generator field and derives the iOS store destination from Apple app ID", async () => { + const key = await createLinkSigningKey("key-1"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => new Date("2026-01-01T01:00:00.000Z")); + const appleIdOnly = { ...definition, iosStoreUrl: undefined }; + const created = await engine.create(appleIdOnly, "https://links.example"); + const clicked = await engine.click({ + clickId: "apple-id-click", + linkId: appleIdOnly.linkId, + token: created.token, + userAgent: "iPhone", + }); + expect(clicked?.destination).toBe("https://apps.apple.com/app/id123456789"); + expect(engine.definition(appleIdOnly.linkId)).toEqual(appleIdOnly); + }); + + it("rejects malformed custom parameter keys and Apple app IDs", async () => { + const key = await createLinkSigningKey("key-1"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]])); + await expect(engine.create({ ...definition, customParameters: { "bad key": "value" } }, "https://links.example")) + .rejects.toThrow("custom parameter"); + await expect(engine.create({ ...definition, appleAppId: "javascript:alert(1)" }, "https://links.example")) + .rejects.toThrow("Apple app ID"); + }); + + it("rejects insecure destinations, reports expired deferred tokens, and keeps click evidence immutable", async () => { + const key = await createLinkSigningKey("key-1"); + let now = new Date("2026-01-01T01:00:00.000Z"); + const engine = new LinkRedirectEngine(key, new Map([[key.keyId, key]]), () => now); + await expect(engine.create({ ...definition, webFallbackUrl: "http://example.com" }, "https://links.example")) + .rejects.toThrow("unsafe"); + const created = await engine.create(definition, "https://links.example"); + const clicked = await engine.click({ clickId: "immutable-click", linkId: definition.linkId, token: created.token, userAgent: "Android" }); + await expect(engine.click({ clickId: "immutable-click", linkId: definition.linkId, token: created.token, userAgent: "iPhone" })) + .resolves.toBeUndefined(); + expect(engine.evidence()).toHaveLength(1); + expect(engine.evidence()[0]?.context.platform).toBe("android"); + now = new Date("2026-01-03T00:00:00.000Z"); + await expect(engine.resolveDeferred(definition.projectId, clicked?.deferredToken ?? "")).resolves.toEqual({ + reason: "expired", + status: "notFound", + }); + }); +}); diff --git a/packages/core/test/domain/measurement/PartnerPostback.test.ts b/packages/core/test/domain/measurement/PartnerPostback.test.ts new file mode 100644 index 000000000..64e93a10a --- /dev/null +++ b/packages/core/test/domain/measurement/PartnerPostback.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; + +import { partnerPostbackRetryDelay, planPartnerPostback, type PartnerCatalogEntry } from "../../../src/domain/measurement/PartnerPostback"; + +const catalog: PartnerCatalogEntry = { + endpoint: "https://partner.example/postback", + fieldMapping: { + campaign: "campaign", + distinctId: "distinctId", + email: "email", + ignored: "notMappedByConsumer", + "partnerData.account": "account", + }, + partner: "partner-a", + requiredFields: ["campaign"], +}; + +const policy = { + anonymize: false, + consentRevision: 3, + deleted: false, + partnerSharing: true, +} as const; + +describe("partner postback planning", () => { + it("maps only catalog fields and filters protected values", () => { + const plan = planPartnerPostback(catalog, "decision-1", { + campaign: "summer", + distinctId: "person-1", + email: "person@example.test", + rawUrl: "https://private.example", + surprise: "must-not-leak", + }, { account: "owned" }, policy); + expect(plan.request?.payload).toEqual({ + account: "owned", + campaign: "summer", + distinctId: "person-1", + }); + expect(JSON.stringify(plan.request)).not.toContain("person@example.test"); + expect(JSON.stringify(plan.request)).not.toContain("must-not-leak"); + expect(plan.audit.filteredFields).toContain("email"); + }); + + it.each([ + [{ ...policy, partnerSharing: false }, "partner-sharing-opt-out"], + [{ ...policy, excludedPartners: ["partner-a"] }, "partner-excluded"], + [{ ...policy, deleted: true }, "subject-deleted"], + ])("suppresses before constructing a request", (current, reason) => { + const plan = planPartnerPostback(catalog, "decision-1", { campaign: "summer" }, {}, current); + expect(plan.request).toBeUndefined(); + expect(plan.audit).toMatchObject({ consentRevision: 3, reason, result: "suppressed" }); + }); + + it("evaluates policy at send time and anonymizes identity", () => { + const captured = { campaign: "summer", distinctId: "person-1" }; + expect(planPartnerPostback(catalog, "decision-1", captured, {}, policy).request).toBeDefined(); + const later = planPartnerPostback(catalog, "decision-1", captured, {}, { ...policy, anonymize: true, consentRevision: 4 }); + expect(later.request?.payload.distinctId).toBe("anonymous:partner-a:decision-1"); + expect(later.audit.consentRevision).toBe(4); + }); + + it("uses stable idempotency and bounded Retry-After-aware backoff", () => { + const first = planPartnerPostback(catalog, "decision-1", { campaign: "summer" }, {}, policy); + const replay = planPartnerPostback(catalog, "decision-1", { campaign: "summer" }, {}, policy); + expect(first.idempotencyKey).toBe(replay.idempotencyKey); + expect(partnerPostbackRetryDelay(2, 10_000)).toBe(10_000); + expect(partnerPostbackRetryDelay(8, undefined)).toBeUndefined(); + }); +}); diff --git a/packages/core/test/domain/measurement/StoreLifecycle.test.ts b/packages/core/test/domain/measurement/StoreLifecycle.test.ts new file mode 100644 index 000000000..5bee2b7b4 --- /dev/null +++ b/packages/core/test/domain/measurement/StoreLifecycle.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { correlateStoreNotification, normalizeStoreLifecycleState, projectStoreNotification, type StoreNotificationType } from "../../../src/domain/measurement/StoreLifecycle"; + +const purchases = [ + { accountToken: "account-1", environment: "production" as const, installationId: "install-account", originalTransactionId: "original-1", personId: "person-current", purchaseToken: "purchase-1", transactionId: "transaction-1" }, + { accountToken: "account-sandbox", environment: "sandbox" as const, installationId: "install-sandbox", originalTransactionId: "original-sandbox" }, +]; + +const notification = (overrides = {}) => ({ + environment: "production" as const, + notificationId: "notification-1", + provider: "apple" as const, + type: "renewed" as const, + ...overrides, +}); + +describe("store notification correlation", () => { + it("uses account, lineage, then transaction precedence", () => { + expect(correlateStoreNotification(notification({ accountToken: "account-1", originalTransactionId: "wrong" }), purchases)).toMatchObject({ key: "account-token", installationId: "install-account" }); + expect(correlateStoreNotification(notification({ originalTransactionId: "original-1" }), purchases)).toMatchObject({ key: "lineage" }); + expect(correlateStoreNotification(notification({ transactionId: "transaction-1" }), purchases)).toMatchObject({ key: "transaction" }); + expect(correlateStoreNotification(notification({ transactionId: "missing" }), purchases)).toEqual({ status: "unmatched", reason: "purchase-evidence-not-found" }); + }); + + it.each([ + ["purchased", "active"], ["renewed", "renewed"], ["canceled", "canceled"], + ["grace-period", "grace"], ["billing-retry", "billing-retry"], ["paused", "paused"], + ["resumed", "resumed"], ["replaced", "replaced"], ["refunded", "refunded"], + ["revoked", "revoked"], ["expired", "expired"], ["prepaid-top-up", "prepaid"], + ] as const)("normalizes %s", (input, expected) => { + expect(normalizeStoreLifecycleState(input as StoreNotificationType)).toBe(expected); + }); + + it("parks an early notification and converges on replay without duplicates", () => { + const current = notification({ accountToken: "account-1" }); + expect(projectStoreNotification(current, [], new Set())).toEqual({ status: "parked" }); + const projected = projectStoreNotification(current, purchases, new Set()); + expect(projected).toMatchObject({ status: "projected", projection: { personId: "person-current", source: "server-correlation", state: "renewed" } }); + expect(projectStoreNotification(current, purchases, new Set(["notification-1"]))).toEqual({ status: "duplicate" }); + expect(JSON.stringify(projected)).not.toMatch(/receipt|purchaseToken|accountToken/); + }); + + it("never merges sandbox notification state into production", () => { + expect(correlateStoreNotification(notification({ accountToken: "account-sandbox" }), purchases)).toEqual({ + status: "unmatched", + reason: "environment-mismatch", + }); + }); +}); diff --git a/packages/core/test/domain/measurement/UninstallInference.test.ts b/packages/core/test/domain/measurement/UninstallInference.test.ts new file mode 100644 index 000000000..14f9271b4 --- /dev/null +++ b/packages/core/test/domain/measurement/UninstallInference.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; + +import { inferUninstalls, type PushDeliveryAttemptEvidence, type PushRegistrationEvidence } from "../../../src/domain/measurement/UninstallInference"; + +const registration = (overrides: Partial = {}): PushRegistrationEvidence => ({ + environment: "production", + installationId: "install-1", + personId: "person-at-failure", + pushDeviceTokenId: "token-id-1", + registeredAt: "2026-07-01T00:00:00.000Z", + ...overrides, +}); + +const attempt = (overrides: Partial = {}): PushDeliveryAttemptEvidence => ({ + attemptId: "attempt-invalid", + occurredAt: "2026-07-20T00:00:00.000Z", + provider: "apns", + providerInvalidAt: "2026-07-19T00:00:00.000Z", + pushDeviceTokenId: "token-id-1", + result: "unregistered", + ...overrides, +}); + +describe("uninstall inference", () => { + it("infers a bounded window for the latest active token", () => { + expect(inferUninstalls([registration()], [ + attempt({ attemptId: "success", occurredAt: "2026-07-10T00:00:00.000Z", providerInvalidAt: undefined, result: "success" }), + attempt(), + ])).toEqual([expect.objectContaining({ + confidence: "high", + inferredAfter: "2026-07-10T00:00:00.000Z", + inferredBefore: "2026-07-19T00:00:00.000Z", + pushDeviceTokenId: "token-id-1", + status: "active", + })]); + }); + + it("ignores rotated, explicitly unregistered, stale, and transient feedback", () => { + expect(inferUninstalls([ + registration({ pushDeviceTokenId: "old" }), + registration({ previousPushDeviceTokenId: "old", pushDeviceTokenId: "new", registeredAt: "2026-07-15T00:00:00.000Z" }), + ], [attempt({ pushDeviceTokenId: "old" })])).toEqual([]); + expect(inferUninstalls([registration({ unregisteredAt: "2026-07-18T00:00:00.000Z" })], [attempt()])).toEqual([]); + expect(inferUninstalls([registration()], [attempt({ providerInvalidAt: "2026-06-01T00:00:00.000Z" })])).toEqual([]); + expect(inferUninstalls([registration()], [attempt({ result: "server-error" })])).toEqual([]); + }); + + it("keeps environment and identity-at-failure without exposing a platform token", () => { + const inference = inferUninstalls([registration({ environment: "development" })], [attempt()])[0]; + expect(inference).toMatchObject({ environment: "development", personId: "person-at-failure" }); + expect(JSON.stringify(inference)).not.toMatch(/rawToken|platformToken|apnsToken|fcmToken/); + }); + + it("is deterministic and de-duplicates repeated invalid feedback", () => { + const evidence = [attempt(), attempt({ attemptId: "attempt-invalid-2", occurredAt: "2026-07-21T00:00:00.000Z" })]; + expect(inferUninstalls([registration()], evidence)).toEqual(inferUninstalls([registration()], [...evidence].reverse())); + expect(inferUninstalls([registration()], evidence)).toHaveLength(1); + }); +}); diff --git a/packages/core/test/services/analyticsIngest/EventCaptureService.integration.test.ts b/packages/core/test/services/analyticsIngest/EventCaptureService.integration.test.ts index 23ce7f311..e2538e8fd 100644 --- a/packages/core/test/services/analyticsIngest/EventCaptureService.integration.test.ts +++ b/packages/core/test/services/analyticsIngest/EventCaptureService.integration.test.ts @@ -129,7 +129,7 @@ const captureEvent = ( ): typeof CaptureEvent.Type => ({ uuid: uniqueId("evt"), event: "page_view", - context: {}, + context: { schemaVersion: 1 }, properties: {}, distinct_id: "user-1", ...overrides, @@ -258,8 +258,8 @@ describe("EventCaptureService.captureEvents", () => { const result = yield* service.captureEvents(captureRequest(token, [eventA, eventB])); - expect(result.accepted).toBe(2); - expect(result.rejected).toBe(0); + expect(result.accepted).toHaveLength(2); + expect(result.rejected).toHaveLength(0); const events = publishedEvents(ingress.batches); expect(events.length).toBe(2); @@ -291,7 +291,7 @@ describe("EventCaptureService.captureEvents", () => { captureRequest(` ${token}\n`, [captureEvent()]), ); - expect(result.accepted).toBe(1); + expect(result.accepted).toHaveLength(1); expect(publishedEvents(ingress.batches).length).toBe(1); }), ).pipe(provideService({ ingress: ingress.layer })); @@ -408,8 +408,12 @@ describe("EventCaptureService.captureEvents", () => { const result = yield* service.captureEvents(captureRequest(token, [reserved, allowed])); - expect(result.rejected).toBe(1); - expect(result.accepted).toBe(1); + expect(result.rejected).toHaveLength(1); + expect(result.accepted).toHaveLength(1); + expect(result.accepted).toEqual([allowed.uuid]); + expect(result.rejected).toEqual([ + { recordId: reserved.uuid, reason: "reserved_event" }, + ]); const events = publishedEvents(ingress.batches); expect(events.length).toBe(1); @@ -423,6 +427,38 @@ describe("EventCaptureService.captureEvents", () => { })(), ); + test( + "acknowledges valid records while classifying invalid context versions and oversized records", + (() => { + const ingress = makeIngressSpy(); + return withCaptureCleanup((trackKey) => + Effect.gen(function* () { + const service = yield* EventCaptureService; + const token = uniqueToken("mixed-ack"); + trackKey(yield* insertPublicApiKey(token)); + + const valid = captureEvent({ event: "valid" }); + const unsupported = captureEvent({ context: { schemaVersion: 2 } }); + const invalidContext = captureEvent({ context: {} }); + const oversized = captureEvent({ properties: { payload: "x".repeat(257 * 1024) } }); + const result = yield* service.captureEvents( + captureRequest(token, [valid, unsupported, invalidContext, oversized]), + ); + + expect(result.accepted).toEqual([valid.uuid]); + expect(result.rejected).toEqual([ + { reason: "unsupported_schema_version", recordId: unsupported.uuid }, + { reason: "invalid_context", recordId: invalidContext.uuid }, + { reason: "payload_too_large", recordId: oversized.uuid }, + ]); + expect(publishedEvents(ingress.batches).map((entry) => entry.envelope.eventId)).toEqual([ + valid.uuid, + ]); + }), + ).pipe(provideService({ ingress: ingress.layer })); + })(), + ); + test( "routes over-quota events to the overflow lane while staying accepted", (() => { @@ -438,8 +474,8 @@ describe("EventCaptureService.captureEvents", () => { // Quota check denies → selectRoute falls back to the overflow lane. const result = yield* service.captureEvents(captureRequest(token, [captureEvent()])); - expect(result.accepted).toBe(1); - expect(result.rejected).toBe(0); + expect(result.accepted).toHaveLength(1); + expect(result.rejected).toHaveLength(0); const events = publishedEvents(ingress.batches); expect(events.length).toBe(1); @@ -464,7 +500,7 @@ describe("EventCaptureService.captureEvents", () => { const result = yield* service.captureEvents(captureRequest(token, [captureEvent()])); - expect(result.accepted).toBe(1); + expect(result.accepted).toHaveLength(1); const events = publishedEvents(ingress.batches); expect(events.length).toBe(1); expect(events[0]?.routeClass).toBe("historical"); @@ -502,8 +538,9 @@ describe("EventCaptureService.captureEvents", () => { captureRequest(token, [captureEvent(), captureEvent()]), ); - expect(result.rejected).toBe(1); - expect(result.accepted).toBe(1); + expect(result.rejected).toHaveLength(1); + expect(result.accepted).toHaveLength(1); + expect(result.rejected[0]?.reason).toBe("policy_rejected"); expect(publishedEvents(ingress.batches).length).toBe(1); }), ).pipe(provideService({ ingress: ingress.layer, policyStore })); diff --git a/packages/core/test/services/analyticsIngest/EventCaptureService.test.ts b/packages/core/test/services/analyticsIngest/EventCaptureService.test.ts index 0b2aee577..616d54ba9 100644 --- a/packages/core/test/services/analyticsIngest/EventCaptureService.test.ts +++ b/packages/core/test/services/analyticsIngest/EventCaptureService.test.ts @@ -48,7 +48,7 @@ const captureEvent = ( ): typeof CaptureEvent.Type => ({ uuid: "evt_uuid_1", event: "page_view", - context: { library: "web" }, + context: { library: "web", schemaVersion: 1 }, properties: { plan: "pro" }, distinct_id: "user_42", ...overrides, @@ -244,7 +244,7 @@ describe("makeEnvelope", () => { expect(envelope.token).toBe("vh_pk_abcd1234"); expect(envelope.event).toBe("page_view"); expect(envelope.distinctId).toBe("user_42"); - expect(envelope.context).toStrictEqual({ library: "web" }); + expect(envelope.context).toStrictEqual({ library: "web", schemaVersion: 1 }); expect(envelope.routing).toStrictEqual(route()); // sentAt (2s) is preferred over receivedAt (5s) since no event.timestamp. expect(envelope.eventTimestamp).toBe("2026-01-01T00:00:02.000Z"); diff --git a/packages/core/test/services/measurement/MeasurementConfigurationService.test.ts b/packages/core/test/services/measurement/MeasurementConfigurationService.test.ts new file mode 100644 index 000000000..0755c5555 --- /dev/null +++ b/packages/core/test/services/measurement/MeasurementConfigurationService.test.ts @@ -0,0 +1,52 @@ +import { + canonicalizeMeasurementConfig, + createMeasurementConfigSigner, +} from "@voidhash/core/services/measurement/MeasurementConfigurationService"; +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; + +const bytes = (value: string): ArrayBuffer => { + const decoded = Uint8Array.from(atob(value), (character) => character.charCodeAt(0)); + return decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength) as ArrayBuffer; +}; + +describe("measurement configuration signing", () => { + it("canonicalizes nested objects independently of insertion order", () => { + expect(canonicalizeMeasurementConfig({ z: 1, a: { y: 2, x: 3 } })).toBe( + canonicalizeMeasurementConfig({ a: { x: 3, y: 2 }, z: 1 }), + ); + }); + + it("creates signatures that verify and reject tampered configuration", async () => { + const signer = await createMeasurementConfigSigner("test-key", undefined, 7); + expect(signer.version).toBe(7); + const payload = new TextEncoder().encode( + canonicalizeMeasurementConfig({ projectId: "project-1", version: 1 }), + ); + const signature = await Effect.runPromise(signer.sign(payload)); + const publicKey = await crypto.subtle.importKey( + "spki", + bytes(signer.publicKeySpki), + "Ed25519", + false, + ["verify"], + ); + await expect( + crypto.subtle.verify("Ed25519", publicKey, bytes(signature), payload), + ).resolves.toBe(true); + await expect( + crypto.subtle.verify( + "Ed25519", + publicKey, + bytes(signature), + new TextEncoder().encode("tampered"), + ), + ).resolves.toBe(false); + }); + + it("rejects invalid monotonic configuration versions", async () => { + await expect(createMeasurementConfigSigner("test-key", undefined, 0)).rejects.toThrow( + "positive safe integer", + ); + }); +}); diff --git a/packages/core/test/services/measurement/ProtectedEvidenceService.test.ts b/packages/core/test/services/measurement/ProtectedEvidenceService.test.ts new file mode 100644 index 000000000..2c299f45a --- /dev/null +++ b/packages/core/test/services/measurement/ProtectedEvidenceService.test.ts @@ -0,0 +1,25 @@ +import { CaptureInvalidRequestError, CapturePayloadTooLargeError } from "@voidhash/api-contracts/event-capture"; +import { decodeProtectedCiphertext } from "@voidhash/core/services/measurement/ProtectedEvidenceService"; +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; + +describe("decodeProtectedCiphertext", () => { + it("decodes canonical base64 without changing ciphertext bytes", async () => { + const result = await Effect.runPromise(decodeProtectedCiphertext("AAECA/7/")); + expect([...result]).toEqual([0, 1, 2, 3, 254, 255]); + }); + + it.each(["not base64", "YWJjZA", "YWJjZA===", "YWJjZA==\n"])( + "rejects non-canonical input %j", + async (input) => { + const error = await Effect.runPromise(Effect.flip(decodeProtectedCiphertext(input))); + expect(error).toBeInstanceOf(CaptureInvalidRequestError); + }, + ); + + it("rejects ciphertext beyond the protected-vault transport bound", async () => { + const input = Buffer.alloc(512 * 1024 + 1, 7).toString("base64"); + const error = await Effect.runPromise(Effect.flip(decodeProtectedCiphertext(input))); + expect(error).toBeInstanceOf(CapturePayloadTooLargeError); + }); +}); diff --git a/packages/db/src/alchemy-migrations/20260720090000_add_protected_measurement_evidence/migration.sql b/packages/db/src/alchemy-migrations/20260720090000_add_protected_measurement_evidence/migration.sql new file mode 100644 index 000000000..1b1df1a77 --- /dev/null +++ b/packages/db/src/alchemy-migrations/20260720090000_add_protected_measurement_evidence/migration.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS "protected_measurement_evidence" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "blob_id" varchar(255) NOT NULL, + "project_id" varchar(255) NOT NULL REFERENCES "project"("id") ON DELETE CASCADE, + "installation_id" varchar(255) NOT NULL, + "purpose" varchar(64) NOT NULL, + "consent_revision" bigint NOT NULL, + "retention_class" varchar(32) NOT NULL, + "encryption_key_version" integer NOT NULL, + "deletion_state" varchar(32) NOT NULL, + "ciphertext" bytea, + "created_at" timestamptz(3) DEFAULT now() NOT NULL, + "updated_at" timestamptz(3) DEFAULT now() NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "protected_measurement_evidence_project_blob_uidx" + ON "protected_measurement_evidence" ("project_id", "blob_id"); +CREATE INDEX IF NOT EXISTS "protected_measurement_evidence_project_purpose_idx" + ON "protected_measurement_evidence" ("project_id", "purpose"); +CREATE INDEX IF NOT EXISTS "protected_measurement_evidence_project_installation_idx" + ON "protected_measurement_evidence" ("project_id", "installation_id"); + +CREATE TABLE IF NOT EXISTS "measurement_deletion_request" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "request_id" varchar(255) NOT NULL, + "project_id" varchar(255) NOT NULL REFERENCES "project"("id") ON DELETE CASCADE, + "installation_id" varchar(255) NOT NULL, + "person_id" varchar(255), + "requested_at" timestamptz(3) NOT NULL, + "completed_at" timestamptz(3) NOT NULL, + "deleted_protected_evidence" integer NOT NULL, + "status" varchar(32) NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "measurement_deletion_request_project_request_uidx" + ON "measurement_deletion_request" ("project_id", "request_id"); +CREATE INDEX IF NOT EXISTS "measurement_deletion_request_project_installation_idx" + ON "measurement_deletion_request" ("project_id", "installation_id"); diff --git a/packages/db/src/alchemy-migrations/20260720170000_add_measurement_links/migration.sql b/packages/db/src/alchemy-migrations/20260720170000_add_measurement_links/migration.sql new file mode 100644 index 000000000..09c3c2db3 --- /dev/null +++ b/packages/db/src/alchemy-migrations/20260720170000_add_measurement_links/migration.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS "measurement_link" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "project_id" varchar(255) NOT NULL REFERENCES "project"("id") ON DELETE CASCADE, + "idempotency_key" varchar(255), + "definition" jsonb NOT NULL, + "signed_token" text NOT NULL, + "expires_at" timestamptz(3) NOT NULL, + "created_at" timestamptz(3) DEFAULT now() NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "measurement_link_project_idempotency_uidx" + ON "measurement_link" ("project_id", "idempotency_key") + WHERE "idempotency_key" IS NOT NULL; +CREATE INDEX IF NOT EXISTS "measurement_link_project_created_idx" + ON "measurement_link" ("project_id", "created_at"); + +CREATE TABLE IF NOT EXISTS "measurement_link_click" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "project_id" varchar(255) NOT NULL REFERENCES "project"("id") ON DELETE CASCADE, + "link_id" varchar(255) NOT NULL, + "context" jsonb NOT NULL, + "deferred_token_hash" varchar(64) NOT NULL, + "deferred_expires_at" timestamptz(3) NOT NULL, + "installation_id" varchar(255), + "consumed_at" timestamptz(3), + "occurred_at" timestamptz(3) NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "measurement_link_click_deferred_hash_uidx" + ON "measurement_link_click" ("deferred_token_hash"); +CREATE INDEX IF NOT EXISTS "measurement_link_click_project_link_idx" + ON "measurement_link_click" ("project_id", "link_id"); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index f1b2c6a98..8f8ea8212 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -3,6 +3,7 @@ import { sql } from "drizzle-orm"; import { bigint, boolean, + bytea, index, integer, jsonb, @@ -191,6 +192,114 @@ export const captureProjectPolicies = pgTable( (table) => [index("capture_project_policy_force_route_idx").on(table.forceRoute)], ); +export const protectedMeasurementEvidence = pgTable( + "protected_measurement_evidence", + { + id: varchar("id", { length: 255 }).primaryKey(), + blobId: varchar("blob_id", { length: 255 }).notNull(), + projectId: varchar("project_id", { length: 255 }) + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + installationId: varchar("installation_id", { length: 255 }).notNull(), + purpose: varchar("purpose", { length: 64 }).notNull(), + consentRevision: bigint("consent_revision", { mode: "number" }).notNull(), + retentionClass: varchar("retention_class", { length: 32 }).notNull(), + encryptionKeyVersion: integer("encryption_key_version").notNull(), + deletionState: varchar("deletion_state", { length: 32 }).notNull(), + ciphertext: bytea("ciphertext"), + createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, precision: 3 }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex("protected_measurement_evidence_project_blob_uidx").on( + table.projectId, + table.blobId, + ), + index("protected_measurement_evidence_project_purpose_idx").on( + table.projectId, + table.purpose, + ), + index("protected_measurement_evidence_project_installation_idx").on( + table.projectId, + table.installationId, + ), + ], +); + +export const measurementDeletionRequests = pgTable( + "measurement_deletion_request", + { + id: varchar("id", { length: 255 }).primaryKey(), + requestId: varchar("request_id", { length: 255 }).notNull(), + projectId: varchar("project_id", { length: 255 }) + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + installationId: varchar("installation_id", { length: 255 }).notNull(), + personId: varchar("person_id", { length: 255 }), + requestedAt: timestamp("requested_at", { withTimezone: true, precision: 3 }).notNull(), + completedAt: timestamp("completed_at", { withTimezone: true, precision: 3 }).notNull(), + deletedProtectedEvidence: integer("deleted_protected_evidence").notNull(), + status: varchar("status", { length: 32 }).notNull(), + }, + (table) => [ + uniqueIndex("measurement_deletion_request_project_request_uidx").on( + table.projectId, + table.requestId, + ), + index("measurement_deletion_request_project_installation_idx").on( + table.projectId, + table.installationId, + ), + ], +); + +/** Immutable project-scoped definitions backing signed short links. */ +export const measurementLinks = pgTable( + "measurement_link", + { + id: varchar("id", { length: 255 }).primaryKey(), + projectId: varchar("project_id", { length: 255 }) + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + idempotencyKey: varchar("idempotency_key", { length: 255 }), + definition: jsonb("definition").$type>().notNull(), + signedToken: text("signed_token").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, precision: 3 }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).defaultNow().notNull(), + }, + (table) => [ + uniqueIndex("measurement_link_project_idempotency_uidx") + .on(table.projectId, table.idempotencyKey) + .where(sql`${table.idempotencyKey} is not null`), + index("measurement_link_project_created_idx").on(table.projectId, table.createdAt), + ], +); + +/** Append-only, privacy-bounded click evidence recorded before redirecting. */ +export const measurementLinkClicks = pgTable( + "measurement_link_click", + { + id: varchar("id", { length: 255 }).primaryKey(), + projectId: varchar("project_id", { length: 255 }) + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + linkId: varchar("link_id", { length: 255 }).notNull(), + context: jsonb("context").$type>().notNull(), + deferredTokenHash: varchar("deferred_token_hash", { length: 64 }).notNull(), + deferredExpiresAt: timestamp("deferred_expires_at", { withTimezone: true, precision: 3 }).notNull(), + installationId: varchar("installation_id", { length: 255 }), + consumedAt: timestamp("consumed_at", { withTimezone: true, precision: 3 }), + occurredAt: timestamp("occurred_at", { withTimezone: true, precision: 3 }).notNull(), + }, + (table) => [ + uniqueIndex("measurement_link_click_deferred_hash_uidx").on(table.deferredTokenHash), + index("measurement_link_click_project_link_idx").on(table.projectId, table.linkId), + ], +); + export const apiKeys = pgTable( "api_key", { diff --git a/packages/generated-clients/openapi/core.json b/packages/generated-clients/openapi/core.json index ddba1820c..c8eb3dad8 100644 --- a/packages/generated-clients/openapi/core.json +++ b/packages/generated-clients/openapi/core.json @@ -1,10 +1,15 @@ { "openapi": "3.1.0", - "info": { "title": "Api", "version": "0.0.1" }, + "info": { + "title": "Api", + "version": "0.0.1" + }, "paths": { "/api/v1/auth/session": { "get": { - "tags": ["auth"], + "tags": [ + "auth" + ], "operationId": "auth.session", "parameters": [], "security": [], @@ -17,23 +22,36 @@ "type": "object", "properties": { "method": { - "anyOf": [ - { "type": "string", "enum": ["api-key"] }, - { "type": "string", "enum": ["publishable-key"] }, - { "type": "string", "enum": ["secret-key"] } + "type": "string", + "enum": [ + "api-key", + "publishable-key", + "secret-key" ] }, - "name": { "type": "string" }, + "name": { + "type": "string" + }, "organizations": { "type": "array", "items": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "name", "slug"], + "required": [ + "id", + "name", + "slug" + ], "additionalProperties": false } }, @@ -42,35 +60,47 @@ "items": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "organizationId": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "name", "organizationId", "slug"], + "required": [ + "id", + "name", + "organizationId", + "slug" + ], "additionalProperties": false } } }, - "required": ["method", "name", "organizations", "projects"], + "required": [ + "method", + "name", + "organizations", + "projects" + ], "additionalProperties": false } } } }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } - }, "403": { "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -80,8 +110,12 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } } @@ -92,22 +126,20 @@ }, "/api/v1/api-keys": { "post": { - "tags": ["api_keys"], + "tags": [ + "api_keys" + ], "operationId": "api_keys.createSecretKey", "parameters": [], "security": [], "responses": { "200": { "description": "ApiKeyWithRawKey", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyWithRawKey" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/ApiKeyWithRawKey" + } } } }, @@ -115,7 +147,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -125,11 +159,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ApiKeyServiceError" }, + { + "$ref": "#/components/schemas/Api_ApiKeyServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -140,13 +180,19 @@ }, "requestBody": { "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/CreateSecretKeyBody" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSecretKeyBody" + } + } }, "required": true } }, "get": { - "tags": ["api_keys"], + "tags": [ + "api_keys" + ], "operationId": "api_keys.listApiKeys", "parameters": [], "security": [], @@ -155,15 +201,12 @@ "description": "Success", "content": { "application/json": { - "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKey" } } - } - } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + } + } } } }, @@ -171,7 +214,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -181,11 +226,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ApiKeyServiceError" }, + { + "$ref": "#/components/schemas/Api_ApiKeyServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -198,24 +249,29 @@ }, "/api/v1/api-keys/{apiKeyId}": { "get": { - "tags": ["api_keys"], + "tags": [ + "api_keys" + ], "operationId": "api_keys.getApiKeyById", "parameters": [ - { "name": "apiKeyId", "in": "path", "schema": { "type": "string" }, "required": true } + { + "name": "apiKeyId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } ], "security": [], "responses": { "200": { "description": "ApiKey", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ApiKey" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/ApiKey" + } } } }, @@ -223,7 +279,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -231,7 +289,9 @@ "description": "Api/ApiKeyNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" + } } } }, @@ -241,11 +301,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ApiKeyServiceError" }, + { + "$ref": "#/components/schemas/Api_ApiKeyServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -256,27 +322,32 @@ } }, "delete": { - "tags": ["api_keys"], + "tags": [ + "api_keys" + ], "operationId": "api_keys.deleteApiKey", "parameters": [ - { "name": "apiKeyId", "in": "path", "schema": { "type": "string" }, "required": true } + { + "name": "apiKeyId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } ], "security": [], "responses": { - "204": { "description": "" }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + "204": { + "description": "" }, "403": { "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -284,7 +355,9 @@ "description": "Api/ApiKeyNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" + } } } }, @@ -294,11 +367,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ApiKeyServiceError" }, + { + "$ref": "#/components/schemas/Api_ApiKeyServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -311,24 +390,29 @@ }, "/api/v1/api-keys/{apiKeyId}/rotate": { "post": { - "tags": ["api_keys"], + "tags": [ + "api_keys" + ], "operationId": "api_keys.rotateSecretKey", "parameters": [ - { "name": "apiKeyId", "in": "path", "schema": { "type": "string" }, "required": true } + { + "name": "apiKeyId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } ], "security": [], "responses": { "200": { "description": "ApiKeyWithRawKey", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyWithRawKey" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/ApiKeyWithRawKey" + } } } }, @@ -336,7 +420,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -344,7 +430,9 @@ "description": "Api/ApiKeyNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_ApiKeyNotFoundError" + } } } }, @@ -354,11 +442,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ApiKeyServiceError" }, + { + "$ref": "#/components/schemas/Api_ApiKeyServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -371,7 +465,9 @@ }, "/api/v1/persons": { "post": { - "tags": ["persons"], + "tags": [ + "persons" + ], "operationId": "persons.createPerson", "parameters": [], "security": [], @@ -379,18 +475,19 @@ "200": { "description": "Person", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/Person" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } } }, "400": { - "description": "Api/PersonInvalidAnonymousIdError | The request or response did not match the expected schema", + "description": "Api/PersonInvalidAnonymousIdError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_PersonInvalidAnonymousIdError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Api_PersonInvalidAnonymousIdError" } } } @@ -399,7 +496,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -409,11 +508,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PersonServiceError" }, + { + "$ref": "#/components/schemas/Api_PersonServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -424,13 +529,19 @@ }, "requestBody": { "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/CreatePersonBody" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonBody" + } + } }, "required": true } }, "get": { - "tags": ["persons"], + "tags": [ + "persons" + ], "operationId": "persons.listPersons", "parameters": [], "security": [], @@ -439,15 +550,12 @@ "description": "Success", "content": { "application/json": { - "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Person" } } - } - } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + } } } }, @@ -455,7 +563,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -465,11 +575,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PersonServiceError" }, + { + "$ref": "#/components/schemas/Api_PersonServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -482,24 +598,29 @@ }, "/api/v1/persons/{personId}": { "get": { - "tags": ["persons"], + "tags": [ + "persons" + ], "operationId": "persons.getPersonById", "parameters": [ - { "name": "personId", "in": "path", "schema": { "type": "string" }, "required": true } + { + "name": "personId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } ], "security": [], "responses": { "200": { "description": "Person", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/Person" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Person" + } } } }, @@ -507,7 +628,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -515,7 +638,9 @@ "description": "Api/PersonNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_PersonNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_PersonNotFoundError" + } } } }, @@ -525,11 +650,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PersonServiceError" }, + { + "$ref": "#/components/schemas/Api_PersonServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -542,24 +673,29 @@ }, "/api/v1/persons/by-distinct-id/{distinctId}": { "get": { - "tags": ["persons"], + "tags": [ + "persons" + ], "operationId": "persons.getPersonByDistinctId", "parameters": [ - { "name": "distinctId", "in": "path", "schema": { "type": "string" }, "required": true } + { + "name": "distinctId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } ], "security": [], "responses": { "200": { "description": "Person", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/Person" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Person" + } } } }, @@ -567,7 +703,9 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, @@ -575,7 +713,9 @@ "description": "Api/PersonNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_PersonNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_PersonNotFoundError" + } } } }, @@ -585,11 +725,17 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PersonServiceError" }, + { + "$ref": "#/components/schemas/Api_PersonServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -600,38 +746,75 @@ } } }, - "/api/v1/organizations": { + "/api/v1/notifications/send": { "post": { - "tags": ["organizations"], - "operationId": "organizations.createOrganization", + "tags": [ + "notifications" + ], + "operationId": "notifications.sendNotification", "parameters": [], "security": [], "responses": { "200": { - "description": "Organization", + "description": "SendNotificationResponse", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/Organization" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendNotificationResponse" + } + } } }, "400": { - "description": "The request or response did not match the expected schema", + "description": "Api/PushDeviceValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_PushDeviceValidationError" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "409": { + "description": "Api/PushSendNotEnabledError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_PushSendNotEnabledError" + } } } }, "500": { - "description": "Api/OrganizationServiceError", + "description": "Api/AuthenticationError | Api/PushSendServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_OrganizationServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_PushSendServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -643,55 +826,51 @@ "requestBody": { "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateOrganizationBody" } + "schema": { + "$ref": "#/components/schemas/SendNotificationBody" + } } }, "required": true } } }, - "/api/v1/perks": { - "get": { - "tags": ["perks"], - "operationId": "perks.listPerks", + "/api/v1/organizations": { + "post": { + "tags": [ + "organizations" + ], + "operationId": "organizations.createOrganization", "parameters": [], "security": [], "responses": { "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Perk" } } - } - } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } - }, - "403": { - "description": "Api/ActionForbiddenError", + "description": "Organization", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Organization" + } } } }, "500": { - "description": "Api/PerkServiceError", + "description": "Api/OrganizationServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PerkServiceError" }, + { + "$ref": "#/components/schemas/Api_OrganizationServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -699,14 +878,26 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationBody" + } + } + }, + "required": true } } }, - "/api/v1/paywall-locations": { + "/api/v1/perks": { "get": { - "tags": ["paywall_locations"], - "operationId": "paywall_locations.listPaywallLocations", - "parameters": [], + "tags": [ + "perks" + ], + "operationId": "perks.listPerks", + "parameters": [], "security": [], "responses": { "200": { @@ -715,38 +906,40 @@ "application/json": { "schema": { "type": "array", - "items": { "$ref": "#/components/schemas/PaywallLocation" } + "items": { + "$ref": "#/components/schemas/Perk" + } } } } }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } - }, "403": { "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "500": { - "description": "Api/PaywallLocationServiceError", + "description": "Api/PerkServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_PaywallLocationServiceError" }, + { + "$ref": "#/components/schemas/Api_PerkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -757,26 +950,32 @@ } } }, - "/api/v1/schema": { - "get": { - "tags": ["schema"], - "operationId": "schema.getSchema", + "/api/v1/paywall-deploys": { + "post": { + "tags": [ + "paywall_deploys" + ], + "operationId": "paywall_deploys.createDeploy", "parameters": [], "security": [], "responses": { - "200": { - "description": "ProjectSchemaResponse", + "201": { + "description": "CreatePaywallDeployResponse", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/ProjectSchemaResponse" } + "schema": { + "$ref": "#/components/schemas/CreatePaywallDeployResponse" + } } } }, "400": { - "description": "The request or response did not match the expected schema", + "description": "Api/PaywallDeployUpgradeRequiredError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_PaywallDeployUpgradeRequiredError" + } } } }, @@ -784,21 +983,39 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "422": { + "description": "Api/PaywallDeployValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_PaywallDeployValidationError" + } } } }, "500": { - "description": "Api/SchemaServiceError", + "description": "Api/PaywallDeployServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_SchemaServiceError" }, + { + "$ref": "#/components/schemas/Api_PaywallDeployServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -806,27 +1023,50 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": {} + } + }, + "required": true } } }, - "/api/v1/schema/version": { - "get": { - "tags": ["schema"], - "operationId": "schema.getSchemaVersion", - "parameters": [], + "/api/v1/paywall-deploys/{deployId}/blobs/{sha256}": { + "put": { + "tags": [ + "paywall_deploys" + ], + "operationId": "paywall_deploys.uploadBlob", + "parameters": [ + { + "name": "deployId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "sha256", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], "security": [], "responses": { "200": { - "description": "SchemaVersion", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SchemaVersion" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", + "description": "UploadPaywallDeployBlobResponse", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/UploadPaywallDeployBlobResponse" + } } } }, @@ -834,72 +1074,73 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, - "500": { - "description": "Api/SchemaServiceError", + "404": { + "description": "Api/DeployBlobNotDeclaredError | Api/PaywallDeployNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_SchemaServiceError" }, { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] + "$ref": "#/components/schemas/Api_DeployBlobNotDeclaredError" + }, + { + "$ref": "#/components/schemas/Api_PaywallDeployNotFoundError" } ] } } } - } - } - } - }, - "/api/v1/projects": { - "post": { - "tags": ["projects"], - "operationId": "projects.createProject", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Project", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } - } }, - "400": { - "description": "The request or response did not match the expected schema", + "409": { + "description": "Api/PaywallDeployNotPendingError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_PaywallDeployNotPendingError" + } } } }, - "403": { - "description": "Api/ActionForbiddenError", + "422": { + "description": "Api/DeployBlobHashMismatchError | Api/PaywallDeployValidationError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_DeployBlobHashMismatchError" + }, + { + "$ref": "#/components/schemas/Api_PaywallDeployValidationError" + } + ] + } } } }, "500": { - "description": "Api/AuthenticationError | Api/ProjectServiceError", + "description": "Api/PaywallDeployServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_ProjectServiceError" }, + { + "$ref": "#/components/schemas/Api_PaywallDeployServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -910,61 +1151,102 @@ }, "requestBody": { "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/CreateProjectBody" } } + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } }, "required": true } } }, - "/api/v1/projects/{organizationId}": { - "get": { - "tags": ["projects"], - "operationId": "projects.listProjects", + "/api/v1/paywall-deploys/{deployId}/finalize": { + "post": { + "tags": [ + "paywall_deploys" + ], + "operationId": "paywall_deploys.finalizeDeploy", "parameters": [ { - "name": "organizationId", + "name": "deployId", "in": "path", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true } ], "security": [], "responses": { "200": { - "description": "Success", + "description": "FinalizePaywallDeployResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinalizePaywallDeployResponse" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, - "400": { - "description": "The request or response did not match the expected schema", + "404": { + "description": "Api/PaywallDeployNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_PaywallDeployNotFoundError" + } } } }, - "403": { - "description": "Api/ActionForbiddenError", + "409": { + "description": "Api/IncompleteDeployError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_IncompleteDeployError" + } + } + } + }, + "422": { + "description": "Api/PaywallDeployValidationError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_PaywallDeployValidationError" + } } } }, "500": { - "description": "Api/ProjectServiceError", + "description": "Api/PaywallDeployServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ProjectServiceError" }, + { + "$ref": "#/components/schemas/Api_PaywallDeployServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -975,10 +1257,12 @@ } } }, - "/api/v1/products": { + "/api/v1/paywall-locations": { "get": { - "tags": ["products"], - "operationId": "products.listProducts", + "tags": [ + "paywall_locations" + ], + "operationId": "paywall_locations.listPaywallLocations", "parameters": [], "security": [], "responses": { @@ -986,15 +1270,12 @@ "description": "Success", "content": { "application/json": { - "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Product" } } - } - } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaywallLocation" + } + } } } }, @@ -1002,21 +1283,29 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "500": { - "description": "Api/ProductServiceError", + "description": "Api/PaywallLocationServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ProductServiceError" }, + { + "$ref": "#/components/schemas/Api_PaywallLocationServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1027,58 +1316,108 @@ } } }, - "/api/v1/product-perks/by-product-id/{productId}": { + "/api/v1/schema": { "get": { - "tags": ["product_perks"], - "operationId": "product_perks.listProductPerksByProductId", - "parameters": [ - { "name": "productId", "in": "path", "schema": { "type": "string" }, "required": true } + "tags": [ + "schema" ], + "operationId": "schema.getSchema", + "parameters": [], "security": [], "responses": { "200": { - "description": "Success", + "description": "ProjectSchemaResponse", "content": { "application/json": { "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/ProductPerk" } + "$ref": "#/components/schemas/ProjectSchemaResponse" } } } }, - "400": { - "description": "Api/ProductPerkValidationError | The request or response did not match the expected schema", + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/SchemaServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ProductPerkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + { + "$ref": "#/components/schemas/Api_SchemaServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } ] } } } + } + } + } + }, + "/api/v1/schema/version": { + "get": { + "tags": [ + "schema" + ], + "operationId": "schema.getSchemaVersion", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "SchemaVersion", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaVersion" + } + } + } }, "403": { "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "500": { - "description": "Api/ProductPerkServiceError", + "description": "Api/SchemaServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_ProductPerkServiceError" }, + { + "$ref": "#/components/schemas/Api_SchemaServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1089,187 +1428,55 @@ } } }, - "/api/v1/sdk/person": { - "get": { - "tags": ["sdk"], - "operationId": "sdk.getPerson", - "parameters": [ - { - "name": "x-distinct-id", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-publishable-key", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-client-bundle-id", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-client-locale", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-client-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-is-backgrounded", - "in": "header", - "schema": { "type": "string", "enum": ["false"] }, - "required": true - }, - { - "name": "x-is-debug-build", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } - ] - }, - "required": true - }, - { - "name": "x-nonce", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-observer-mode", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } - ] - }, - "required": true - }, - { - "name": "x-platform", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-platform-brand", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-device", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-flavor", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } - ] - }, - "required": true - }, - { - "name": "x-platform-flavor-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-preferred-locales", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-sdk", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } - ] - }, - "required": true - }, - { - "name": "x-sdk-version", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-storefront", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - } + "/api/v1/projects": { + "post": { + "tags": [ + "projects" ], + "operationId": "projects.createProject", + "parameters": [], "security": [], "responses": { "200": { - "description": "SdkPerson", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SdkPerson" } } - } - }, - "400": { - "description": "Api/SdkValidationError | The request or response did not match the expected schema", + "description": "Project", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_SdkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Project" } } } }, - "404": { - "description": "Api/SdkPersonNotFoundError", + "403": { + "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "500": { - "description": "Api/AuthenticationError | Api/SdkServiceError", + "description": "Api/AuthenticationError | Api/ProjectServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_ProjectServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1277,198 +1484,136 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProjectBody" + } + } + }, + "required": true } } }, - "/api/v1/sdk/identify": { - "post": { - "tags": ["sdk"], - "operationId": "sdk.identifyPerson", + "/api/v1/projects/{organizationId}": { + "get": { + "tags": [ + "projects" + ], + "operationId": "projects.listProjects", "parameters": [ { - "name": "x-distinct-id", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-publishable-key", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-client-bundle-id", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-client-locale", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-client-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-is-backgrounded", - "in": "header", - "schema": { "type": "string", "enum": ["false"] }, - "required": true - }, - { - "name": "x-is-debug-build", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } - ] - }, - "required": true - }, - { - "name": "x-nonce", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-observer-mode", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } - ] - }, - "required": true - }, - { - "name": "x-platform", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-platform-brand", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-device", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-flavor", - "in": "header", - "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } - ] - }, - "required": true - }, - { - "name": "x-platform-flavor-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-version", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-preferred-locales", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-sdk", - "in": "header", + "name": "organizationId", + "in": "path", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } - ] + "type": "string" }, "required": true - }, - { - "name": "x-sdk-version", - "in": "header", - "schema": { "type": "string" }, - "required": true - }, - { - "name": "x-storefront", - "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false } ], "security": [], "responses": { "200": { - "description": "SdkPerson", + "description": "Success", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SdkPerson" } } + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + } + } } }, - "400": { - "description": "Api/SdkValidationError | The request or response did not match the expected schema", + "403": { + "description": "Api/ActionForbiddenError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_SdkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Api_ActionForbiddenError" } } } }, - "404": { - "description": "Api/SdkPersonNotFoundError", + "500": { + "description": "Api/ProjectServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_ProjectServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/products": { + "get": { + "tags": [ + "products" + ], + "operationId": "products.listProducts", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" } + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } } } }, - "409": { - "description": "Api/SdkPersonAlreadyIdentifiedError", + "403": { + "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_SdkPersonAlreadyIdentifiedError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "500": { - "description": "Api/AuthenticationError | Api/SdkServiceError", + "description": "Api/ProductServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_ProductServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1476,63 +1621,167 @@ } } } - }, - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SdkIdentifyBody" } } + } + } + }, + "/api/v1/product-perks/by-product-id/{productId}": { + "get": { + "tags": [ + "product_perks" + ], + "operationId": "product_perks.listProductPerksByProductId", + "parameters": [ + { + "name": "productId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductPerk" + } + } + } + } }, - "required": true + "400": { + "description": "Api/ProductPerkValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ProductPerkValidationError" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/ProductPerkServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_ProductPerkServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } } } }, - "/api/v1/sdk/person/traits": { - "post": { - "tags": ["sdk"], - "operationId": "sdk.syncPersonAttributes", + "/api/v1/sdk/person": { + "get": { + "tags": [ + "sdk" + ], + "operationId": "sdk.getPerson", "parameters": [ { "name": "x-distinct-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-publishable-key", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-bundle-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-locale", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-client-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-is-backgrounded", "in": "header", - "schema": { "type": "string", "enum": ["false"] }, + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, "required": true }, { "name": "x-is-debug-build", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1540,16 +1789,26 @@ { "name": "x-nonce", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-observer-mode", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1557,28 +1816,49 @@ { "name": "x-platform", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-platform-brand", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-device", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-flavor", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } + "type": "string", + "enum": [ + "native", + "browser" ] }, "required": true @@ -1586,28 +1866,56 @@ { "name": "x-platform-flavor-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-preferred-locales", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-sdk", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } + "type": "string", + "enum": [ + "react-native", + "web" ] }, "required": true @@ -1615,13 +1923,24 @@ { "name": "x-sdk-version", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-storefront", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false } ], @@ -1630,18 +1949,19 @@ "200": { "description": "SdkPerson", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SdkPerson" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/SdkPerson" + } + } } }, "400": { - "description": "Api/SdkValidationError | The request or response did not match the expected schema", + "description": "Api/SdkValidationError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_SdkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Api_SdkValidationError" } } } @@ -1650,7 +1970,9 @@ "description": "Api/SdkPersonNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" + } } } }, @@ -1660,12 +1982,20 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1673,65 +2003,89 @@ } } } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SdkSyncPersonAttributesBody" } - } - }, - "required": true } } }, - "/api/v1/sdk/sync-transaction": { + "/api/v1/sdk/identify": { "post": { - "tags": ["sdk"], - "operationId": "sdk.syncTransaction", + "tags": [ + "sdk" + ], + "operationId": "sdk.identifyPerson", "parameters": [ { "name": "x-distinct-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-publishable-key", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-bundle-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-locale", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-client-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-is-backgrounded", "in": "header", - "schema": { "type": "string", "enum": ["false"] }, + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, "required": true }, { "name": "x-is-debug-build", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1739,16 +2093,26 @@ { "name": "x-nonce", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-observer-mode", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1756,28 +2120,49 @@ { "name": "x-platform", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-platform-brand", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-device", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "required": false - }, - { - "name": "x-platform-flavor", - "in": "header", "schema": { "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" ] }, "required": true @@ -1785,28 +2170,56 @@ { "name": "x-platform-flavor-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-preferred-locales", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-sdk", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } + "type": "string", + "enum": [ + "react-native", + "web" ] }, "required": true @@ -1814,35 +2227,65 @@ { "name": "x-sdk-version", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-storefront", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false } ], "security": [], "responses": { "200": { - "description": "SdkSyncTransactionResponse", + "description": "SdkPerson", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/SdkSyncTransactionResponse" } + "schema": { + "$ref": "#/components/schemas/SdkPerson" + } } } }, "400": { - "description": "Api/SdkValidationError | The request or response did not match the expected schema", + "description": "Api/SdkValidationError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_SdkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Api_SdkValidationError" + } + } + } + }, + "404": { + "description": "Api/SdkPersonNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" + } + } + } + }, + "409": { + "description": "Api/SdkPersonAlreadyIdentifiedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_SdkPersonAlreadyIdentifiedError" } } } @@ -1853,12 +2296,20 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -1871,45 +2322,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "appAccountToken": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "platform": { - "anyOf": [ - { "type": "string", "enum": ["ios"] }, - { "type": "string", "enum": ["android"] } - ] - }, - "providerProductId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "productSlug": { "type": "string" }, - "purchaseDate": { - "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } - ] - }, - "purchaseToken": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "quantity": { - "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } - ] - }, - "receipt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "transactionId": { "type": "string" } - }, - "required": [ - "platform", - "productSlug", - "purchaseDate", - "quantity", - "transactionId" - ], - "additionalProperties": false + "$ref": "#/components/schemas/SdkIdentifyBody" } } }, @@ -1917,54 +2330,86 @@ } } }, - "/api/v1/sdk/evaluate-flags": { + "/api/v1/sdk/person/traits": { "post": { - "tags": ["sdk"], - "operationId": "sdk.evaluateFeatureFlags", + "tags": [ + "sdk" + ], + "operationId": "sdk.syncPersonAttributes", "parameters": [ { "name": "x-distinct-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-publishable-key", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-bundle-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-locale", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-client-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-is-backgrounded", "in": "header", - "schema": { "type": "string", "enum": ["false"] }, + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, "required": true }, { "name": "x-is-debug-build", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1972,16 +2417,26 @@ { "name": "x-nonce", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-observer-mode", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -1989,28 +2444,49 @@ { "name": "x-platform", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-platform-brand", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-device", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-flavor", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } + "type": "string", + "enum": [ + "native", + "browser" ] }, "required": true @@ -2018,28 +2494,56 @@ { "name": "x-platform-flavor-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-preferred-locales", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-sdk", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } + "type": "string", + "enum": [ + "react-native", + "web" ] }, "required": true @@ -2047,31 +2551,56 @@ { "name": "x-sdk-version", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-storefront", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false } ], "security": [], "responses": { "200": { - "description": "SdkFeatureFlagsResponse", + "description": "SdkPerson", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/SdkFeatureFlagsResponse" } + "schema": { + "$ref": "#/components/schemas/SdkPerson" + } } } }, "400": { - "description": "The request or response did not match the expected schema", + "description": "Api/SdkValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_SdkValidationError" + } + } + } + }, + "404": { + "description": "Api/SdkPersonNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_SdkPersonNotFoundError" + } } } }, @@ -2081,12 +2610,20 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2098,61 +2635,95 @@ "requestBody": { "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/EvaluateFeatureFlagsBody" } + "schema": { + "$ref": "#/components/schemas/SdkSyncPersonAttributesBody" + } } }, "required": true } } }, - "/api/v1/sdk/resolve-paywall": { + "/api/v1/sdk/sync-transaction": { "post": { - "tags": ["sdk"], - "operationId": "sdk.resolvePaywall", + "tags": [ + "sdk" + ], + "operationId": "sdk.syncTransaction", "parameters": [ { "name": "x-distinct-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-publishable-key", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-bundle-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-locale", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-client-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-is-backgrounded", "in": "header", - "schema": { "type": "string", "enum": ["false"] }, + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, "required": true }, { "name": "x-is-debug-build", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -2160,16 +2731,26 @@ { "name": "x-nonce", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-observer-mode", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -2177,28 +2758,49 @@ { "name": "x-platform", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-platform-brand", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-device", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-flavor", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } + "type": "string", + "enum": [ + "native", + "browser" ] }, "required": true @@ -2206,28 +2808,56 @@ { "name": "x-platform-flavor-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-preferred-locales", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-sdk", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } + "type": "string", + "enum": [ + "react-native", + "web" ] }, "required": true @@ -2235,40 +2865,45 @@ { "name": "x-sdk-version", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-storefront", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false } ], "security": [], "responses": { "200": { - "description": "Success", + "description": "SdkSyncTransactionResponse", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/SdkResolvedPaywall" }, - { "type": "null" } - ] + "$ref": "#/components/schemas/SdkSyncTransactionResponse" } } } }, "400": { - "description": "Api/SdkValidationError | The request or response did not match the expected schema", + "description": "Api/SdkValidationError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_SdkValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/Api_SdkValidationError" } } } @@ -2279,12 +2914,20 @@ "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SdkServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2296,61 +2939,232 @@ "requestBody": { "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/SdkResolvePaywallBody" } + "schema": { + "type": "object", + "properties": { + "appAccountToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "platform": { + "type": "string", + "enum": [ + "ios", + "android" + ] + }, + "providerProductId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "productSlug": { + "type": "string" + }, + "purchaseDate": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "purchaseToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "quantity": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "receipt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transactionId": { + "type": "string" + } + }, + "required": [ + "platform", + "productSlug", + "purchaseDate", + "quantity", + "transactionId" + ], + "additionalProperties": false + } } }, "required": true } } }, - "/api/v1/sdk/schema": { - "get": { - "tags": ["sdk"], - "operationId": "sdk.getSchema", + "/api/v1/sdk/evaluate-flags": { + "post": { + "tags": [ + "sdk" + ], + "operationId": "sdk.evaluateFeatureFlags", "parameters": [ { "name": "x-distinct-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-publishable-key", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-bundle-id", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-client-locale", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-client-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-is-backgrounded", "in": "header", - "schema": { "type": "string", "enum": ["false"] }, + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, "required": true }, { "name": "x-is-debug-build", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -2358,16 +3172,26 @@ { "name": "x-nonce", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-observer-mode", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["true"] }, - { "type": "string", "enum": ["false"] } + "type": "string", + "enum": [ + "true", + "false" ] }, "required": true @@ -2375,28 +3199,49 @@ { "name": "x-platform", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-platform-brand", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-device", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-flavor", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["native"] }, - { "type": "string", "enum": ["browser"] } + "type": "string", + "enum": [ + "native", + "browser" ] }, "required": true @@ -2404,28 +3249,56 @@ { "name": "x-platform-flavor-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-platform-version", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-preferred-locales", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false }, { "name": "x-sdk", "in": "header", "schema": { - "anyOf": [ - { "type": "string", "enum": ["react-native"] }, - { "type": "string", "enum": ["web"] } + "type": "string", + "enum": [ + "react-native", + "web" ] }, "required": true @@ -2433,44 +3306,59 @@ { "name": "x-sdk-version", "in": "header", - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "required": true }, { "name": "x-storefront", "in": "header", - "schema": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "required": false } ], "security": [], "responses": { "200": { - "description": "SdkSchema", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/SdkSchema" } } - } - }, - "400": { - "description": "The request or response did not match the expected schema", + "description": "SdkFeatureFlagsResponse", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/SdkFeatureFlagsResponse" + } } } }, "500": { - "description": "Api/AuthenticationError | Api/SchemaServiceError", + "description": "Api/AuthenticationError | Api/SdkServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_SchemaServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2478,205 +3366,310 @@ } } } - } - } - }, - "/api/v1/users/current": { - "get": { - "tags": ["users"], - "operationId": "users.getUser", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "User", - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvaluateFeatureFlagsBody" } } }, - "500": { - "description": "Api/AuthenticationError | Api/UserServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_UserServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] - } - } - } - } + "required": true } } }, - "/api/v1/payment-provider-configurations": { - "get": { - "tags": ["payment_provider_configurations"], - "operationId": "payment_provider_configurations.listPaymentProviderConfigurations", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/PaymentProviderConfiguration" } - } - } - } + "/api/v1/sdk/resolve-paywall": { + "post": { + "tags": [ + "sdk" + ], + "operationId": "sdk.resolvePaywall", + "parameters": [ + { + "name": "x-distinct-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-client-bundle-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "500": { - "description": "Api/PaymentProviderConfigurationServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_PaymentProviderConfigurationServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] - } - } - } - } - } - } - }, - "/api/v1/payment-provider-products": { - "get": { - "tags": ["payment_provider_products"], - "operationId": "payment_provider_products.listPaymentProviderProducts", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/PaymentProviderProduct" } + { + "name": "x-client-locale", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } + ] + }, + "required": false }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + { + "name": "x-client-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-is-backgrounded", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, + "required": true }, - "500": { - "description": "Api/PaymentProviderProductServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_PaymentProviderProductServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] + { + "name": "x-is-debug-build", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-nonce", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } + ] + }, + "required": false + }, + { + "name": "x-observer-mode", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-platform", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-platform-brand", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-device", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" + ] + }, + "required": true + }, + { + "name": "x-platform-flavor-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-preferred-locales", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-sdk", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "react-native", + "web" + ] + }, + "required": true + }, + { + "name": "x-sdk-version", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-storefront", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } - } - } - }, - "/api/v1/webhooks/endpoints": { - "post": { - "tags": ["webhooks"], - "operationId": "webhooks.createWebhookEndpoint", - "parameters": [], + ], "security": [], "responses": { "200": { - "description": "WebhookEndpoint", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } - } - }, - "400": { - "description": "Api/WebhookValidationError | The request or response did not match the expected schema", + "description": "Success", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + { + "$ref": "#/components/schemas/SdkResolvedPaywall" + }, + { + "type": "null" + } ] } } } }, - "403": { - "description": "Api/ActionForbiddenError", + "400": { + "description": "Api/SdkValidationError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_SdkValidationError" + } } } }, "500": { - "description": "Api/WebhookServiceError", + "description": "Api/AuthenticationError | Api/SdkServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SdkServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2688,179 +3681,289 @@ "requestBody": { "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateWebhookEndpointBody" } + "schema": { + "$ref": "#/components/schemas/SdkResolvePaywallBody" + } } }, "required": true } - }, + } + }, + "/api/v1/sdk/schema": { "get": { - "tags": ["webhooks"], - "operationId": "webhooks.listWebhookEndpoints", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/WebhookEndpoint" } - } - } - } - }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + "tags": [ + "sdk" + ], + "operationId": "sdk.getSchema", + "parameters": [ + { + "name": "x-distinct-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "500": { - "description": "Api/WebhookServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] + { + "name": "x-client-bundle-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-client-locale", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } - } - } - } - }, - "/api/v1/webhooks/endpoints/{endpointId}": { - "get": { - "tags": ["webhooks"], - "operationId": "webhooks.getWebhookEndpoint", - "parameters": [ - { "name": "endpointId", "in": "path", "schema": { "type": "string" }, "required": true } - ], - "security": [], - "responses": { - "200": { - "description": "WebhookEndpoint", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } - } + ] + }, + "required": false }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + { + "name": "x-client-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-is-backgrounded", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, + "required": true }, - "404": { - "description": "Api/WebhookEndpointNotFoundError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" } - } - } + { + "name": "x-is-debug-build", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true }, - "500": { - "description": "Api/WebhookServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] + { + "name": "x-nonce", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } + ] + }, + "required": false + }, + { + "name": "x-observer-mode", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-platform", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-platform-brand", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-device", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" + ] + }, + "required": true + }, + { + "name": "x-platform-flavor-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-preferred-locales", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-sdk", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "react-native", + "web" + ] + }, + "required": true + }, + { + "name": "x-sdk-version", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-storefront", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } - } - }, - "patch": { - "tags": ["webhooks"], - "operationId": "webhooks.updateWebhookEndpoint", - "parameters": [ - { "name": "endpointId", "in": "path", "schema": { "type": "string" }, "required": true } ], "security": [], "responses": { "200": { - "description": "WebhookEndpoint", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } - } - }, - "400": { - "description": "Api/WebhookValidationError | The request or response did not match the expected schema", + "description": "SdkSchema", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/SdkSchema" } } } }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } - }, - "404": { - "description": "Api/WebhookEndpointNotFoundError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" } - } - } - }, "500": { - "description": "Api/WebhookServiceError", + "description": "Api/AuthenticationError | Api/SchemaServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_SchemaServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2868,90 +3971,270 @@ } } } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateWebhookEndpointBody" } - } - }, - "required": true } - }, - "delete": { - "tags": ["webhooks"], - "operationId": "webhooks.deleteWebhookEndpoint", - "parameters": [ - { "name": "endpointId", "in": "path", "schema": { "type": "string" }, "required": true } + } + }, + "/api/v1/sdk/push-devices/register": { + "post": { + "tags": [ + "sdk" ], - "security": [], - "responses": { - "204": { "description": "" }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + "operationId": "sdk.registerDevice", + "parameters": [ + { + "name": "x-distinct-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "404": { - "description": "Api/WebhookEndpointNotFoundError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" } - } - } + { + "name": "x-client-bundle-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "500": { - "description": "Api/WebhookServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] + { + "name": "x-client-locale", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } + ] + }, + "required": false + }, + { + "name": "x-client-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-is-backgrounded", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, + "required": true + }, + { + "name": "x-is-debug-build", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-nonce", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-observer-mode", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-platform", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-platform-brand", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-device", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" + ] + }, + "required": true + }, + { + "name": "x-platform-flavor-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-preferred-locales", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-sdk", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "react-native", + "web" + ] + }, + "required": true + }, + { + "name": "x-sdk-version", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-storefront", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } - } - } - }, - "/api/v1/webhooks/endpoints/{endpointId}/rotate-secret": { - "post": { - "tags": ["webhooks"], - "operationId": "webhooks.rotateWebhookSecret", - "parameters": [ - { "name": "endpointId", "in": "path", "schema": { "type": "string" }, "required": true } ], "security": [], "responses": { "200": { - "description": "WebhookEndpoint", + "description": "RegisterDeviceResponse", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterDeviceResponse" + } + } } }, "400": { - "description": "The request or response did not match the expected schema", + "description": "Api/PushDeviceValidationError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_PushDeviceValidationError" + } } } }, @@ -2959,29 +4242,42 @@ "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "404": { - "description": "Api/WebhookEndpointNotFoundError", + "description": "Api/PushDeviceNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_PushDeviceNotFoundError" + } } } }, "500": { - "description": "Api/WebhookServiceError", + "description": "Api/AuthenticationError | Api/PushDeviceServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_PushDeviceServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -2989,114 +4285,316 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterDeviceBody" + } + } + }, + "required": true } } }, - "/api/v1/webhooks/endpoints/{endpointId}/test": { + "/api/v1/sdk/push-devices/refresh": { "post": { - "tags": ["webhooks"], - "operationId": "webhooks.testWebhookEndpoint", - "parameters": [ - { "name": "endpointId", "in": "path", "schema": { "type": "string" }, "required": true } + "tags": [ + "sdk" ], - "security": [], - "responses": { - "200": { - "description": "WebhookDelivery", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDelivery" } } - } + "operationId": "sdk.refreshDevice", + "parameters": [ + { + "name": "x-distinct-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } - } - } + { + "name": "x-client-bundle-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "404": { - "description": "Api/WebhookEndpointNotFoundError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" } - } - } + { + "name": "x-client-locale", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false }, - "500": { - "description": "Api/WebhookServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] + { + "name": "x-client-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } + ] + }, + "required": false + }, + { + "name": "x-is-backgrounded", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, + "required": true + }, + { + "name": "x-is-debug-build", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-nonce", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-observer-mode", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-platform", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-platform-brand", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-device", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" + ] + }, + "required": true + }, + { + "name": "x-platform-flavor-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-preferred-locales", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-sdk", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "react-native", + "web" + ] + }, + "required": true + }, + { + "name": "x-sdk-version", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-storefront", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } - } - } - }, - "/api/v1/webhooks/deliveries": { - "get": { - "tags": ["webhooks"], - "operationId": "webhooks.listWebhookDeliveries", - "parameters": [], + ], "security": [], "responses": { "200": { - "description": "Success", + "description": "" + }, + "400": { + "description": "Api/PushDeviceValidationError", "content": { "application/json": { "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/WebhookDelivery" } + "$ref": "#/components/schemas/Api_PushDeviceValidationError" } } } }, - "400": { - "description": "The request or response did not match the expected schema", + "403": { + "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, - "403": { - "description": "Api/ActionForbiddenError", + "404": { + "description": "Api/PushDeviceNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_PushDeviceNotFoundError" + } } } }, "500": { - "description": "Api/WebhookServiceError", + "description": "Api/AuthenticationError | Api/PushDeviceServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_PushDeviceServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -3104,61 +4602,306 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshDeviceBody" + } + } + }, + "required": true } } }, - "/api/v1/webhooks/deliveries/{deliveryId}": { - "get": { - "tags": ["webhooks"], - "operationId": "webhooks.getWebhookDelivery", - "parameters": [ - { "name": "deliveryId", "in": "path", "schema": { "type": "string" }, "required": true } + "/api/v1/sdk/push-devices/unregister": { + "post": { + "tags": [ + "sdk" ], - "security": [], - "responses": { - "200": { - "description": "WebhookDeliveryWithAttempts", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/WebhookDeliveryWithAttempts" } - } - } + "operationId": "sdk.unregisterDevice", + "parameters": [ + { + "name": "x-distinct-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true }, - "400": { - "description": "The request or response did not match the expected schema", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - } - } + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-client-bundle-id", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-client-locale", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-client-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-is-backgrounded", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "false" + ] + }, + "required": true + }, + { + "name": "x-is-debug-build", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-nonce", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-observer-mode", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": true + }, + { + "name": "x-platform", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-platform-brand", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-device", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-flavor", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "native", + "browser" + ] + }, + "required": true + }, + { + "name": "x-platform-flavor-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-platform-version", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-preferred-locales", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "x-sdk", + "in": "header", + "schema": { + "type": "string", + "enum": [ + "react-native", + "web" + ] + }, + "required": true + }, + { + "name": "x-sdk-version", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-storefront", + "in": "header", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "" }, "403": { "description": "Api/ActionForbiddenError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } } } }, "404": { - "description": "Api/WebhookDeliveryNotFoundError", + "description": "Api/PushDeviceNotFoundError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookDeliveryNotFoundError" } + "schema": { + "$ref": "#/components/schemas/Api_PushDeviceNotFoundError" + } } } }, "500": { - "description": "Api/WebhookServiceError", + "description": "Api/AuthenticationError | Api/PushDeviceServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_PushDeviceServiceError" + }, { "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } ] } ] @@ -3166,326 +4909,2230 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnregisterDeviceBody" + } + } + }, + "required": true } } }, - "/api/v1/webhooks/deliveries/{deliveryId}/retry": { - "post": { - "tags": ["webhooks"], - "operationId": "webhooks.retryWebhookDelivery", - "parameters": [ - { "name": "deliveryId", "in": "path", "schema": { "type": "string" }, "required": true } + "/api/v1/users/current": { + "get": { + "tags": [ + "users" ], + "operationId": "users.getUser", + "parameters": [], "security": [], "responses": { "200": { - "description": "WebhookDelivery", + "description": "User", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDelivery" } } + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } } }, - "400": { - "description": "Api/WebhookValidationError | The request or response did not match the expected schema", + "500": { + "description": "Api/AuthenticationError | Api/UserServiceError", "content": { "application/json": { "schema": { "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookValidationError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_UserServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } ] } } } + } + } + } + }, + "/api/v1/payment-provider-configurations": { + "get": { + "tags": [ + "payment_provider_configurations" + ], + "operationId": "payment_provider_configurations.listPaymentProviderConfigurations", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentProviderConfiguration" + } + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/PaymentProviderConfigurationServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_PaymentProviderConfigurationServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/payment-provider-products": { + "get": { + "tags": [ + "payment_provider_products" + ], + "operationId": "payment_provider_products.listPaymentProviderProducts", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentProviderProduct" + } + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/PaymentProviderProductServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_PaymentProviderProductServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/endpoints": { + "post": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.createWebhookEndpoint", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "WebhookEndpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEndpoint" + } + } + } + }, + "400": { + "description": "Api/WebhookValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookValidationError" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookEndpointBody" + } + } + }, + "required": true + } + }, + "get": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.listWebhookEndpoints", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookEndpoint" + } + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/endpoints/{endpointId}": { + "get": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.getWebhookEndpoint", + "parameters": [ + { + "name": "endpointId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookEndpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEndpoint" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookEndpointNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + }, + "patch": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.updateWebhookEndpoint", + "parameters": [ + { + "name": "endpointId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookEndpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEndpoint" + } + } + } + }, + "400": { + "description": "Api/WebhookValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookValidationError" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookEndpointNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookEndpointBody" + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.deleteWebhookEndpoint", + "parameters": [ + { + "name": "endpointId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookEndpointNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/endpoints/{endpointId}/rotate-secret": { + "post": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.rotateWebhookSecret", + "parameters": [ + { + "name": "endpointId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookEndpoint", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEndpoint" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookEndpointNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/endpoints/{endpointId}/test": { + "post": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.testWebhookEndpoint", + "parameters": [ + { + "name": "endpointId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookDelivery", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookEndpointNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookEndpointNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/deliveries": { + "get": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.listWebhookDeliveries", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/deliveries/{deliveryId}": { + "get": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.getWebhookDelivery", + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookDeliveryWithAttempts", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDeliveryWithAttempts" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookDeliveryNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookDeliveryNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v1/webhooks/deliveries/{deliveryId}/retry": { + "post": { + "tags": [ + "webhooks" + ], + "operationId": "webhooks.retryWebhookDelivery", + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "WebhookDelivery", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } + } + }, + "400": { + "description": "Api/WebhookValidationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookValidationError" + } + } + } + }, + "403": { + "description": "Api/ActionForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_ActionForbiddenError" + } + } + } + }, + "404": { + "description": "Api/WebhookDeliveryNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Api_WebhookDeliveryNotFoundError" + } + } + } + }, + "500": { + "description": "Api/WebhookServiceError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_WebhookServiceError" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/Api_AuthenticationError" + }, + { + "$ref": "#/components/schemas/Api_NotAuthenticatedError" + } + ] + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Api_ActionForbiddenError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/ActionForbiddenError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Api_AuthenticationError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/AuthenticationError" + ] + }, + "cause": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "cause", + "message" + ], + "additionalProperties": false + }, + "Api_NotAuthenticatedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/NotAuthenticatedError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CreateSecretKeyBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + } + }, + "required": [ + "name", + "projectId" + ], + "additionalProperties": false + }, + "ApiKeyWithRawKey": { + "type": "object", + "properties": { + "end": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isPublic": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "rawKey": { + "type": "string" + } + }, + "required": [ + "end", + "id", + "isPublic", + "name", + "prefix", + "projectId", + "rawKey" + ], + "additionalProperties": false + }, + "Api_ApiKeyServiceError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/ApiKeyServiceError" + ] + }, + "cause": { + "type": "string" + } + }, + "required": [ + "_tag", + "cause" + ], + "additionalProperties": false + }, + "ApiKey": { + "type": "object", + "properties": { + "end": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isPublic": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "rawKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "end", + "id", + "isPublic", + "name", + "prefix", + "projectId" + ], + "additionalProperties": false + }, + "Api_ApiKeyNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/ApiKeyNotFoundError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CreatePersonBody": { + "type": "object", + "properties": { + "distinctId": { + "type": "string" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "distinctId" + ], + "additionalProperties": false + }, + "Person": { + "type": "object", + "properties": { + "personId": { + "type": "string" + }, + "distinctId": { + "type": "string" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "personId", + "distinctId", + "email", + "name" + ], + "additionalProperties": false + }, + "Api_PersonInvalidAnonymousIdError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PersonInvalidAnonymousIdError" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "id" + ], + "additionalProperties": false + }, + "Api_PersonServiceError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PersonServiceError" + ] + }, + "cause": { + "type": "string" + } + }, + "required": [ + "_tag", + "cause" + ], + "additionalProperties": false + }, + "Api_PersonNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PersonNotFoundError" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "id" + ], + "additionalProperties": false + }, + "SendNotificationBody": { + "type": "object", + "properties": { + "personIds": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "distinctIds": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "body": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "sound": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "badge": { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "priority": { + "anyOf": [ + { + "type": "string", + "enum": [ + "default", + "high" + ] + }, + { + "type": "null" + } + ] + }, + "ttl": { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "channelId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "collapseId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "idempotencyKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "title", + "body" + ], + "additionalProperties": false + }, + "SendNotificationResponse": { + "type": "object", + "properties": { + "pushNotificationSendId": { + "type": "string" }, - "403": { - "description": "Api/ActionForbiddenError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_ActionForbiddenError" } + "deviceCount": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } - } + ] }, - "404": { - "description": "Api/WebhookDeliveryNotFoundError", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Api_WebhookDeliveryNotFoundError" } - } - } + "status": { + "type": "string", + "enum": [ + "pending", + "in_progress", + "succeeded", + "partial_failed", + "failed", + "no_recipients" + ] }, - "500": { - "description": "Api/WebhookServiceError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/Api_WebhookServiceError" }, - { - "anyOf": [ - { "$ref": "#/components/schemas/Api_AuthenticationError" }, - { "$ref": "#/components/schemas/Api_NotAuthenticatedError" } - ] - } - ] - } - } + "unresolvedDistinctIds": { + "type": "array", + "items": { + "type": "string" } } - } - } - } - }, - "components": { - "schemas": { - "Api_ActionForbiddenError": { + }, + "required": [ + "pushNotificationSendId", + "deviceCount", + "status", + "unresolvedDistinctIds" + ], + "additionalProperties": false + }, + "Api_PushSendServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ActionForbiddenError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PushSendServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, - "Api_AuthenticationError": { + "Api_PushDeviceValidationError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/AuthenticationError"] }, - "cause": { "type": "string" }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PushDeviceValidationError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "cause", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, - "Api_NotAuthenticatedError": { + "Api_PushSendNotEnabledError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/NotAuthenticatedError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PushSendNotEnabledError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, - "effect_HttpApiSchemaError": { + "CreateOrganizationBody": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["HttpApiSchemaError"] }, - "message": { "type": "string" } + "name": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "name" + ], "additionalProperties": false }, - "CreateSecretKeyBody": { + "Organization": { "type": "object", - "properties": { "name": { "type": "string" }, "projectId": { "type": "string" } }, - "required": ["name", "projectId"], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug" + ], "additionalProperties": false }, - "ApiKeyWithRawKey": { + "Api_OrganizationServiceError": { "type": "object", "properties": { - "end": { "type": "string" }, - "id": { "type": "string" }, - "isPublic": { "type": "boolean" }, - "name": { "type": "string" }, - "prefix": { "type": "string" }, - "projectId": { "type": "string" }, - "rawKey": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/OrganizationServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["end", "id", "isPublic", "name", "prefix", "projectId", "rawKey"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, - "Api_ApiKeyServiceError": { + "Perk": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ApiKeyServiceError"] }, - "cause": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "id", + "name", + "projectId", + "slug" + ], "additionalProperties": false }, - "ApiKey": { + "Api_PerkServiceError": { "type": "object", "properties": { - "end": { "type": "string" }, - "id": { "type": "string" }, - "isPublic": { "type": "boolean" }, - "name": { "type": "string" }, - "prefix": { "type": "string" }, - "projectId": { "type": "string" }, - "rawKey": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + "_tag": { + "type": "string", + "enum": [ + "Api/PerkServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["end", "id", "isPublic", "name", "prefix", "projectId"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, - "Api_ApiKeyNotFoundError": { + "CreatePaywallDeployResponse": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ApiKeyNotFoundError"] }, - "message": { "type": "string" } + "deployId": { + "type": "string" + }, + "missing": { + "type": "array", + "items": { + "type": "string" + } + } }, - "required": ["_tag", "message"], + "required": [ + "deployId", + "missing" + ], "additionalProperties": false }, - "CreatePersonBody": { + "Api_PaywallDeployServiceError": { "type": "object", "properties": { - "distinctId": { "type": "string" }, - "email": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallDeployServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["distinctId"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, - "Person": { + "Api_PaywallDeployUpgradeRequiredError": { "type": "object", "properties": { - "personId": { "type": "string" }, - "distinctId": { "type": "string" }, - "email": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallDeployUpgradeRequiredError" + ] + }, + "message": { + "type": "string" + }, + "schemaVersion": { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "null" + } + ] + } }, - "required": ["personId", "distinctId", "email", "name"], + "required": [ + "_tag", + "message", + "schemaVersion" + ], "additionalProperties": false }, - "Api_PersonInvalidAnonymousIdError": { + "Api_PaywallDeployValidationError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PersonInvalidAnonymousIdError"] }, - "id": { "type": "string", "allOf": [{ "minLength": 1 }] } + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallDeployValidationError" + ] + }, + "message": { + "type": "string" + }, + "violations": { + "type": "array", + "items": { + "type": "string" + } + } }, - "required": ["_tag", "id"], + "required": [ + "_tag", + "message", + "violations" + ], "additionalProperties": false }, - "Api_PersonServiceError": { + "UploadPaywallDeployBlobResponse": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + }, + "Api_DeployBlobHashMismatchError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PersonServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/DeployBlobHashMismatchError" + ] + }, + "actualSha256": { + "type": "string" + }, + "expectedSha256": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "actualSha256", + "expectedSha256" + ], "additionalProperties": false }, - "Api_PersonNotFoundError": { + "Api_DeployBlobNotDeclaredError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PersonNotFoundError"] }, - "id": { "type": "string", "allOf": [{ "minLength": 1 }] } + "_tag": { + "type": "string", + "enum": [ + "Api/DeployBlobNotDeclaredError" + ] + }, + "sha256": { + "type": "string" + } }, - "required": ["_tag", "id"], + "required": [ + "_tag", + "sha256" + ], "additionalProperties": false }, - "CreateOrganizationBody": { + "Api_PaywallDeployNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallDeployNotFoundError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Api_PaywallDeployNotPendingError": { "type": "object", - "properties": { "name": { "type": "string" } }, - "required": ["name"], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallDeployNotPendingError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, - "Organization": { + "FinalizedPaywallDeployComponent": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "componentId": { + "type": "string" + }, + "contentHash": { + "type": "string" + }, + "id": { + "type": "string" + }, + "version": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } }, - "required": ["id", "name", "slug"], + "required": [ + "componentId", + "contentHash", + "id", + "version" + ], "additionalProperties": false }, - "Api_OrganizationServiceError": { + "FinalizedPaywallDeployPaywall": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/OrganizationServiceError"] }, - "cause": { "type": "string" } + "contentHash": { + "type": "string" + }, + "id": { + "type": "string" + }, + "paywallId": { + "type": "string" + }, + "releaseId": { + "type": "string" + }, + "url": { + "type": "string" + }, + "version": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } }, - "required": ["_tag", "cause"], + "required": [ + "contentHash", + "id", + "paywallId", + "releaseId", + "url", + "version" + ], "additionalProperties": false }, - "Perk": { + "FinalizePaywallDeployResponse": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "projectId": { "type": "string" }, - "slug": { "type": "string" } + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FinalizedPaywallDeployComponent" + } + }, + "deployId": { + "type": "string" + }, + "paywalls": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FinalizedPaywallDeployPaywall" + } + }, + "status": { + "type": "string", + "enum": [ + "ready" + ] + } }, - "required": ["id", "name", "projectId", "slug"], + "required": [ + "components", + "deployId", + "paywalls", + "status" + ], "additionalProperties": false }, - "Api_PerkServiceError": { + "Api_IncompleteDeployError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PerkServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/IncompleteDeployError" + ] + }, + "missing": { + "type": "array", + "items": { + "type": "string" + } + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "missing" + ], "additionalProperties": false }, "PaywallLocation": { "type": "object", "properties": { - "description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "id": { "type": "string" }, - "name": { "type": "string" }, - "projectId": { "type": "string" }, - "slug": { "type": "string" } + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["description", "id", "name", "projectId", "slug"], + "required": [ + "description", + "id", + "name", + "projectId", + "slug" + ], "additionalProperties": false }, "Api_PaywallLocationServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PaywallLocationServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PaywallLocationServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "SchemaLocation": { "type": "object", "properties": { - "description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["description", "name", "slug"], + "required": [ + "description", + "name", + "slug" + ], "additionalProperties": false }, "SchemaPerk": { "type": "object", - "properties": { "name": { "type": "string" }, "slug": { "type": "string" } }, - "required": ["name", "slug"], + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "name", + "slug" + ], "additionalProperties": false }, "SchemaProductProvider": { "type": "object", "properties": { - "configuration": { "type": "object", "additionalProperties": { "type": "null" } }, + "configuration": { + "type": "object" + }, "providerId": { - "anyOf": [ - { "type": "string", "enum": ["appleAppStore"] }, - { "type": "string", "enum": ["googlePlay"] } + "type": "string", + "enum": [ + "appleAppStore", + "googlePlay" ] } }, - "required": ["configuration", "providerId"], + "required": [ + "configuration", + "providerId" + ], "additionalProperties": false }, "SchemaProduct": { "type": "object", "properties": { - "name": { "type": "string" }, - "perks": { "type": "array", "items": { "type": "string" } }, + "name": { + "type": "string" + }, + "perks": { + "type": "array", + "items": { + "type": "string" + } + }, "providers": { "type": "array", - "items": { "$ref": "#/components/schemas/SchemaProductProvider" } + "items": { + "$ref": "#/components/schemas/SchemaProductProvider" + } + }, + "slug": { + "type": "string" }, - "slug": { "type": "string" }, "type": { "type": "string", - "enum": ["subscription", "one-time", "one-time-consumable"] + "enum": [ + "subscription", + "one-time", + "one-time-consumable" + ] } }, - "required": ["name", "perks", "providers", "slug", "type"], + "required": [ + "name", + "perks", + "providers", + "slug", + "type" + ], "additionalProperties": false }, "ProjectSchemaResponse": { @@ -3494,157 +7141,325 @@ "enabledProviders": { "type": "array", "items": { - "anyOf": [ - { "type": "string", "enum": ["appleAppStore"] }, - { "type": "string", "enum": ["googlePlay"] } + "type": "string", + "enum": [ + "appleAppStore", + "googlePlay" ] } }, "locations": { "type": "array", - "items": { "$ref": "#/components/schemas/SchemaLocation" } + "items": { + "$ref": "#/components/schemas/SchemaLocation" + } + }, + "perks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchemaPerk" + } }, - "perks": { "type": "array", "items": { "$ref": "#/components/schemas/SchemaPerk" } }, "products": { "type": "array", - "items": { "$ref": "#/components/schemas/SchemaProduct" } + "items": { + "$ref": "#/components/schemas/SchemaProduct" + } }, - "version": { "type": "string" } + "version": { + "type": "string" + } }, - "required": ["enabledProviders", "locations", "perks", "products", "version"], + "required": [ + "enabledProviders", + "locations", + "perks", + "products", + "version" + ], "additionalProperties": false }, "Api_SchemaServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/SchemaServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/SchemaServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "SchemaVersion": { "type": "object", - "properties": { "version": { "type": "string" } }, - "required": ["version"], + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], "additionalProperties": false }, "CreateProjectBody": { "type": "object", - "properties": { "name": { "type": "string" }, "organizationId": { "type": "string" } }, - "required": ["name", "organizationId"], + "properties": { + "name": { + "type": "string" + }, + "organizationId": { + "type": "string" + } + }, + "required": [ + "name", + "organizationId" + ], "additionalProperties": false }, "Project": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "name", "slug"], + "required": [ + "id", + "name", + "slug" + ], "additionalProperties": false }, "Api_ProjectServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ProjectServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/ProjectServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "Product": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "projectId": { "type": "string" }, - "slug": { "type": "string" }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "slug": { + "type": "string" + }, "type": { - "anyOf": [ - { "type": "string", "enum": ["subscription"] }, - { "type": "string", "enum": ["one-time"] }, - { "type": "string", "enum": ["one-time-consumable"] } + "type": "string", + "enum": [ + "subscription", + "one-time", + "one-time-consumable" ] } }, - "required": ["id", "name", "projectId", "slug", "type"], + "required": [ + "id", + "name", + "projectId", + "slug", + "type" + ], "additionalProperties": false }, "Api_ProductServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ProductServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/ProductServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "ProductPerk": { "type": "object", "properties": { - "id": { "type": "string" }, - "perkId": { "type": "string" }, - "productId": { "type": "string" } + "id": { + "type": "string" + }, + "perkId": { + "type": "string" + }, + "productId": { + "type": "string" + } }, - "required": ["id", "perkId", "productId"], + "required": [ + "id", + "perkId", + "productId" + ], "additionalProperties": false }, "Api_ProductPerkServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ProductPerkServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/ProductPerkServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "Api_ProductPerkValidationError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/ProductPerkValidationError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/ProductPerkValidationError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "SdkEntitlementGrant": { "type": "object", "properties": { - "expiresAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "perkId": { "type": "string" }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "perkId": { + "type": "string" + }, "source": { + "type": "string", + "enum": [ + "subscription", + "purchase", + "manual" + ] + }, + "sourceId": { "anyOf": [ - { "type": "string", "enum": ["subscription"] }, - { "type": "string", "enum": ["purchase"] }, - { "type": "string", "enum": ["manual"] } + { + "type": "string" + }, + { + "type": "null" + } ] }, - "sourceId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "sourcePersonId": { "type": "string" }, + "sourcePersonId": { + "type": "string" + }, "status": { - "anyOf": [ - { "type": "string", "enum": ["active"] }, - { "type": "string", "enum": ["expired"] } + "type": "string", + "enum": [ + "active", + "expired" ] } }, - "required": ["expiresAt", "perkId", "source", "sourceId", "sourcePersonId", "status"], + "required": [ + "expiresAt", + "perkId", + "source", + "sourceId", + "sourcePersonId", + "status" + ], "additionalProperties": false }, "SdkPurchaseHistoryEntry": { "type": "object", "properties": { - "createdAt": { "type": "string" }, - "productId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "providerKey": { "type": "string" }, - "purchaseId": { "type": "string" }, - "sourcePersonId": { "type": "string" }, - "type": { + "createdAt": { + "type": "string" + }, + "productId": { "anyOf": [ - { "type": "string", "enum": ["one_time"] }, - { "type": "string", "enum": ["subscription"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "providerKey": { + "type": "string" + }, + "purchaseId": { + "type": "string" + }, + "sourcePersonId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "one_time", + "subscription" ] } }, @@ -3661,41 +7476,110 @@ "SdkCurrentSubscription": { "type": "object", "properties": { - "expiresAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "productId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "productId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "status": { + "type": "string", + "enum": [ + "none", + "active", + "canceled", + "past_due", + "trialing" + ] + }, + "subscriptionId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expiresAt", + "productId", + "status", + "subscriptionId" + ], + "additionalProperties": false + }, + "SdkSubscriptionHistoryEntry": { + "type": "object", + "properties": { + "canceledAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isTrial": { + "type": "boolean" + }, + "productId": { "anyOf": [ - { "type": "string", "enum": ["none"] }, - { "type": "string", "enum": ["active"] }, - { "type": "string", "enum": ["canceled"] }, - { "type": "string", "enum": ["past_due"] }, - { "type": "string", "enum": ["trialing"] } + { + "type": "string" + }, + { + "type": "null" + } ] }, - "subscriptionId": { "anyOf": [{ "type": "string" }, { "type": "null" }] } - }, - "required": ["expiresAt", "productId", "status", "subscriptionId"], - "additionalProperties": false - }, - "SdkSubscriptionHistoryEntry": { - "type": "object", - "properties": { - "canceledAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "expiresAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "isTrial": { "type": "boolean" }, - "productId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "sourcePersonId": { "type": "string" }, - "startsAt": { "type": "string" }, + "sourcePersonId": { + "type": "string" + }, + "startsAt": { + "type": "string" + }, "status": { - "anyOf": [ - { "type": "string", "enum": ["active"] }, - { "type": "string", "enum": ["canceled"] }, - { "type": "string", "enum": ["expired"] }, - { "type": "string", "enum": ["trialing"] }, - { "type": "string", "enum": ["past_due"] } + "type": "string", + "enum": [ + "active", + "canceled", + "expired", + "trialing", + "past_due" ] }, - "subscriptionId": { "type": "string" } + "subscriptionId": { + "type": "string" + } }, "required": [ "canceledAt", @@ -3712,45 +7596,94 @@ "SdkPerson": { "type": "object", "properties": { - "distinctId": { "type": "string" }, - "email": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "distinctId": { + "type": "string" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "entitlements": { "type": "object", "properties": { "grants": { "type": "array", - "items": { "$ref": "#/components/schemas/SdkEntitlementGrant" } + "items": { + "$ref": "#/components/schemas/SdkEntitlementGrant" + } } }, - "required": ["grants"], + "required": [ + "grants" + ], "additionalProperties": false }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "personId": { "type": "string" }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "personId": { + "type": "string" + }, "purchases": { "type": "object", "properties": { "history": { "type": "array", - "items": { "$ref": "#/components/schemas/SdkPurchaseHistoryEntry" } + "items": { + "$ref": "#/components/schemas/SdkPurchaseHistoryEntry" + } } }, - "required": ["history"], + "required": [ + "history" + ], "additionalProperties": false }, "snapshotContext": { "type": "object", "properties": { - "includedPersonIds": { "type": "array", "items": { "type": "string" } }, - "migrationJobId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "mode": { + "includedPersonIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "migrationJobId": { "anyOf": [ - { "type": "string", "enum": ["persisted"] }, - { "type": "string", "enum": ["temporary_pending_transfer"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mode": { + "type": "string", + "enum": [ + "persisted", + "temporary_pending_transfer" ] } }, - "required": ["includedPersonIds", "migrationJobId", "mode"], + "required": [ + "includedPersonIds", + "migrationJobId", + "mode" + ], "additionalProperties": false }, "subscriptions": { @@ -3758,16 +7691,25 @@ "properties": { "current": { "anyOf": [ - { "$ref": "#/components/schemas/SdkCurrentSubscription" }, - { "type": "null" } + { + "$ref": "#/components/schemas/SdkCurrentSubscription" + }, + { + "type": "null" + } ] }, "history": { "type": "array", - "items": { "$ref": "#/components/schemas/SdkSubscriptionHistoryEntry" } + "items": { + "$ref": "#/components/schemas/SdkSubscriptionHistoryEntry" + } } }, - "required": ["current", "history"], + "required": [ + "current", + "history" + ], "additionalProperties": false } }, @@ -3786,98 +7728,322 @@ "Api_SdkServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/SdkServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/SdkServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "Api_SdkPersonNotFoundError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/SdkPersonNotFoundError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/SdkPersonNotFoundError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "Api_SdkValidationError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/SdkValidationError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/SdkValidationError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "SdkIdentifyBody": { "type": "object", "properties": { - "distinctId": { "type": "string" }, - "email": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "distinctId": { + "type": "string" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "traits": { "anyOf": [ { "type": "object", "additionalProperties": { "anyOf": [ - { "type": "string" }, + { + "type": "string" + }, { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - { "type": "boolean" }, - { "type": "null" } + { + "type": "boolean" + }, + { + "type": "null" + } ] } }, - { "type": "null" } + { + "type": "null" + } ] } }, - "required": ["distinctId"], + "required": [ + "distinctId" + ], "additionalProperties": false }, "Api_SdkPersonAlreadyIdentifiedError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/SdkPersonAlreadyIdentifiedError"] }, - "distinctId": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/SdkPersonAlreadyIdentifiedError" + ] + }, + "distinctId": { + "type": "string" + } }, - "required": ["_tag", "distinctId"], + "required": [ + "_tag", + "distinctId" + ], "additionalProperties": false }, "SdkSyncPersonAttributesBody": { "type": "object", "properties": { - "email": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "traits": { "anyOf": [ { "type": "object", "additionalProperties": { "anyOf": [ - { "type": "string" }, + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "setOnce": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - { "type": "boolean" }, - { "type": "null" } + { + "type": "boolean" + }, + { + "type": "null" + } ] } }, - { "type": "null" } + { + "type": "null" + } + ] + }, + "clientEventId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] } }, @@ -3885,15 +8051,31 @@ }, "SdkSyncTransactionResponse": { "type": "object", - "properties": { "accepted": { "type": "boolean" } }, - "required": ["accepted"], + "properties": { + "accepted": { + "type": "boolean" + } + }, + "required": [ + "accepted" + ], "additionalProperties": false }, "EvaluateFeatureFlagsBody": { "type": "object", "properties": { "flagKeys": { - "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "null" }] + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -3901,12 +8083,37 @@ "SdkFeatureFlagResult": { "type": "object", "properties": { - "enabled": { "type": "boolean" }, - "key": { "type": "string" }, - "payload": { "anyOf": [{ "type": "null" }, { "type": "null" }] }, - "variantKey": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + "enabled": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "payload": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "variantKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } }, - "required": ["enabled", "key", "payload", "variantKey"], + "required": [ + "enabled", + "key", + "payload", + "variantKey" + ], "additionalProperties": false }, "SdkFeatureFlagsResponse": { @@ -3914,67 +8121,239 @@ "properties": { "flags": { "type": "array", - "items": { "$ref": "#/components/schemas/SdkFeatureFlagResult" } + "items": { + "$ref": "#/components/schemas/SdkFeatureFlagResult" + } } }, - "required": ["flags"], + "required": [ + "flags" + ], "additionalProperties": false }, "SdkResolvePaywallBody": { "type": "object", - "properties": { "locationSlug": { "type": "string" } }, - "required": ["locationSlug"], + "properties": { + "locationSlug": { + "type": "string" + } + }, + "required": [ + "locationSlug" + ], "additionalProperties": false }, "SdkResolvedPaywallShowing": { "type": "object", "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "paywall": { "anyOf": [ { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "name", "slug"], + "required": [ + "id", + "name", + "slug" + ], "additionalProperties": false }, - { "type": "null" } + { + "type": "null" + } + ] + }, + "paywallId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] }, - "paywallId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, "paywallRelease": { "anyOf": [ { "type": "object", "properties": { - "htmlUrl": { "type": "string" }, - "publishedAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "releaseId": { "type": "string" }, + "htmlUrl": { + "type": "string" + }, + "publishedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "releaseId": { + "type": "string" + }, + "runtime": { + "anyOf": [ + { + "type": "object", + "properties": { + "contentHash": { + "type": "string" + }, + "productSlugs": { + "type": "array", + "items": { + "type": "string" + } + }, + "variables": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + } + ] + } + } + }, + "required": [ + "contentHash", + "productSlugs", + "variables" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, "version": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] } }, - "required": ["htmlUrl", "publishedAt", "releaseId", "version"], + "required": [ + "htmlUrl", + "publishedAt", + "releaseId", + "runtime", + "version" + ], "additionalProperties": false }, - { "type": "null" } + { + "type": "null" + } ] }, - "paywallReleaseId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "startedAt": { "type": "string" }, - "type": { + "paywallReleaseId": { "anyOf": [ - { "type": "string", "enum": ["paywall_release"] }, - { "type": "string", "enum": ["feature_flag"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "startedAt": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "paywall_release", + "feature_flag" ] } }, @@ -3995,32 +8374,74 @@ "location": { "type": "object", "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "name", "slug"], + "required": [ + "id", + "name", + "slug" + ], "additionalProperties": false }, - "showing": { "$ref": "#/components/schemas/SdkResolvedPaywallShowing" } + "showing": { + "$ref": "#/components/schemas/SdkResolvedPaywallShowing" + } }, - "required": ["location", "showing"], + "required": [ + "location", + "showing" + ], "additionalProperties": false }, "SdkSchemaLocation": { "type": "object", "properties": { - "description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["description", "name", "slug"], + "required": [ + "description", + "name", + "slug" + ], "additionalProperties": false }, "SdkSchemaPerk": { "type": "object", - "properties": { "name": { "type": "string" }, "slug": { "type": "string" } }, - "required": ["name", "slug"], + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "name", + "slug" + ], "additionalProperties": false }, "SdkSchemaProduct": { @@ -4031,43 +8452,76 @@ "properties": { "perks": { "type": "object", - "additionalProperties": { "type": "boolean", "enum": [true] } + "additionalProperties": { + "type": "boolean", + "enum": [ + true + ] + } }, "providers": { "type": "object", "properties": { "appleAppStore": { "anyOf": [ - { "type": "object", "additionalProperties": { "type": "null" } }, - { "type": "null" } + { + "type": "object" + }, + { + "type": "null" + } ] }, "googlePlay": { "anyOf": [ - { "type": "object", "additionalProperties": { "type": "null" } }, - { "type": "null" } + { + "type": "object" + }, + { + "type": "null" + } ] } }, "additionalProperties": false } }, - "required": ["perks", "providers"], + "required": [ + "perks", + "providers" + ], "additionalProperties": false }, "properties": { "type": "object", - "properties": { "name": { "type": "string" } }, - "required": ["name"], + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], "additionalProperties": false }, - "slug": { "type": "string" }, + "slug": { + "type": "string" + }, "type": { "type": "string", - "enum": ["subscription", "one-time", "one-time-consumable"] + "enum": [ + "subscription", + "one-time", + "one-time-consumable" + ] } }, - "required": ["configuration", "properties", "slug", "type"], + "required": [ + "configuration", + "properties", + "slug", + "type" + ], "additionalProperties": false }, "SdkSchema": { @@ -4075,41 +8529,254 @@ "properties": { "locations": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/SdkSchemaLocation" } + "additionalProperties": { + "$ref": "#/components/schemas/SdkSchemaLocation" + } }, "perks": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/SdkSchemaPerk" } + "additionalProperties": { + "$ref": "#/components/schemas/SdkSchemaPerk" + } }, "products": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/SdkSchemaProduct" } + "additionalProperties": { + "$ref": "#/components/schemas/SdkSchemaProduct" + } + }, + "version": { + "type": "string" + } + }, + "required": [ + "locations", + "perks", + "products", + "version" + ], + "additionalProperties": false + }, + "RegisterDeviceBody": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "enum": [ + "ios", + "android" + ] + }, + "provider": { + "type": "string", + "enum": [ + "fcm", + "apns" + ] + }, + "platformToken": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "bundleId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "environment": { + "anyOf": [ + { + "type": "string", + "enum": [ + "sandbox", + "production" + ] + }, + { + "type": "null" + } + ] + }, + "previousPushDeviceTokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "platform", + "provider", + "platformToken" + ], + "additionalProperties": false + }, + "RegisterDeviceResponse": { + "type": "object", + "properties": { + "pushDeviceTokenId": { + "type": "string" + } + }, + "required": [ + "pushDeviceTokenId" + ], + "additionalProperties": false + }, + "Api_PushDeviceServiceError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PushDeviceServiceError" + ] + }, + "cause": { + "type": "string" + } + }, + "required": [ + "_tag", + "cause" + ], + "additionalProperties": false + }, + "Api_PushDeviceNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Api/PushDeviceNotFoundError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "RefreshDeviceBody": { + "type": "object", + "properties": { + "pushDeviceTokenId": { + "type": "string" }, - "version": { "type": "string" } + "platformToken": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "pushDeviceTokenId", + "platformToken" + ], + "additionalProperties": false + }, + "UnregisterDeviceBody": { + "type": "object", + "properties": { + "pushDeviceTokenId": { + "type": "string" + } }, - "required": ["locations", "perks", "products", "version"], + "required": [ + "pushDeviceTokenId" + ], "additionalProperties": false }, "User": { "type": "object", "properties": { - "createdAt": { "type": "string" }, - "email": { "type": "string" }, - "emailVerified": { "type": "boolean" }, - "id": { "type": "string" }, - "image": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, "organizations": { "type": "array", "items": { "type": "object", "properties": { - "id": { "type": "string" }, - "logo": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "logo": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "workosOrganizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } }, - "required": ["id", "logo", "name", "slug"], + "required": [ + "id", + "logo", + "name", + "slug", + "workosOrganizationId" + ], "additionalProperties": false } }, @@ -4118,17 +8785,42 @@ "items": { "type": "object", "properties": { - "id": { "type": "string" }, - "logo": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, - "organizationId": { "type": "string" }, - "slug": { "type": "string" } + "id": { + "type": "string" + }, + "logo": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "slug": { + "type": "string" + } }, - "required": ["id", "logo", "name", "organizationId", "slug"], + "required": [ + "id", + "logo", + "name", + "organizationId", + "slug" + ], "additionalProperties": false } }, - "updatedAt": { "type": "string" } + "updatedAt": { + "type": "string" + } }, "required": [ "createdAt", @@ -4146,41 +8838,87 @@ "Api_UserServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/UserServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/UserServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "PaymentProviderConfiguration": { "type": "object", "properties": { - "enabled": { "type": "boolean" }, - "id": { "type": "string" }, - "name": { "type": "string" }, - "projectId": { "type": "string" }, - "providerId": { "type": "string" } + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "providerId": { + "type": "string" + } }, - "required": ["enabled", "id", "name", "projectId", "providerId"], + "required": [ + "enabled", + "id", + "name", + "projectId", + "providerId" + ], "additionalProperties": false }, "Api_PaymentProviderConfigurationServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PaymentProviderConfigurationServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PaymentProviderConfigurationServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "PaymentProviderProduct": { "type": "object", "properties": { - "configuration": { "type": "object", "additionalProperties": { "type": "null" } }, - "id": { "type": "string" }, - "paymentProviderConfigurationId": { "type": "string" }, - "productId": { "type": "string" }, - "providerId": { "type": "string" } + "configuration": { + "type": "object" + }, + "id": { + "type": "string" + }, + "paymentProviderConfigurationId": { + "type": "string" + }, + "productId": { + "type": "string" + }, + "providerId": { + "type": "string" + } }, "required": [ "configuration", @@ -4194,21 +8932,53 @@ "Api_PaymentProviderProductServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/PaymentProviderProductServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/PaymentProviderProductServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "CreateWebhookEndpointBody": { "type": "object", "properties": { - "description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "events": { "type": "array", "items": { "type": "string" } }, - "name": { "type": "string" }, - "url": { "type": "string" } + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "url": { + "type": "string" + } }, - "required": ["events", "name", "url"], + "required": [ + "events", + "name", + "url" + ], "additionalProperties": false }, "WebhookEndpoint": { @@ -4216,43 +8986,111 @@ "properties": { "consecutiveFailures": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "person.created", + "person.updated", + "person.deleted", + "subscription.created", + "subscription.renewed", + "subscription.cancelled", + "subscription.expired", + "purchase.completed", + "purchase.refunded" + ] + } + }, + "id": { + "type": "string" + }, + "lastSuccessAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] }, - "createdAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "events": { - "type": "array", - "items": { - "anyOf": [ - { "type": "string", "enum": ["person.created"] }, - { "type": "string", "enum": ["person.updated"] }, - { "type": "string", "enum": ["person.deleted"] }, - { "type": "string", "enum": ["subscription.created"] }, - { "type": "string", "enum": ["subscription.renewed"] }, - { "type": "string", "enum": ["subscription.cancelled"] }, - { "type": "string", "enum": ["subscription.expired"] }, - { "type": "string", "enum": ["purchase.completed"] }, - { "type": "string", "enum": ["purchase.refunded"] } - ] - } - }, - "id": { "type": "string" }, - "lastSuccessAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "name": { "type": "string" }, - "projectId": { "type": "string" }, - "secret": { "type": "string" }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "secret": { + "type": "string" + }, "status": { - "anyOf": [ - { "type": "string", "enum": ["active"] }, - { "type": "string", "enum": ["disabled"] }, - { "type": "string", "enum": ["failed"] } + "type": "string", + "enum": [ + "active", + "disabled", + "failed" ] }, - "url": { "type": "string" } + "url": { + "type": "string" + } }, "required": [ "consecutiveFailures", @@ -4272,52 +9110,127 @@ "Api_WebhookValidationError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/WebhookValidationError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/WebhookValidationError" + ] + }, + "message": { + "type": "string" + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "Api_WebhookServiceError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/WebhookServiceError"] }, - "cause": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/WebhookServiceError" + ] + }, + "cause": { + "type": "string" + } }, - "required": ["_tag", "cause"], + "required": [ + "_tag", + "cause" + ], "additionalProperties": false }, "Api_WebhookEndpointNotFoundError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/WebhookEndpointNotFoundError"] }, - "endpointId": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/WebhookEndpointNotFoundError" + ] + }, + "endpointId": { + "type": "string" + } }, - "required": ["_tag", "endpointId"], + "required": [ + "_tag", + "endpointId" + ], "additionalProperties": false }, "UpdateWebhookEndpointBody": { "type": "object", "properties": { "description": { - "anyOf": [{ "anyOf": [{ "type": "string" }, { "type": "null" }] }, { "type": "null" }] + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] }, "events": { - "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "null" }] + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "name": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, "status": { "anyOf": [ { - "anyOf": [ - { "type": "string", "enum": ["active"] }, - { "type": "string", "enum": ["disabled"] } + "type": "string", + "enum": [ + "active", + "disabled" ] }, - { "type": "null" } + { + "type": "null" + } ] }, - "url": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } }, "additionalProperties": false }, @@ -4326,38 +9239,134 @@ "properties": { "attemptCount": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "completedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] }, - "completedAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "createdAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "eventOccurredAt": { "type": "string" }, - "eventType": { "type": "string" }, - "id": { "type": "string" }, + "eventOccurredAt": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "id": { + "type": "string" + }, "maxAttempts": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - "nextAttemptAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "payload": { "type": "null" }, - "projectId": { "type": "string" }, - "status": { + "nextAttemptAt": { "anyOf": [ - { "type": "string", "enum": ["pending"] }, - { "type": "string", "enum": ["in_progress"] }, - { "type": "string", "enum": ["succeeded"] }, - { "type": "string", "enum": ["failed"] }, - { "type": "string", "enum": ["exhausted"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "payload": {}, + "projectId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "in_progress", + "succeeded", + "failed", + "exhausted" ] }, - "webhookEndpointId": { "type": "string" } + "webhookEndpointId": { + "type": "string" + } }, "required": [ "attemptCount", @@ -4380,43 +9389,165 @@ "properties": { "attemptNumber": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] }, - "createdAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, "durationMs": { "anyOf": [ { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - { "type": "null" } + { + "type": "null" + } + ] + }, + "errorMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] }, - "errorMessage": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "id": { "type": "string" }, - "responseBody": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, "statusCode": { "anyOf": [ { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - { "type": "null" } + { + "type": "null" + } ] }, - "succeeded": { "type": "boolean" } + "succeeded": { + "type": "boolean" + } }, "required": [ "attemptNumber", @@ -4435,42 +9566,140 @@ "properties": { "attemptCount": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, "attempts": { "type": "array", - "items": { "$ref": "#/components/schemas/WebhookDeliveryAttempt" } + "items": { + "$ref": "#/components/schemas/WebhookDeliveryAttempt" + } + }, + "completedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventOccurredAt": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "id": { + "type": "string" }, - "completedAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "createdAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "eventOccurredAt": { "type": "string" }, - "eventType": { "type": "string" }, - "id": { "type": "string" }, "maxAttempts": { "anyOf": [ - { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } ] }, - "nextAttemptAt": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, - "payload": { "type": "null" }, - "projectId": { "type": "string" }, - "status": { + "nextAttemptAt": { "anyOf": [ - { "type": "string", "enum": ["pending"] }, - { "type": "string", "enum": ["in_progress"] }, - { "type": "string", "enum": ["succeeded"] }, - { "type": "string", "enum": ["failed"] }, - { "type": "string", "enum": ["exhausted"] } + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "payload": {}, + "projectId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "in_progress", + "succeeded", + "failed", + "exhausted" ] }, - "webhookEndpointId": { "type": "string" } + "webhookEndpointId": { + "type": "string" + } }, "required": [ "attemptCount", @@ -4492,10 +9721,20 @@ "Api_WebhookDeliveryNotFoundError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["Api/WebhookDeliveryNotFoundError"] }, - "deliveryId": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "Api/WebhookDeliveryNotFoundError" + ] + }, + "deliveryId": { + "type": "string" + } }, - "required": ["_tag", "deliveryId"], + "required": [ + "_tag", + "deliveryId" + ], "additionalProperties": false } }, @@ -4503,20 +9742,56 @@ }, "security": [], "tags": [ - { "name": "auth" }, - { "name": "api_keys" }, - { "name": "persons" }, - { "name": "organizations" }, - { "name": "perks" }, - { "name": "paywall_locations" }, - { "name": "schema" }, - { "name": "projects" }, - { "name": "products" }, - { "name": "product_perks" }, - { "name": "sdk" }, - { "name": "users" }, - { "name": "payment_provider_configurations" }, - { "name": "payment_provider_products" }, - { "name": "webhooks" } + { + "name": "auth" + }, + { + "name": "api_keys" + }, + { + "name": "persons" + }, + { + "name": "notifications" + }, + { + "name": "organizations" + }, + { + "name": "perks" + }, + { + "name": "paywall_deploys" + }, + { + "name": "paywall_locations" + }, + { + "name": "schema" + }, + { + "name": "projects" + }, + { + "name": "products" + }, + { + "name": "product_perks" + }, + { + "name": "sdk" + }, + { + "name": "users" + }, + { + "name": "payment_provider_configurations" + }, + { + "name": "payment_provider_products" + }, + { + "name": "webhooks" + } ] } diff --git a/packages/generated-clients/openapi/event-capture.json b/packages/generated-clients/openapi/event-capture.json index 7baa97abb..addecb6f1 100644 --- a/packages/generated-clients/openapi/event-capture.json +++ b/packages/generated-clients/openapi/event-capture.json @@ -1,31 +1,35 @@ { "openapi": "3.1.0", - "info": { "title": "Api", "version": "0.0.1" }, + "info": { + "title": "Api", + "version": "0.0.1" + }, "paths": { "/i/v1/capture": { "post": { - "tags": ["event_capture"], + "tags": [ + "event_capture" + ], "operationId": "event_capture.capture", "parameters": [], "security": [], "responses": { - "200": { + "202": { "description": "CaptureAcceptedResponse", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureAcceptedResponse" } + "schema": { + "$ref": "#/components/schemas/CaptureAcceptedResponse" + } } } }, "400": { - "description": "CaptureInvalidRequestError | The request or response did not match the expected schema", + "description": "CaptureInvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/CaptureInvalidRequestError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/CaptureInvalidRequestError" } } } @@ -34,7 +38,9 @@ "description": "CaptureUnauthorizedError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureUnauthorizedError" } + "schema": { + "$ref": "#/components/schemas/CaptureUnauthorizedError" + } } } }, @@ -42,7 +48,9 @@ "description": "CapturePayloadTooLargeError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CapturePayloadTooLargeError" } + "schema": { + "$ref": "#/components/schemas/CapturePayloadTooLargeError" + } } } }, @@ -50,7 +58,9 @@ "description": "CaptureRateLimitedError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureRateLimitedError" } + "schema": { + "$ref": "#/components/schemas/CaptureRateLimitedError" + } } } }, @@ -58,7 +68,9 @@ "description": "CaptureInternalServerError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureInternalServerError" } + "schema": { + "$ref": "#/components/schemas/CaptureInternalServerError" + } } } }, @@ -66,7 +78,9 @@ "description": "CaptureDependencyUnavailableError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureDependencyUnavailableError" } + "schema": { + "$ref": "#/components/schemas/CaptureDependencyUnavailableError" + } } } } @@ -77,31 +91,78 @@ "schema": { "type": "object", "properties": { - "uuid": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "event": { "type": "string", "allOf": [{ "minLength": 1 }] }, + "uuid": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "event": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, "context": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_" + } }, "properties": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_1" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_1" + } + }, + "distinct_id": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] }, - "distinct_id": { "type": "string", "allOf": [{ "minLength": 1 }] }, "session_id": { "anyOf": [ - { "type": "string", "allOf": [{ "minLength": 1 }] }, - { "type": "null" } + { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + { + "type": "null" + } ] }, "timestamp": { "anyOf": [ - { "type": "string", "allOf": [{ "format": "date-time" }] }, - { "type": "null" } + { + "type": "string" + }, + { + "type": "null" + } ] }, - "sent_at": { "type": "string", "allOf": [{ "format": "date-time" }] }, - "token": { "type": "string", "allOf": [{ "minLength": 1 }] } + "sent_at": { + "type": "string" + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } }, "required": [ "uuid", @@ -122,28 +183,29 @@ }, "/i/v1/batch": { "post": { - "tags": ["event_capture"], + "tags": [ + "event_capture" + ], "operationId": "event_capture.batch", "parameters": [], "security": [], "responses": { - "200": { + "202": { "description": "CaptureAcceptedResponse", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureAcceptedResponse" } + "schema": { + "$ref": "#/components/schemas/CaptureAcceptedResponse" + } } } }, "400": { - "description": "CaptureInvalidRequestError | The request or response did not match the expected schema", + "description": "CaptureInvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/CaptureInvalidRequestError" }, - { "$ref": "#/components/schemas/effect_HttpApiSchemaError" } - ] + "$ref": "#/components/schemas/CaptureInvalidRequestError" } } } @@ -152,7 +214,9 @@ "description": "CaptureUnauthorizedError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureUnauthorizedError" } + "schema": { + "$ref": "#/components/schemas/CaptureUnauthorizedError" + } } } }, @@ -160,7 +224,9 @@ "description": "CapturePayloadTooLargeError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CapturePayloadTooLargeError" } + "schema": { + "$ref": "#/components/schemas/CapturePayloadTooLargeError" + } } } }, @@ -168,7 +234,9 @@ "description": "CaptureRateLimitedError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureRateLimitedError" } + "schema": { + "$ref": "#/components/schemas/CaptureRateLimitedError" + } } } }, @@ -176,7 +244,9 @@ "description": "CaptureInternalServerError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureInternalServerError" } + "schema": { + "$ref": "#/components/schemas/CaptureInternalServerError" + } } } }, @@ -184,7 +254,9 @@ "description": "CaptureDependencyUnavailableError", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaptureDependencyUnavailableError" } + "schema": { + "$ref": "#/components/schemas/CaptureDependencyUnavailableError" + } } } } @@ -201,31 +273,75 @@ { "type": "object", "properties": { - "uuid": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "event": { "type": "string", "allOf": [{ "minLength": 1 }] }, + "uuid": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "event": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, "context": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_" + } }, "properties": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_1" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_1" + } + }, + "distinct_id": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] }, - "distinct_id": { "type": "string", "allOf": [{ "minLength": 1 }] }, "session_id": { "anyOf": [ - { "type": "string", "allOf": [{ "minLength": 1 }] }, - { "type": "null" } + { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + { + "type": "null" + } ] }, "timestamp": { "anyOf": [ - { "type": "string", "allOf": [{ "format": "date-time" }] }, - { "type": "null" } + { + "type": "string" + }, + { + "type": "null" + } ] } }, - "required": ["uuid", "event", "context", "properties", "distinct_id"], + "required": [ + "uuid", + "event", + "context", + "properties", + "distinct_id" + ], "additionalProperties": false } ], @@ -233,38 +349,413 @@ "items": { "type": "object", "properties": { - "uuid": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "event": { "type": "string", "allOf": [{ "minLength": 1 }] }, + "uuid": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "event": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, "context": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_" + } }, "properties": { "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Union_1" } + "additionalProperties": { + "$ref": "#/components/schemas/Union_1" + } + }, + "distinct_id": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] }, - "distinct_id": { "type": "string", "allOf": [{ "minLength": 1 }] }, "session_id": { "anyOf": [ - { "type": "string", "allOf": [{ "minLength": 1 }] }, - { "type": "null" } + { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + { + "type": "null" + } ] }, "timestamp": { "anyOf": [ - { "type": "string", "allOf": [{ "format": "date-time" }] }, - { "type": "null" } + { + "type": "string" + }, + { + "type": "null" + } ] } }, - "required": ["uuid", "event", "context", "properties", "distinct_id"], + "required": [ + "uuid", + "event", + "context", + "properties", + "distinct_id" + ], "additionalProperties": false } }, - "sent_at": { "type": "string", "allOf": [{ "format": "date-time" }] }, - "token": { "type": "string", "allOf": [{ "minLength": 1 }] } + "sent_at": { + "type": "string" + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "events", + "sent_at", + "token" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/i/v1/measurement/protected": { + "post": { + "tags": [ + "event_capture" + ], + "operationId": "event_capture.protected", + "parameters": [], + "security": [], + "responses": { + "202": { + "description": "ProtectedEvidenceAcceptedResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtectedEvidenceAcceptedResponse" + } + } + } + }, + "400": { + "description": "CaptureInvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureInvalidRequestError" + } + } + } + }, + "401": { + "description": "CaptureUnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureUnauthorizedError" + } + } + } + }, + "409": { + "description": "ProtectedEvidenceConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtectedEvidenceConflictError" + } + } + } + }, + "413": { + "description": "CapturePayloadTooLargeError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CapturePayloadTooLargeError" + } + } + } + }, + "500": { + "description": "CaptureInternalServerError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureInternalServerError" + } + } + } + }, + "503": { + "description": "CaptureDependencyUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureDependencyUnavailableError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "blobId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "ciphertext": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "consentRevision": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletionState": { + "type": "string", + "enum": [ + "active", + "deletion-requested", + "deleted" + ] + }, + "encryptionKeyVersion": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "installationId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "purpose": { + "type": "string", + "enum": [ + "advertising-identifier", + "diagnostic-authorization", + "email", + "install-referrer", + "link-capture", + "partner-context", + "phone", + "purchase-receipt", + "push-token" + ] + }, + "retentionClass": { + "type": "string", + "enum": [ + "ephemeral", + "installation", + "legal", + "transaction" + ] + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "blobId", + "ciphertext", + "consentRevision", + "deletionState", + "encryptionKeyVersion", + "installationId", + "purpose", + "retentionClass", + "token" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/i/v1/measurement/delete": { + "post": { + "tags": [ + "event_capture" + ], + "operationId": "event_capture.deleteMeasurementData", + "parameters": [], + "security": [], + "responses": { + "202": { + "description": "MeasurementDeletionAcceptedResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementDeletionAcceptedResponse" + } + } + } + }, + "400": { + "description": "CaptureInvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureInvalidRequestError" + } + } + } + }, + "401": { + "description": "CaptureUnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureUnauthorizedError" + } + } + } + }, + "429": { + "description": "CaptureRateLimitedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureRateLimitedError" + } + } + } + }, + "500": { + "description": "CaptureInternalServerError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureInternalServerError" + } + } + } + }, + "503": { + "description": "CaptureDependencyUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureDependencyUnavailableError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "installationId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "personId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + { + "type": "null" + } + ] + }, + "requestId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "requestedAt": { + "type": "string" + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } }, - "required": ["events", "sent_at", "token"], + "required": [ + "installationId", + "requestId", + "requestedAt", + "token" + ], "additionalProperties": false } } @@ -272,109 +763,692 @@ "required": true } } + }, + "/i/v1/measurement/config": { + "get": { + "tags": [ + "event_capture" + ], + "operationId": "event_capture.getMeasurementConfiguration", + "parameters": [ + { + "name": "x-publishable-key", + "in": "header", + "schema": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "SignedMeasurementConfigurationResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignedMeasurementConfigurationResponse" + } + } + } + }, + "401": { + "description": "CaptureUnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureUnauthorizedError" + } + } + } + }, + "500": { + "description": "CaptureInternalServerError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureInternalServerError" + } + } + } + }, + "503": { + "description": "CaptureDependencyUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptureDependencyUnavailableError" + } + } + } + } + } + } } }, "components": { "schemas": { "Union_": { "anyOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "type": "null" }, - { "type": "array", "items": { "$ref": "#/components/schemas/Union_" } }, - { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/Union_" } } + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Union_" + } + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Union_" + } + } ] }, "Union_1": { "anyOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "type": "null" }, - { "type": "array", "items": { "$ref": "#/components/schemas/Union_1" } }, - { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/Union_1" } } + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Union_1" + } + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Union_1" + } + } ] }, - "CaptureAcceptedResponse": { + "CaptureRejectedRecord": { "type": "object", - "properties": { "accepted": { "type": "integer" }, "rejected": { "type": "integer" } }, - "required": ["accepted", "rejected"], + "properties": { + "recordId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "malformed_envelope", + "unsupported_schema_version", + "payload_too_large", + "invalid_project_scope", + "duplicate", + "invalid_context", + "reserved_event", + "policy_rejected" + ] + } + }, + "required": [ + "recordId", + "reason" + ], "additionalProperties": false }, - "CaptureInvalidRequestError": { + "CaptureAcceptedResponse": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CaptureInvalidRequestError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["invalid_request"] } + "accepted": { + "type": "array", + "items": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "rejected": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaptureRejectedRecord" + } + } }, - "required": ["_tag", "error", "code"], + "required": [ + "accepted", + "rejected" + ], "additionalProperties": false }, - "effect_HttpApiSchemaError": { + "CaptureInvalidRequestError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["HttpApiSchemaError"] }, - "message": { "type": "string" } + "_tag": { + "type": "string", + "enum": [ + "CaptureInvalidRequestError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_request" + ] + } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "error", + "code" + ], "additionalProperties": false }, "CaptureUnauthorizedError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CaptureUnauthorizedError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["unauthorized"] } + "_tag": { + "type": "string", + "enum": [ + "CaptureUnauthorizedError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "unauthorized" + ] + } }, - "required": ["_tag", "error", "code"], + "required": [ + "_tag", + "error", + "code" + ], "additionalProperties": false }, "CapturePayloadTooLargeError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CapturePayloadTooLargeError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["payload_too_large"] } + "_tag": { + "type": "string", + "enum": [ + "CapturePayloadTooLargeError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "payload_too_large" + ] + } }, - "required": ["_tag", "error", "code"], + "required": [ + "_tag", + "error", + "code" + ], "additionalProperties": false }, "CaptureRateLimitedError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CaptureRateLimitedError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["rate_limited"] }, - "retry_after_ms": { "anyOf": [{ "type": "integer" }, { "type": "null" }] } + "_tag": { + "type": "string", + "enum": [ + "CaptureRateLimitedError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "rate_limited" + ] + }, + "retry_after_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } }, - "required": ["_tag", "error", "code"], + "required": [ + "_tag", + "error", + "code" + ], "additionalProperties": false }, "CaptureDependencyUnavailableError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CaptureDependencyUnavailableError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["dependency_unavailable"] } + "_tag": { + "type": "string", + "enum": [ + "CaptureDependencyUnavailableError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "dependency_unavailable" + ] + } }, - "required": ["_tag", "error", "code"], + "required": [ + "_tag", + "error", + "code" + ], "additionalProperties": false }, "CaptureInternalServerError": { "type": "object", "properties": { - "_tag": { "type": "string", "enum": ["CaptureInternalServerError"] }, - "error": { "type": "string", "allOf": [{ "minLength": 1 }] }, - "code": { "type": "string", "enum": ["internal_error"] } + "_tag": { + "type": "string", + "enum": [ + "CaptureInternalServerError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "internal_error" + ] + } + }, + "required": [ + "_tag", + "error", + "code" + ], + "additionalProperties": false + }, + "ProtectedEvidenceAcceptedResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean", + "enum": [ + true + ] + }, + "blobId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "accepted", + "blobId" + ], + "additionalProperties": false + }, + "ProtectedEvidenceConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ProtectedEvidenceConflictError" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "code": { + "type": "string", + "enum": [ + "protected_evidence_conflict" + ] + } + }, + "required": [ + "_tag", + "error", + "code" + ], + "additionalProperties": false + }, + "MeasurementDeletionAcceptedResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean", + "enum": [ + true + ] + }, + "deletedProtectedEvidence": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "requestId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "status": { + "type": "string", + "enum": [ + "completed" + ] + } + }, + "required": [ + "accepted", + "deletedProtectedEvidence", + "requestId", + "status" + ], + "additionalProperties": false + }, + "SignedMeasurementConfigurationResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "string" + }, + "keyId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "payload": { + "type": "object", + "properties": { + "collectors": { + "type": "object", + "properties": { + "appleAttributionEnabled": { + "type": "boolean" + }, + "linkAllowedDomains": { + "type": "array", + "items": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + } + }, + "required": [ + "appleAttributionEnabled", + "linkAllowedDomains" + ], + "additionalProperties": false + }, + "conversionRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coarseValue": { + "anyOf": [ + { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + { + "type": "null" + } + ] + }, + "eventName": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "fineValue": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + }, + { + "maximum": 63 + } + ] + }, + "lockWindow": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "minimumCount": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "window": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + }, + { + "maximum": 3 + } + ] + } + }, + "required": [ + "eventName", + "fineValue", + "minimumCount", + "window" + ], + "additionalProperties": false + } + }, + "schemaVersion": { + "type": "number", + "enum": [ + 1 + ] + }, + "storage": { + "type": "object", + "properties": { + "maxOutboxBytes": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "maxOutboxRecords": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "maxProtectedBytes": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "maxOutboxBytes", + "maxOutboxRecords", + "maxProtectedBytes" + ], + "additionalProperties": false + } + }, + "required": [ + "collectors", + "conversionRules", + "schemaVersion", + "storage" + ], + "additionalProperties": false + }, + "projectId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "signature": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } }, - "required": ["_tag", "error", "code"], + "required": [ + "expiresAt", + "keyId", + "payload", + "projectId", + "signature", + "version" + ], "additionalProperties": false } }, "securitySchemes": {} }, "security": [], - "tags": [{ "name": "event_capture" }] + "tags": [ + { + "name": "event_capture" + } + ] } diff --git a/packages/generated-clients/openapi/links.json b/packages/generated-clients/openapi/links.json new file mode 100644 index 000000000..c77cdf002 --- /dev/null +++ b/packages/generated-clients/openapi/links.json @@ -0,0 +1,1043 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": { + "/l/v1/links": { + "post": { + "tags": [ + "links" + ], + "operationId": "links.createLink", + "parameters": [], + "security": [], + "responses": { + "201": { + "description": "CreateLinkResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLinkResponse" + } + } + } + }, + "400": { + "description": "LinkInvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkInvalidRequestError" + } + } + } + }, + "401": { + "description": "LinkUnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkUnauthorizedError" + } + } + } + }, + "429": { + "description": "LinkRateLimitedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRateLimitedError" + } + } + } + }, + "503": { + "description": "LinkServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkServiceUnavailableError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "brandedDomain": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "campaign": { + "anyOf": [ + { + "type": "object", + "properties": { + "ad": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "adSet": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "campaign": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "channel": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "mediaSource": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "customParameters": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + { + "type": "null" + } + ] + }, + "destination": { + "type": "object", + "properties": { + "androidStoreUrl": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "appleAppId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "baseDeepLink": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "deepLinkValue": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + "iosStoreUrl": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "subvalues": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + { + "type": "null" + } + ] + }, + "webFallbackUrl": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "deepLinkValue" + ], + "additionalProperties": false + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "idempotencyKey": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "referrerCustomerId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "referrerImageUrl": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "referrerName": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "referrerUid": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "templateId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + "required": [ + "destination", + "token" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/l/v1/deferred/resolve": { + "post": { + "tags": [ + "links" + ], + "operationId": "links.resolveDeferredLink", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "campaign": { + "anyOf": [ + { + "type": "object", + "properties": { + "ad": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "adSet": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "campaign": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "channel": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "mediaSource": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "clickId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "clickedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "deferred": { + "anyOf": [ + { + "type": "boolean", + "enum": [ + true + ] + }, + { + "type": "null" + } + ] + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "linkId": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "reason": { + "anyOf": [ + { + "type": "string", + "enum": [ + "expired", + "not-found", + "replayed", + "invalid" + ] + }, + { + "type": "null" + } + ] + }, + "route": { + "anyOf": [ + { + "type": "object", + "properties": { + "subvalues": { + "type": "object", + "additionalProperties": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + "value": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + "required": [ + "subvalues", + "value" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "signature": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "found", + "notFound" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "LinkInvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkInvalidRequestError" + } + } + } + }, + "401": { + "description": "LinkUnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkUnauthorizedError" + } + } + } + }, + "429": { + "description": "LinkRateLimitedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRateLimitedError" + } + } + } + }, + "503": { + "description": "LinkServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkServiceUnavailableError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deferredToken": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + "installationId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + "platform": { + "type": "string", + "enum": [ + "ios", + "android" + ] + }, + "token": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + "required": [ + "deferredToken", + "installationId", + "platform", + "token" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + } + }, + "components": { + "schemas": { + "CreateLinkResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "string" + }, + "linkId": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + }, + "url": { + "type": "string", + "allOf": [ + { + "minLength": 1 + }, + { + "maxLength": 1024 + } + ] + } + }, + "required": [ + "expiresAt", + "linkId", + "url" + ], + "additionalProperties": false + }, + "LinkInvalidRequestError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "LinkInvalidRequestError" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_link_request" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "code", + "error" + ], + "additionalProperties": false + }, + "LinkUnauthorizedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "LinkUnauthorizedError" + ] + }, + "code": { + "type": "string", + "enum": [ + "unauthorized" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "code", + "error" + ], + "additionalProperties": false + }, + "LinkRateLimitedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "LinkRateLimitedError" + ] + }, + "code": { + "type": "string", + "enum": [ + "rate_limited" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "code", + "error" + ], + "additionalProperties": false + }, + "LinkServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "LinkServiceUnavailableError" + ] + }, + "code": { + "type": "string", + "enum": [ + "service_unavailable" + ] + }, + "error": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + } + }, + "required": [ + "_tag", + "code", + "error" + ], + "additionalProperties": false + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "links" + } + ] +} diff --git a/packages/generated-clients/package.json b/packages/generated-clients/package.json index c138a5723..c25abcd7e 100644 --- a/packages/generated-clients/package.json +++ b/packages/generated-clients/package.json @@ -7,7 +7,8 @@ "exports": { ".": "./src/index.ts", "./core": "./src/core/index.ts", - "./event-capture": "./src/event-capture/index.ts" + "./event-capture": "./src/event-capture/index.ts", + "./links": "./src/links/index.ts" }, "scripts": { "openapi:generate": "node ../../scripts/generate-openapi-clients.mjs", diff --git a/packages/generated-clients/src/core/generated.ts b/packages/generated-clients/src/core/generated.ts index 12a42e75b..bb2976810 100644 --- a/packages/generated-clients/src/core/generated.ts +++ b/packages/generated-clients/src/core/generated.ts @@ -1,400 +1,498 @@ -import type * as HttpClient from "effect/unstable/http/HttpClient"; -import * as HttpClientError from "effect/unstable/http/HttpClientError"; -import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; -import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; -import * as Data from "effect/Data"; -import * as Effect from "effect/Effect"; +import type * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" -export type AuthSession200MethodEnum = "secret-key"; +export type AuthSession200Method = "api-key" | "publishable-key" | "secret-key" export interface AuthSession200 { - readonly method: AuthSession200MethodEnum | AuthSession200MethodEnum | AuthSession200MethodEnum; - readonly name: string; - readonly organizations: ReadonlyArray<{ - readonly id: string; - readonly name: string; - readonly slug: string; - }>; - readonly projects: ReadonlyArray<{ - readonly id: string; - readonly name: string; - readonly organizationId: string; - readonly slug: string; - }>; -} - -export type EffectHttpApiSchemaErrorTag = "HttpApiSchemaError"; - -export interface EffectHttpApiSchemaError { - readonly _tag: EffectHttpApiSchemaErrorTag; - readonly message: string; -} - -export type ApiActionForbiddenErrorTag = "Api/ActionForbiddenError"; + readonly "method": AuthSession200Method; + readonly "name": string; + readonly "organizations": ReadonlyArray<{ + readonly "id": string; + readonly "name": string; + readonly "slug": string +}>; + readonly "projects": ReadonlyArray<{ + readonly "id": string; + readonly "name": string; + readonly "organizationId": string; + readonly "slug": string +}> +} + +export type ApiActionForbiddenErrorTag = "Api/ActionForbiddenError" export interface ApiActionForbiddenError { - readonly _tag: ApiActionForbiddenErrorTag; - readonly message: string; + readonly "_tag": ApiActionForbiddenErrorTag; + readonly "message": string } -export type ApiAuthenticationErrorTag = "Api/AuthenticationError"; +export type ApiAuthenticationErrorTag = "Api/AuthenticationError" export interface ApiAuthenticationError { - readonly _tag: ApiAuthenticationErrorTag; - readonly cause: string; - readonly message: string; + readonly "_tag": ApiAuthenticationErrorTag; + readonly "cause": string; + readonly "message": string } -export type ApiNotAuthenticatedErrorTag = "Api/NotAuthenticatedError"; +export type ApiNotAuthenticatedErrorTag = "Api/NotAuthenticatedError" export interface ApiNotAuthenticatedError { - readonly _tag: ApiNotAuthenticatedErrorTag; - readonly message: string; + readonly "_tag": ApiNotAuthenticatedErrorTag; + readonly "message": string } -export type AuthSession500 = ApiAuthenticationError | ApiNotAuthenticatedError; +export type AuthSession500 = ApiAuthenticationError | ApiNotAuthenticatedError export interface ApiKey { - readonly end: string; - readonly id: string; - readonly isPublic: boolean; - readonly name: string; - readonly prefix: string; - readonly projectId: string; - readonly rawKey?: string | null | undefined; + readonly "end": string; + readonly "id": string; + readonly "isPublic": boolean; + readonly "name": string; + readonly "prefix": string; + readonly "projectId": string; + readonly "rawKey"?: string | null | undefined } -export type ApiKeysListApiKeys200 = ReadonlyArray; +export type ApiKeysListApiKeys200 = ReadonlyArray -export type ApiApiKeyServiceErrorTag = "Api/ApiKeyServiceError"; +export type ApiApiKeyServiceErrorTag = "Api/ApiKeyServiceError" export interface ApiApiKeyServiceError { - readonly _tag: ApiApiKeyServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiApiKeyServiceErrorTag; + readonly "cause": string } -export type ApiKeysListApiKeys500 = - | ApiApiKeyServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiKeysListApiKeys500 = ApiApiKeyServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface CreateSecretKeyBody { - readonly name: string; - readonly projectId: string; + readonly "name": string; + readonly "projectId": string } export interface ApiKeyWithRawKey { - readonly end: string; - readonly id: string; - readonly isPublic: boolean; - readonly name: string; - readonly prefix: string; - readonly projectId: string; - readonly rawKey: string; + readonly "end": string; + readonly "id": string; + readonly "isPublic": boolean; + readonly "name": string; + readonly "prefix": string; + readonly "projectId": string; + readonly "rawKey": string } -export type ApiKeysCreateSecretKey500 = - | ApiApiKeyServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiKeysCreateSecretKey500 = ApiApiKeyServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ApiApiKeyNotFoundErrorTag = "Api/ApiKeyNotFoundError"; +export type ApiApiKeyNotFoundErrorTag = "Api/ApiKeyNotFoundError" export interface ApiApiKeyNotFoundError { - readonly _tag: ApiApiKeyNotFoundErrorTag; - readonly message: string; + readonly "_tag": ApiApiKeyNotFoundErrorTag; + readonly "message": string } -export type ApiKeysGetApiKeyById500 = - | ApiApiKeyServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiKeysGetApiKeyById500 = ApiApiKeyServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ApiKeysDeleteApiKey500 = - | ApiApiKeyServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiKeysDeleteApiKey500 = ApiApiKeyServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ApiKeysRotateSecretKey500 = - | ApiApiKeyServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiKeysRotateSecretKey500 = ApiApiKeyServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface Person { - readonly personId: string; - readonly distinctId: string; - readonly email: string | null; - readonly name: string | null; + readonly "personId": string; + readonly "distinctId": string; + readonly "email": string | null; + readonly "name": string | null } -export type PersonsListPersons200 = ReadonlyArray; +export type PersonsListPersons200 = ReadonlyArray -export type ApiPersonServiceErrorTag = "Api/PersonServiceError"; +export type ApiPersonServiceErrorTag = "Api/PersonServiceError" export interface ApiPersonServiceError { - readonly _tag: ApiPersonServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiPersonServiceErrorTag; + readonly "cause": string } -export type PersonsListPersons500 = - | ApiPersonServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type PersonsListPersons500 = ApiPersonServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface CreatePersonBody { - readonly distinctId: string; - readonly email?: string | null | undefined; - readonly name?: string | null | undefined; + readonly "distinctId": string; + readonly "email"?: string | null | undefined; + readonly "name"?: string | null | undefined } -export type ApiPersonInvalidAnonymousIdErrorTag = "Api/PersonInvalidAnonymousIdError"; +export type ApiPersonInvalidAnonymousIdErrorTag = "Api/PersonInvalidAnonymousIdError" export interface ApiPersonInvalidAnonymousIdError { - readonly _tag: ApiPersonInvalidAnonymousIdErrorTag; - readonly id: string; + readonly "_tag": ApiPersonInvalidAnonymousIdErrorTag; + readonly "id": string } -export type PersonsCreatePerson400 = ApiPersonInvalidAnonymousIdError | EffectHttpApiSchemaError; - -export type PersonsCreatePerson500 = - | ApiPersonServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type PersonsCreatePerson500 = ApiPersonServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ApiPersonNotFoundErrorTag = "Api/PersonNotFoundError"; +export type ApiPersonNotFoundErrorTag = "Api/PersonNotFoundError" export interface ApiPersonNotFoundError { - readonly _tag: ApiPersonNotFoundErrorTag; - readonly id: string; + readonly "_tag": ApiPersonNotFoundErrorTag; + readonly "id": string +} + +export type PersonsGetPersonById500 = ApiPersonServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type PersonsGetPersonByDistinctId500 = ApiPersonServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type SendNotificationBodyBadgeEnum = "Infinity" | "-Infinity" | "NaN" + +export type SendNotificationBodyPriorityEnum = "default" | "high" + +export type SendNotificationBodyTtlEnum = "Infinity" | "-Infinity" | "NaN" + +export interface SendNotificationBody { + readonly "personIds"?: ReadonlyArray | null | undefined; + readonly "distinctIds"?: ReadonlyArray | null | undefined; + readonly "title": string; + readonly "body": string; + readonly "data"?: Record | null | undefined; + readonly "sound"?: string | null | undefined; + readonly "badge"?: number | "NaN" | "Infinity" | "-Infinity" | SendNotificationBodyBadgeEnum | null | undefined; + readonly "priority"?: SendNotificationBodyPriorityEnum | null | undefined; + readonly "ttl"?: number | "NaN" | "Infinity" | "-Infinity" | SendNotificationBodyTtlEnum | null | undefined; + readonly "channelId"?: string | null | undefined; + readonly "collapseId"?: string | null | undefined; + readonly "idempotencyKey"?: string | null | undefined +} + +export type SendNotificationResponseDeviceCountEnum = "Infinity" | "-Infinity" | "NaN" + +export type SendNotificationResponseStatus = "pending" | "in_progress" | "succeeded" | "partial_failed" | "failed" | "no_recipients" + +export interface SendNotificationResponse { + readonly "pushNotificationSendId": string; + readonly "deviceCount": number | "NaN" | "Infinity" | "-Infinity" | SendNotificationResponseDeviceCountEnum; + readonly "status": SendNotificationResponseStatus; + readonly "unresolvedDistinctIds": ReadonlyArray +} + +export type ApiPushDeviceValidationErrorTag = "Api/PushDeviceValidationError" + +export interface ApiPushDeviceValidationError { + readonly "_tag": ApiPushDeviceValidationErrorTag; + readonly "message": string +} + +export type ApiPushSendNotEnabledErrorTag = "Api/PushSendNotEnabledError" + +export interface ApiPushSendNotEnabledError { + readonly "_tag": ApiPushSendNotEnabledErrorTag; + readonly "message": string } -export type PersonsGetPersonById500 = - | ApiPersonServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiPushSendServiceErrorTag = "Api/PushSendServiceError" -export type PersonsGetPersonByDistinctId500 = - | ApiPersonServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export interface ApiPushSendServiceError { + readonly "_tag": ApiPushSendServiceErrorTag; + readonly "cause": string +} + +export type NotificationsSendNotification500 = ApiAuthenticationError | ApiPushSendServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface CreateOrganizationBody { - readonly name: string; + readonly "name": string } export interface Organization { - readonly id: string; - readonly name: string; - readonly slug: string; + readonly "id": string; + readonly "name": string; + readonly "slug": string } -export type ApiOrganizationServiceErrorTag = "Api/OrganizationServiceError"; +export type ApiOrganizationServiceErrorTag = "Api/OrganizationServiceError" export interface ApiOrganizationServiceError { - readonly _tag: ApiOrganizationServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiOrganizationServiceErrorTag; + readonly "cause": string } -export type OrganizationsCreateOrganization500 = - | ApiOrganizationServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type OrganizationsCreateOrganization500 = ApiOrganizationServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface Perk { - readonly id: string; - readonly name: string; - readonly projectId: string; - readonly slug: string; + readonly "id": string; + readonly "name": string; + readonly "projectId": string; + readonly "slug": string } -export type PerksListPerks200 = ReadonlyArray; +export type PerksListPerks200 = ReadonlyArray -export type ApiPerkServiceErrorTag = "Api/PerkServiceError"; +export type ApiPerkServiceErrorTag = "Api/PerkServiceError" export interface ApiPerkServiceError { - readonly _tag: ApiPerkServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiPerkServiceErrorTag; + readonly "cause": string +} + +export type PerksListPerks500 = ApiPerkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export interface PaywallDeploysCreateDeployRequest { + +} + +export interface CreatePaywallDeployResponse { + readonly "deployId": string; + readonly "missing": ReadonlyArray +} + +export type ApiPaywallDeployUpgradeRequiredErrorTag = "Api/PaywallDeployUpgradeRequiredError" + +export type ApiPaywallDeployUpgradeRequiredErrorSchemaVersionEnum = "Infinity" | "-Infinity" | "NaN" + +export interface ApiPaywallDeployUpgradeRequiredError { + readonly "_tag": ApiPaywallDeployUpgradeRequiredErrorTag; + readonly "message": string; + readonly "schemaVersion": number | "NaN" | "Infinity" | "-Infinity" | ApiPaywallDeployUpgradeRequiredErrorSchemaVersionEnum | null +} + +export type ApiPaywallDeployValidationErrorTag = "Api/PaywallDeployValidationError" + +export interface ApiPaywallDeployValidationError { + readonly "_tag": ApiPaywallDeployValidationErrorTag; + readonly "message": string; + readonly "violations": ReadonlyArray +} + +export type ApiPaywallDeployServiceErrorTag = "Api/PaywallDeployServiceError" + +export interface ApiPaywallDeployServiceError { + readonly "_tag": ApiPaywallDeployServiceErrorTag; + readonly "cause": string +} + +export type PaywallDeploysCreateDeploy500 = ApiPaywallDeployServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type UploadPaywallDeployBlobResponse = Record + +export type ApiDeployBlobNotDeclaredErrorTag = "Api/DeployBlobNotDeclaredError" + +export interface ApiDeployBlobNotDeclaredError { + readonly "_tag": ApiDeployBlobNotDeclaredErrorTag; + readonly "sha256": string } -export type PerksListPerks500 = - | ApiPerkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiPaywallDeployNotFoundErrorTag = "Api/PaywallDeployNotFoundError" + +export interface ApiPaywallDeployNotFoundError { + readonly "_tag": ApiPaywallDeployNotFoundErrorTag; + readonly "message": string +} + +export type PaywallDeploysUploadBlob404 = ApiDeployBlobNotDeclaredError | ApiPaywallDeployNotFoundError + +export type ApiPaywallDeployNotPendingErrorTag = "Api/PaywallDeployNotPendingError" + +export interface ApiPaywallDeployNotPendingError { + readonly "_tag": ApiPaywallDeployNotPendingErrorTag; + readonly "message": string +} + +export type ApiDeployBlobHashMismatchErrorTag = "Api/DeployBlobHashMismatchError" + +export interface ApiDeployBlobHashMismatchError { + readonly "_tag": ApiDeployBlobHashMismatchErrorTag; + readonly "actualSha256": string; + readonly "expectedSha256": string +} + +export type PaywallDeploysUploadBlob422 = ApiDeployBlobHashMismatchError | ApiPaywallDeployValidationError + +export type PaywallDeploysUploadBlob500 = ApiPaywallDeployServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type FinalizedPaywallDeployComponentVersionEnum = "Infinity" | "-Infinity" | "NaN" + +export interface FinalizedPaywallDeployComponent { + readonly "componentId": string; + readonly "contentHash": string; + readonly "id": string; + readonly "version": number | "NaN" | "Infinity" | "-Infinity" | FinalizedPaywallDeployComponentVersionEnum +} + +export type FinalizedPaywallDeployPaywallVersionEnum = "Infinity" | "-Infinity" | "NaN" + +export interface FinalizedPaywallDeployPaywall { + readonly "contentHash": string; + readonly "id": string; + readonly "paywallId": string; + readonly "releaseId": string; + readonly "url": string; + readonly "version": number | "NaN" | "Infinity" | "-Infinity" | FinalizedPaywallDeployPaywallVersionEnum +} + +export type FinalizePaywallDeployResponseStatus = "ready" + +export interface FinalizePaywallDeployResponse { + readonly "components": ReadonlyArray; + readonly "deployId": string; + readonly "paywalls": ReadonlyArray; + readonly "status": FinalizePaywallDeployResponseStatus +} + +export type ApiIncompleteDeployErrorTag = "Api/IncompleteDeployError" + +export interface ApiIncompleteDeployError { + readonly "_tag": ApiIncompleteDeployErrorTag; + readonly "missing": ReadonlyArray +} + +export type PaywallDeploysFinalizeDeploy500 = ApiPaywallDeployServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface PaywallLocation { - readonly description: string | null; - readonly id: string; - readonly name: string; - readonly projectId: string; - readonly slug: string; + readonly "description": string | null; + readonly "id": string; + readonly "name": string; + readonly "projectId": string; + readonly "slug": string } -export type PaywallLocationsListPaywallLocations200 = ReadonlyArray; +export type PaywallLocationsListPaywallLocations200 = ReadonlyArray -export type ApiPaywallLocationServiceErrorTag = "Api/PaywallLocationServiceError"; +export type ApiPaywallLocationServiceErrorTag = "Api/PaywallLocationServiceError" export interface ApiPaywallLocationServiceError { - readonly _tag: ApiPaywallLocationServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiPaywallLocationServiceErrorTag; + readonly "cause": string } -export type PaywallLocationsListPaywallLocations500 = - | ApiPaywallLocationServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type PaywallLocationsListPaywallLocations500 = ApiPaywallLocationServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface SchemaLocation { - readonly description: string | null; - readonly name: string; - readonly slug: string; + readonly "description": string | null; + readonly "name": string; + readonly "slug": string } export interface SchemaPerk { - readonly name: string; - readonly slug: string; + readonly "name": string; + readonly "slug": string } -export type SchemaProductProviderProviderIdEnum = "appleAppStore" | "googlePlay"; +export type SchemaProductProviderProviderId = "appleAppStore" | "googlePlay" export interface SchemaProductProvider { - readonly configuration: Record; - readonly providerId: SchemaProductProviderProviderIdEnum | SchemaProductProviderProviderIdEnum; + readonly "configuration": Record; + readonly "providerId": SchemaProductProviderProviderId } -export type SchemaProductType = "one-time" | "one-time-consumable" | "subscription"; +export type SchemaProductType = "subscription" | "one-time" | "one-time-consumable" export interface SchemaProduct { - readonly name: string; - readonly perks: ReadonlyArray; - readonly providers: ReadonlyArray; - readonly slug: string; - readonly type: SchemaProductType; + readonly "name": string; + readonly "perks": ReadonlyArray; + readonly "providers": ReadonlyArray; + readonly "slug": string; + readonly "type": SchemaProductType } export interface ProjectSchemaResponse { - readonly enabledProviders: ReadonlyArray<"appleAppStore" | "googlePlay">; - readonly locations: ReadonlyArray; - readonly perks: ReadonlyArray; - readonly products: ReadonlyArray; - readonly version: string; + readonly "enabledProviders": ReadonlyArray<"appleAppStore" | "googlePlay">; + readonly "locations": ReadonlyArray; + readonly "perks": ReadonlyArray; + readonly "products": ReadonlyArray; + readonly "version": string } -export type ApiSchemaServiceErrorTag = "Api/SchemaServiceError"; +export type ApiSchemaServiceErrorTag = "Api/SchemaServiceError" export interface ApiSchemaServiceError { - readonly _tag: ApiSchemaServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiSchemaServiceErrorTag; + readonly "cause": string } -export type SchemaGetSchema500 = - | ApiSchemaServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SchemaGetSchema500 = ApiSchemaServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface SchemaVersion { - readonly version: string; + readonly "version": string } -export type SchemaGetSchemaVersion500 = - | ApiSchemaServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SchemaGetSchemaVersion500 = ApiSchemaServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface CreateProjectBody { - readonly name: string; - readonly organizationId: string; + readonly "name": string; + readonly "organizationId": string } export interface Project { - readonly id: string; - readonly name: string; - readonly slug: string; + readonly "id": string; + readonly "name": string; + readonly "slug": string } -export type ApiProjectServiceErrorTag = "Api/ProjectServiceError"; +export type ApiProjectServiceErrorTag = "Api/ProjectServiceError" export interface ApiProjectServiceError { - readonly _tag: ApiProjectServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiProjectServiceErrorTag; + readonly "cause": string } -export type ProjectsCreateProject500 = - | ApiAuthenticationError - | ApiProjectServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ProjectsCreateProject500 = ApiAuthenticationError | ApiProjectServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ProjectsListProjects200 = ReadonlyArray; +export type ProjectsListProjects200 = ReadonlyArray -export type ProjectsListProjects500 = - | ApiProjectServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ProjectsListProjects500 = ApiProjectServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type ProductTypeEnum = "one-time-consumable"; +export type ProductType = "subscription" | "one-time" | "one-time-consumable" export interface Product { - readonly id: string; - readonly name: string; - readonly projectId: string; - readonly slug: string; - readonly type: ProductTypeEnum | ProductTypeEnum | ProductTypeEnum; + readonly "id": string; + readonly "name": string; + readonly "projectId": string; + readonly "slug": string; + readonly "type": ProductType } -export type ProductsListProducts200 = ReadonlyArray; +export type ProductsListProducts200 = ReadonlyArray -export type ApiProductServiceErrorTag = "Api/ProductServiceError"; +export type ApiProductServiceErrorTag = "Api/ProductServiceError" export interface ApiProductServiceError { - readonly _tag: ApiProductServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiProductServiceErrorTag; + readonly "cause": string } -export type ProductsListProducts500 = - | ApiProductServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ProductsListProducts500 = ApiProductServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface ProductPerk { - readonly id: string; - readonly perkId: string; - readonly productId: string; + readonly "id": string; + readonly "perkId": string; + readonly "productId": string } -export type ProductPerksListProductPerksByProductId200 = ReadonlyArray; +export type ProductPerksListProductPerksByProductId200 = ReadonlyArray -export type ApiProductPerkValidationErrorTag = "Api/ProductPerkValidationError"; +export type ApiProductPerkValidationErrorTag = "Api/ProductPerkValidationError" export interface ApiProductPerkValidationError { - readonly _tag: ApiProductPerkValidationErrorTag; - readonly message: string; + readonly "_tag": ApiProductPerkValidationErrorTag; + readonly "message": string } -export type ProductPerksListProductPerksByProductId400 = - | ApiProductPerkValidationError - | EffectHttpApiSchemaError; - -export type ApiProductPerkServiceErrorTag = "Api/ProductPerkServiceError"; +export type ApiProductPerkServiceErrorTag = "Api/ProductPerkServiceError" export interface ApiProductPerkServiceError { - readonly _tag: ApiProductPerkServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiProductPerkServiceErrorTag; + readonly "cause": string } -export type ProductPerksListProductPerksByProductId500 = - | ApiProductPerkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ProductPerksListProductPerksByProductId500 = ApiProductPerkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkGetPersonParamsXIsBackgrounded = "false"; +export type SdkGetPersonParamsXIsBackgrounded = "false" -export type SdkGetPersonParamsXIsDebugBuildEnum = "false"; +export type SdkGetPersonParamsXIsDebugBuild = "true" | "false" -export type SdkGetPersonParamsXObserverModeEnum = "false"; +export type SdkGetPersonParamsXObserverMode = "true" | "false" -export type SdkGetPersonParamsXPlatformFlavorEnum = "browser"; +export type SdkGetPersonParamsXPlatformFlavor = "native" | "browser" -export type SdkGetPersonParamsXSdkEnum = "web"; +export type SdkGetPersonParamsXSdk = "react-native" | "web" export interface SdkGetPersonParams { readonly "x-distinct-id": string; @@ -403,148 +501,123 @@ export interface SdkGetPersonParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkGetPersonParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkGetPersonParamsXIsDebugBuildEnum - | SdkGetPersonParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkGetPersonParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkGetPersonParamsXObserverModeEnum - | SdkGetPersonParamsXObserverModeEnum; + readonly "x-observer-mode": SdkGetPersonParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkGetPersonParamsXPlatformFlavorEnum - | SdkGetPersonParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkGetPersonParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkGetPersonParamsXSdkEnum | SdkGetPersonParamsXSdkEnum; + readonly "x-sdk": SdkGetPersonParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } -export type SdkEntitlementGrantSourceEnum = "manual"; +export type SdkEntitlementGrantSource = "subscription" | "purchase" | "manual" -export type SdkEntitlementGrantStatusEnum = "expired"; +export type SdkEntitlementGrantStatus = "active" | "expired" export interface SdkEntitlementGrant { - readonly expiresAt: string | null; - readonly perkId: string; - readonly source: - | SdkEntitlementGrantSourceEnum - | SdkEntitlementGrantSourceEnum - | SdkEntitlementGrantSourceEnum; - readonly sourceId: string | null; - readonly sourcePersonId: string; - readonly status: SdkEntitlementGrantStatusEnum | SdkEntitlementGrantStatusEnum; + readonly "expiresAt": string | null; + readonly "perkId": string; + readonly "source": SdkEntitlementGrantSource; + readonly "sourceId": string | null; + readonly "sourcePersonId": string; + readonly "status": SdkEntitlementGrantStatus } -export type SdkPurchaseHistoryEntryTypeEnum = "subscription"; +export type SdkPurchaseHistoryEntryType = "one_time" | "subscription" export interface SdkPurchaseHistoryEntry { - readonly createdAt: string; - readonly productId: string | null; - readonly providerKey: string; - readonly purchaseId: string; - readonly sourcePersonId: string; - readonly type: SdkPurchaseHistoryEntryTypeEnum | SdkPurchaseHistoryEntryTypeEnum; + readonly "createdAt": string; + readonly "productId": string | null; + readonly "providerKey": string; + readonly "purchaseId": string; + readonly "sourcePersonId": string; + readonly "type": SdkPurchaseHistoryEntryType } -export type SdkPersonSnapshotContextModeEnum = "temporary_pending_transfer"; +export type SdkPersonSnapshotContextMode = "persisted" | "temporary_pending_transfer" -export type SdkCurrentSubscriptionStatusEnum = "trialing"; +export type SdkCurrentSubscriptionStatus = "none" | "active" | "canceled" | "past_due" | "trialing" export interface SdkCurrentSubscription { - readonly expiresAt: string | null; - readonly productId: string | null; - readonly status: - | SdkCurrentSubscriptionStatusEnum - | SdkCurrentSubscriptionStatusEnum - | SdkCurrentSubscriptionStatusEnum - | SdkCurrentSubscriptionStatusEnum - | SdkCurrentSubscriptionStatusEnum; - readonly subscriptionId: string | null; + readonly "expiresAt": string | null; + readonly "productId": string | null; + readonly "status": SdkCurrentSubscriptionStatus; + readonly "subscriptionId": string | null } -export type SdkSubscriptionHistoryEntryStatusEnum = "past_due"; +export type SdkSubscriptionHistoryEntryStatus = "active" | "canceled" | "expired" | "trialing" | "past_due" export interface SdkSubscriptionHistoryEntry { - readonly canceledAt: string | null; - readonly expiresAt: string | null; - readonly isTrial: boolean; - readonly productId: string | null; - readonly sourcePersonId: string; - readonly startsAt: string; - readonly status: - | SdkSubscriptionHistoryEntryStatusEnum - | SdkSubscriptionHistoryEntryStatusEnum - | SdkSubscriptionHistoryEntryStatusEnum - | SdkSubscriptionHistoryEntryStatusEnum - | SdkSubscriptionHistoryEntryStatusEnum; - readonly subscriptionId: string; + readonly "canceledAt": string | null; + readonly "expiresAt": string | null; + readonly "isTrial": boolean; + readonly "productId": string | null; + readonly "sourcePersonId": string; + readonly "startsAt": string; + readonly "status": SdkSubscriptionHistoryEntryStatus; + readonly "subscriptionId": string } export interface SdkPerson { - readonly distinctId: string; - readonly email: string | null; - readonly entitlements: { - readonly grants: ReadonlyArray; - }; - readonly name: string | null; - readonly personId: string; - readonly purchases: { - readonly history: ReadonlyArray; - }; - readonly snapshotContext: { - readonly includedPersonIds: ReadonlyArray; - readonly migrationJobId: string | null; - readonly mode: SdkPersonSnapshotContextModeEnum | SdkPersonSnapshotContextModeEnum; - }; - readonly subscriptions: { - readonly current: SdkCurrentSubscription | null; - readonly history: ReadonlyArray; - }; -} - -export type ApiSdkValidationErrorTag = "Api/SdkValidationError"; + readonly "distinctId": string; + readonly "email": string | null; + readonly "entitlements": { + readonly "grants": ReadonlyArray +}; + readonly "name": string | null; + readonly "personId": string; + readonly "purchases": { + readonly "history": ReadonlyArray +}; + readonly "snapshotContext": { + readonly "includedPersonIds": ReadonlyArray; + readonly "migrationJobId": string | null; + readonly "mode": SdkPersonSnapshotContextMode +}; + readonly "subscriptions": { + readonly "current": SdkCurrentSubscription | null; + readonly "history": ReadonlyArray +} +} + +export type ApiSdkValidationErrorTag = "Api/SdkValidationError" export interface ApiSdkValidationError { - readonly _tag: ApiSdkValidationErrorTag; - readonly message: string; + readonly "_tag": ApiSdkValidationErrorTag; + readonly "message": string } -export type SdkGetPerson400 = ApiSdkValidationError | EffectHttpApiSchemaError; - -export type ApiSdkPersonNotFoundErrorTag = "Api/SdkPersonNotFoundError"; +export type ApiSdkPersonNotFoundErrorTag = "Api/SdkPersonNotFoundError" export interface ApiSdkPersonNotFoundError { - readonly _tag: ApiSdkPersonNotFoundErrorTag; - readonly message: string; + readonly "_tag": ApiSdkPersonNotFoundErrorTag; + readonly "message": string } -export type ApiSdkServiceErrorTag = "Api/SdkServiceError"; +export type ApiSdkServiceErrorTag = "Api/SdkServiceError" export interface ApiSdkServiceError { - readonly _tag: ApiSdkServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiSdkServiceErrorTag; + readonly "cause": string } -export type SdkGetPerson500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkGetPerson500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkIdentifyPersonParamsXIsBackgrounded = "false"; +export type SdkIdentifyPersonParamsXIsBackgrounded = "false" -export type SdkIdentifyPersonParamsXIsDebugBuildEnum = "false"; +export type SdkIdentifyPersonParamsXIsDebugBuild = "true" | "false" -export type SdkIdentifyPersonParamsXObserverModeEnum = "false"; +export type SdkIdentifyPersonParamsXObserverMode = "true" | "false" -export type SdkIdentifyPersonParamsXPlatformFlavorEnum = "browser"; +export type SdkIdentifyPersonParamsXPlatformFlavor = "native" | "browser" -export type SdkIdentifyPersonParamsXSdkEnum = "web"; +export type SdkIdentifyPersonParamsXSdk = "react-native" | "web" export interface SdkIdentifyPersonParams { readonly "x-distinct-id": string; @@ -553,58 +626,46 @@ export interface SdkIdentifyPersonParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkIdentifyPersonParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkIdentifyPersonParamsXIsDebugBuildEnum - | SdkIdentifyPersonParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkIdentifyPersonParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkIdentifyPersonParamsXObserverModeEnum - | SdkIdentifyPersonParamsXObserverModeEnum; + readonly "x-observer-mode": SdkIdentifyPersonParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkIdentifyPersonParamsXPlatformFlavorEnum - | SdkIdentifyPersonParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkIdentifyPersonParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkIdentifyPersonParamsXSdkEnum | SdkIdentifyPersonParamsXSdkEnum; + readonly "x-sdk": SdkIdentifyPersonParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } export interface SdkIdentifyBody { - readonly distinctId: string; - readonly email?: string | null | undefined; - readonly name?: string | null | undefined; - readonly traits?: Record | null | undefined; + readonly "distinctId": string; + readonly "email"?: string | null | undefined; + readonly "name"?: string | null | undefined; + readonly "traits"?: Record | null | undefined } -export type SdkIdentifyPerson400 = ApiSdkValidationError | EffectHttpApiSchemaError; - -export type ApiSdkPersonAlreadyIdentifiedErrorTag = "Api/SdkPersonAlreadyIdentifiedError"; +export type ApiSdkPersonAlreadyIdentifiedErrorTag = "Api/SdkPersonAlreadyIdentifiedError" export interface ApiSdkPersonAlreadyIdentifiedError { - readonly _tag: ApiSdkPersonAlreadyIdentifiedErrorTag; - readonly distinctId: string; + readonly "_tag": ApiSdkPersonAlreadyIdentifiedErrorTag; + readonly "distinctId": string } -export type SdkIdentifyPerson500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkIdentifyPerson500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkSyncPersonAttributesParamsXIsBackgrounded = "false"; +export type SdkSyncPersonAttributesParamsXIsBackgrounded = "false" -export type SdkSyncPersonAttributesParamsXIsDebugBuildEnum = "false"; +export type SdkSyncPersonAttributesParamsXIsDebugBuild = "true" | "false" -export type SdkSyncPersonAttributesParamsXObserverModeEnum = "false"; +export type SdkSyncPersonAttributesParamsXObserverMode = "true" | "false" -export type SdkSyncPersonAttributesParamsXPlatformFlavorEnum = "browser"; +export type SdkSyncPersonAttributesParamsXPlatformFlavor = "native" | "browser" -export type SdkSyncPersonAttributesParamsXSdkEnum = "web"; +export type SdkSyncPersonAttributesParamsXSdk = "react-native" | "web" export interface SdkSyncPersonAttributesParams { readonly "x-distinct-id": string; @@ -613,52 +674,40 @@ export interface SdkSyncPersonAttributesParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkSyncPersonAttributesParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkSyncPersonAttributesParamsXIsDebugBuildEnum - | SdkSyncPersonAttributesParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkSyncPersonAttributesParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkSyncPersonAttributesParamsXObserverModeEnum - | SdkSyncPersonAttributesParamsXObserverModeEnum; + readonly "x-observer-mode": SdkSyncPersonAttributesParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkSyncPersonAttributesParamsXPlatformFlavorEnum - | SdkSyncPersonAttributesParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkSyncPersonAttributesParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkSyncPersonAttributesParamsXSdkEnum | SdkSyncPersonAttributesParamsXSdkEnum; + readonly "x-sdk": SdkSyncPersonAttributesParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } export interface SdkSyncPersonAttributesBody { - readonly email?: string | null | undefined; - readonly name?: string | null | undefined; - readonly traits?: Record | null | undefined; - readonly setOnce?: Record | null | undefined; - readonly clientEventId?: string | null | undefined; + readonly "email"?: string | null | undefined; + readonly "name"?: string | null | undefined; + readonly "traits"?: Record | null | undefined; + readonly "setOnce"?: Record | null | undefined; + readonly "clientEventId"?: string | null | undefined } -export type SdkSyncPersonAttributes400 = ApiSdkValidationError | EffectHttpApiSchemaError; - -export type SdkSyncPersonAttributes500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkSyncPersonAttributes500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkSyncTransactionParamsXIsBackgrounded = "false" | "true"; +export type SdkSyncTransactionParamsXIsBackgrounded = "false" -export type SdkSyncTransactionParamsXIsDebugBuildEnum = "false" | "true"; +export type SdkSyncTransactionParamsXIsDebugBuild = "true" | "false" -export type SdkSyncTransactionParamsXObserverModeEnum = "false" | "true"; +export type SdkSyncTransactionParamsXObserverMode = "true" | "false" -export type SdkSyncTransactionParamsXPlatformFlavorEnum = "browser" | "native"; +export type SdkSyncTransactionParamsXPlatformFlavor = "native" | "browser" -export type SdkSyncTransactionParamsXSdkEnum = "react-native" | "web"; +export type SdkSyncTransactionParamsXSdk = "react-native" | "web" export interface SdkSyncTransactionParams { readonly "x-distinct-id": string; @@ -667,74 +716,54 @@ export interface SdkSyncTransactionParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkSyncTransactionParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkSyncTransactionParamsXIsDebugBuildEnum - | SdkSyncTransactionParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkSyncTransactionParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkSyncTransactionParamsXObserverModeEnum - | SdkSyncTransactionParamsXObserverModeEnum; + readonly "x-observer-mode": SdkSyncTransactionParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkSyncTransactionParamsXPlatformFlavorEnum - | SdkSyncTransactionParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkSyncTransactionParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkSyncTransactionParamsXSdkEnum | SdkSyncTransactionParamsXSdkEnum; + readonly "x-sdk": SdkSyncTransactionParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } -export type SdkSyncTransactionRequestPlatformEnum = "android" | "ios"; +export type SdkSyncTransactionRequestPlatform = "ios" | "android" -export type SdkSyncTransactionRequestPurchaseDateEnum = "-Infinity"; +export type SdkSyncTransactionRequestPurchaseDateEnum = "Infinity" | "-Infinity" | "NaN" -export type SdkSyncTransactionRequestQuantityEnum = "-Infinity"; +export type SdkSyncTransactionRequestQuantityEnum = "Infinity" | "-Infinity" | "NaN" export interface SdkSyncTransactionRequest { - readonly appAccountToken?: string | null | undefined; - readonly platform: SdkSyncTransactionRequestPlatformEnum | SdkSyncTransactionRequestPlatformEnum; - readonly providerProductId?: string | null | undefined; - readonly productSlug: string; - readonly purchaseDate: - | number - | SdkSyncTransactionRequestPurchaseDateEnum - | SdkSyncTransactionRequestPurchaseDateEnum - | SdkSyncTransactionRequestPurchaseDateEnum; - readonly purchaseToken?: string | null | undefined; - readonly quantity: - | number - | SdkSyncTransactionRequestQuantityEnum - | SdkSyncTransactionRequestQuantityEnum - | SdkSyncTransactionRequestQuantityEnum; - readonly receipt?: string | null | undefined; - readonly transactionId: string; + readonly "appAccountToken"?: string | null | undefined; + readonly "platform": SdkSyncTransactionRequestPlatform; + readonly "providerProductId"?: string | null | undefined; + readonly "productSlug": string; + readonly "purchaseDate": number | "NaN" | "Infinity" | "-Infinity" | SdkSyncTransactionRequestPurchaseDateEnum; + readonly "purchaseToken"?: string | null | undefined; + readonly "quantity": number | "NaN" | "Infinity" | "-Infinity" | SdkSyncTransactionRequestQuantityEnum; + readonly "receipt"?: string | null | undefined; + readonly "transactionId": string } export interface SdkSyncTransactionResponse { - readonly accepted: boolean; + readonly "accepted": boolean } -export type SdkSyncTransaction400 = ApiSdkValidationError | EffectHttpApiSchemaError; +export type SdkSyncTransaction500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkSyncTransaction500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkEvaluateFeatureFlagsParamsXIsBackgrounded = "false" -export type SdkEvaluateFeatureFlagsParamsXIsBackgrounded = "false"; +export type SdkEvaluateFeatureFlagsParamsXIsDebugBuild = "true" | "false" -export type SdkEvaluateFeatureFlagsParamsXIsDebugBuildEnum = "false"; +export type SdkEvaluateFeatureFlagsParamsXObserverMode = "true" | "false" -export type SdkEvaluateFeatureFlagsParamsXObserverModeEnum = "false"; +export type SdkEvaluateFeatureFlagsParamsXPlatformFlavor = "native" | "browser" -export type SdkEvaluateFeatureFlagsParamsXPlatformFlavorEnum = "browser"; - -export type SdkEvaluateFeatureFlagsParamsXSdkEnum = "web"; +export type SdkEvaluateFeatureFlagsParamsXSdk = "react-native" | "web" export interface SdkEvaluateFeatureFlagsParams { readonly "x-distinct-id": string; @@ -743,56 +772,46 @@ export interface SdkEvaluateFeatureFlagsParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkEvaluateFeatureFlagsParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkEvaluateFeatureFlagsParamsXIsDebugBuildEnum - | SdkEvaluateFeatureFlagsParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkEvaluateFeatureFlagsParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkEvaluateFeatureFlagsParamsXObserverModeEnum - | SdkEvaluateFeatureFlagsParamsXObserverModeEnum; + readonly "x-observer-mode": SdkEvaluateFeatureFlagsParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkEvaluateFeatureFlagsParamsXPlatformFlavorEnum - | SdkEvaluateFeatureFlagsParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkEvaluateFeatureFlagsParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkEvaluateFeatureFlagsParamsXSdkEnum | SdkEvaluateFeatureFlagsParamsXSdkEnum; + readonly "x-sdk": SdkEvaluateFeatureFlagsParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } export interface EvaluateFeatureFlagsBody { - readonly flagKeys?: ReadonlyArray | null | undefined; + readonly "flagKeys"?: ReadonlyArray | null | undefined } export interface SdkFeatureFlagResult { - readonly enabled: boolean; - readonly key: string; - readonly variantKey: string | null; + readonly "enabled": boolean; + readonly "key": string; + readonly "variantKey": string | null } export interface SdkFeatureFlagsResponse { - readonly flags: ReadonlyArray; + readonly "flags": ReadonlyArray } -export type SdkEvaluateFeatureFlags500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkEvaluateFeatureFlags500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkResolvePaywallParamsXIsBackgrounded = "false"; +export type SdkResolvePaywallParamsXIsBackgrounded = "false" -export type SdkResolvePaywallParamsXIsDebugBuildEnum = "false"; +export type SdkResolvePaywallParamsXIsDebugBuild = "true" | "false" -export type SdkResolvePaywallParamsXObserverModeEnum = "false"; +export type SdkResolvePaywallParamsXObserverMode = "true" | "false" -export type SdkResolvePaywallParamsXPlatformFlavorEnum = "browser"; +export type SdkResolvePaywallParamsXPlatformFlavor = "native" | "browser" -export type SdkResolvePaywallParamsXSdkEnum = "web"; +export type SdkResolvePaywallParamsXSdk = "react-native" | "web" export interface SdkResolvePaywallParams { readonly "x-distinct-id": string; @@ -801,93 +820,75 @@ export interface SdkResolvePaywallParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkResolvePaywallParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkResolvePaywallParamsXIsDebugBuildEnum - | SdkResolvePaywallParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkResolvePaywallParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkResolvePaywallParamsXObserverModeEnum - | SdkResolvePaywallParamsXObserverModeEnum; + readonly "x-observer-mode": SdkResolvePaywallParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkResolvePaywallParamsXPlatformFlavorEnum - | SdkResolvePaywallParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkResolvePaywallParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkResolvePaywallParamsXSdkEnum | SdkResolvePaywallParamsXSdkEnum; + readonly "x-sdk": SdkResolvePaywallParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } export interface SdkResolvePaywallBody { - readonly locationSlug: string; + readonly "locationSlug": string } -export type SdkResolvedPaywallShowingPaywallReleaseEnumVersionEnum = "-Infinity"; +export type SdkResolvedPaywallShowingPaywallReleaseEnumVersionEnum = "Infinity" | "-Infinity" | "NaN" -export type SdkResolvedPaywallShowingTypeEnum = "feature_flag"; +export type SdkResolvedPaywallShowingType = "paywall_release" | "feature_flag" export interface SdkResolvedPaywallShowing { - readonly id: string; - readonly paywall: { - readonly id: string; - readonly name: string; - readonly slug: string; - } | null; - readonly paywallId: string | null; - readonly paywallRelease: { - readonly htmlUrl: string; - readonly publishedAt: string | null; - readonly releaseId: string; - readonly version: number | "NaN" | "Infinity" | "-Infinity"; - // extended by hand for paywall-deploy contract §6 (code-release runtime block); keep when regenerating. - // Optional/nullable on purpose: old servers omit it (visual-editor releases send null) and the - // client decodes responses with a plain JSON cast, so absence must remain valid. - readonly runtime?: - | { - readonly contentHash: string; - readonly productSlugs: ReadonlyArray; - readonly variables: Readonly>; - } - | null - | undefined; - } | null; - readonly paywallReleaseId: string | null; - readonly startedAt: string; - readonly type: SdkResolvedPaywallShowingTypeEnum | SdkResolvedPaywallShowingTypeEnum; + readonly "id": string; + readonly "paywall": { + readonly "id": string; + readonly "name": string; + readonly "slug": string +} | null; + readonly "paywallId": string | null; + readonly "paywallRelease": { + readonly "htmlUrl": string; + readonly "publishedAt": string | null; + readonly "releaseId": string; + readonly "runtime": { + readonly "contentHash": string; + readonly "productSlugs": ReadonlyArray; + readonly "variables": Record +} | null; + readonly "version": number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" +} | null; + readonly "paywallReleaseId": string | null; + readonly "startedAt": string; + readonly "type": SdkResolvedPaywallShowingType } export interface SdkResolvedPaywall { - readonly location: { - readonly id: string; - readonly name: string; - readonly slug: string; - }; - readonly showing: SdkResolvedPaywallShowing; + readonly "location": { + readonly "id": string; + readonly "name": string; + readonly "slug": string +}; + readonly "showing": SdkResolvedPaywallShowing } -export type SdkResolvePaywall200 = SdkResolvedPaywall | null; - -export type SdkResolvePaywall400 = ApiSdkValidationError | EffectHttpApiSchemaError; +export type SdkResolvePaywall200 = SdkResolvedPaywall | null -export type SdkResolvePaywall500 = - | ApiAuthenticationError - | ApiSdkServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type SdkResolvePaywall500 = ApiAuthenticationError | ApiSdkServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type SdkGetSchemaParamsXIsBackgrounded = "false"; +export type SdkGetSchemaParamsXIsBackgrounded = "false" -export type SdkGetSchemaParamsXIsDebugBuildEnum = "false"; +export type SdkGetSchemaParamsXIsDebugBuild = "true" | "false" -export type SdkGetSchemaParamsXObserverModeEnum = "false"; +export type SdkGetSchemaParamsXObserverMode = "true" | "false" -export type SdkGetSchemaParamsXPlatformFlavorEnum = "browser"; +export type SdkGetSchemaParamsXPlatformFlavor = "native" | "browser" -export type SdkGetSchemaParamsXSdkEnum = "web"; +export type SdkGetSchemaParamsXSdk = "react-native" | "web" export interface SdkGetSchemaParams { readonly "x-distinct-id": string; @@ -896,374 +897,395 @@ export interface SdkGetSchemaParams { readonly "x-client-locale"?: string | null | undefined; readonly "x-client-version"?: string | null | undefined; readonly "x-is-backgrounded": SdkGetSchemaParamsXIsBackgrounded; - readonly "x-is-debug-build": - | SdkGetSchemaParamsXIsDebugBuildEnum - | SdkGetSchemaParamsXIsDebugBuildEnum; + readonly "x-is-debug-build": SdkGetSchemaParamsXIsDebugBuild; readonly "x-nonce"?: string | null | undefined; - readonly "x-observer-mode": - | SdkGetSchemaParamsXObserverModeEnum - | SdkGetSchemaParamsXObserverModeEnum; + readonly "x-observer-mode": SdkGetSchemaParamsXObserverMode; readonly "x-platform": string; readonly "x-platform-brand"?: string | null | undefined; readonly "x-platform-device"?: string | null | undefined; - readonly "x-platform-flavor": - | SdkGetSchemaParamsXPlatformFlavorEnum - | SdkGetSchemaParamsXPlatformFlavorEnum; + readonly "x-platform-flavor": SdkGetSchemaParamsXPlatformFlavor; readonly "x-platform-flavor-version"?: string | null | undefined; readonly "x-platform-version"?: string | null | undefined; readonly "x-preferred-locales"?: string | null | undefined; - readonly "x-sdk": SdkGetSchemaParamsXSdkEnum | SdkGetSchemaParamsXSdkEnum; + readonly "x-sdk": SdkGetSchemaParamsXSdk; readonly "x-sdk-version": string; - readonly "x-storefront"?: string | null | undefined; + readonly "x-storefront"?: string | null | undefined } export interface SdkSchema { - readonly locations: Record; - readonly perks: Record; - readonly products: Record; - readonly version: string; + readonly "locations": Record; + readonly "perks": Record; + readonly "products": Record; + readonly "version": string +} + +export type SdkGetSchema500 = ApiAuthenticationError | ApiSchemaServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type SdkRegisterDeviceParamsXIsBackgrounded = "false" + +export type SdkRegisterDeviceParamsXIsDebugBuild = "true" | "false" + +export type SdkRegisterDeviceParamsXObserverMode = "true" | "false" + +export type SdkRegisterDeviceParamsXPlatformFlavor = "native" | "browser" + +export type SdkRegisterDeviceParamsXSdk = "react-native" | "web" + +export interface SdkRegisterDeviceParams { + readonly "x-distinct-id": string; + readonly "x-publishable-key": string; + readonly "x-client-bundle-id": string; + readonly "x-client-locale"?: string | null | undefined; + readonly "x-client-version"?: string | null | undefined; + readonly "x-is-backgrounded": SdkRegisterDeviceParamsXIsBackgrounded; + readonly "x-is-debug-build": SdkRegisterDeviceParamsXIsDebugBuild; + readonly "x-nonce"?: string | null | undefined; + readonly "x-observer-mode": SdkRegisterDeviceParamsXObserverMode; + readonly "x-platform": string; + readonly "x-platform-brand"?: string | null | undefined; + readonly "x-platform-device"?: string | null | undefined; + readonly "x-platform-flavor": SdkRegisterDeviceParamsXPlatformFlavor; + readonly "x-platform-flavor-version"?: string | null | undefined; + readonly "x-platform-version"?: string | null | undefined; + readonly "x-preferred-locales"?: string | null | undefined; + readonly "x-sdk": SdkRegisterDeviceParamsXSdk; + readonly "x-sdk-version": string; + readonly "x-storefront"?: string | null | undefined +} + +export type RegisterDeviceBodyPlatform = "ios" | "android" + +export type RegisterDeviceBodyProvider = "fcm" | "apns" + +export type RegisterDeviceBodyEnvironmentEnum = "sandbox" | "production" + +export interface RegisterDeviceBody { + readonly "platform": RegisterDeviceBodyPlatform; + readonly "provider": RegisterDeviceBodyProvider; + readonly "platformToken": string; + readonly "bundleId"?: string | null | undefined; + readonly "environment"?: RegisterDeviceBodyEnvironmentEnum | null | undefined; + readonly "previousPushDeviceTokenId"?: string | null | undefined +} + +export interface RegisterDeviceResponse { + readonly "pushDeviceTokenId": string +} + +export type ApiPushDeviceNotFoundErrorTag = "Api/PushDeviceNotFoundError" + +export interface ApiPushDeviceNotFoundError { + readonly "_tag": ApiPushDeviceNotFoundErrorTag; + readonly "message": string } -export type SdkGetSchema500 = - | ApiAuthenticationError - | ApiSchemaServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type ApiPushDeviceServiceErrorTag = "Api/PushDeviceServiceError" + +export interface ApiPushDeviceServiceError { + readonly "_tag": ApiPushDeviceServiceErrorTag; + readonly "cause": string +} + +export type SdkRegisterDevice500 = ApiAuthenticationError | ApiPushDeviceServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type SdkRefreshDeviceParamsXIsBackgrounded = "false" + +export type SdkRefreshDeviceParamsXIsDebugBuild = "true" | "false" + +export type SdkRefreshDeviceParamsXObserverMode = "true" | "false" + +export type SdkRefreshDeviceParamsXPlatformFlavor = "native" | "browser" + +export type SdkRefreshDeviceParamsXSdk = "react-native" | "web" + +export interface SdkRefreshDeviceParams { + readonly "x-distinct-id": string; + readonly "x-publishable-key": string; + readonly "x-client-bundle-id": string; + readonly "x-client-locale"?: string | null | undefined; + readonly "x-client-version"?: string | null | undefined; + readonly "x-is-backgrounded": SdkRefreshDeviceParamsXIsBackgrounded; + readonly "x-is-debug-build": SdkRefreshDeviceParamsXIsDebugBuild; + readonly "x-nonce"?: string | null | undefined; + readonly "x-observer-mode": SdkRefreshDeviceParamsXObserverMode; + readonly "x-platform": string; + readonly "x-platform-brand"?: string | null | undefined; + readonly "x-platform-device"?: string | null | undefined; + readonly "x-platform-flavor": SdkRefreshDeviceParamsXPlatformFlavor; + readonly "x-platform-flavor-version"?: string | null | undefined; + readonly "x-platform-version"?: string | null | undefined; + readonly "x-preferred-locales"?: string | null | undefined; + readonly "x-sdk": SdkRefreshDeviceParamsXSdk; + readonly "x-sdk-version": string; + readonly "x-storefront"?: string | null | undefined +} + +export interface RefreshDeviceBody { + readonly "pushDeviceTokenId": string; + readonly "platformToken": string +} + +export type SdkRefreshDevice500 = ApiAuthenticationError | ApiPushDeviceServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type SdkUnregisterDeviceParamsXIsBackgrounded = "false" + +export type SdkUnregisterDeviceParamsXIsDebugBuild = "true" | "false" + +export type SdkUnregisterDeviceParamsXObserverMode = "true" | "false" + +export type SdkUnregisterDeviceParamsXPlatformFlavor = "native" | "browser" + +export type SdkUnregisterDeviceParamsXSdk = "react-native" | "web" + +export interface SdkUnregisterDeviceParams { + readonly "x-distinct-id": string; + readonly "x-publishable-key": string; + readonly "x-client-bundle-id": string; + readonly "x-client-locale"?: string | null | undefined; + readonly "x-client-version"?: string | null | undefined; + readonly "x-is-backgrounded": SdkUnregisterDeviceParamsXIsBackgrounded; + readonly "x-is-debug-build": SdkUnregisterDeviceParamsXIsDebugBuild; + readonly "x-nonce"?: string | null | undefined; + readonly "x-observer-mode": SdkUnregisterDeviceParamsXObserverMode; + readonly "x-platform": string; + readonly "x-platform-brand"?: string | null | undefined; + readonly "x-platform-device"?: string | null | undefined; + readonly "x-platform-flavor": SdkUnregisterDeviceParamsXPlatformFlavor; + readonly "x-platform-flavor-version"?: string | null | undefined; + readonly "x-platform-version"?: string | null | undefined; + readonly "x-preferred-locales"?: string | null | undefined; + readonly "x-sdk": SdkUnregisterDeviceParamsXSdk; + readonly "x-sdk-version": string; + readonly "x-storefront"?: string | null | undefined +} + +export interface UnregisterDeviceBody { + readonly "pushDeviceTokenId": string +} + +export type SdkUnregisterDevice500 = ApiAuthenticationError | ApiPushDeviceServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface User { - readonly createdAt: string; - readonly email: string; - readonly emailVerified: boolean; - readonly id: string; - readonly image: string | null; - readonly name: string; - readonly organizations: ReadonlyArray<{ - readonly id: string; - readonly logo: string | null; - readonly name: string; - readonly slug: string; - }>; - readonly projects: ReadonlyArray<{ - readonly id: string; - readonly logo: string | null; - readonly name: string; - readonly organizationId: string; - readonly slug: string; - }>; - readonly updatedAt: string; -} - -export type ApiUserServiceErrorTag = "Api/UserServiceError"; + readonly "createdAt": string; + readonly "email": string; + readonly "emailVerified": boolean; + readonly "id": string; + readonly "image": string | null; + readonly "name": string; + readonly "organizations": ReadonlyArray<{ + readonly "id": string; + readonly "logo": string | null; + readonly "name": string; + readonly "slug": string; + readonly "workosOrganizationId": string | null +}>; + readonly "projects": ReadonlyArray<{ + readonly "id": string; + readonly "logo": string | null; + readonly "name": string; + readonly "organizationId": string; + readonly "slug": string +}>; + readonly "updatedAt": string +} + +export type ApiUserServiceErrorTag = "Api/UserServiceError" export interface ApiUserServiceError { - readonly _tag: ApiUserServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiUserServiceErrorTag; + readonly "cause": string } -export type UsersGetUser500 = - | ApiAuthenticationError - | ApiUserServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type UsersGetUser500 = ApiAuthenticationError | ApiUserServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface PaymentProviderConfiguration { - readonly enabled: boolean; - readonly id: string; - readonly name: string; - readonly projectId: string; - readonly providerId: string; + readonly "enabled": boolean; + readonly "id": string; + readonly "name": string; + readonly "projectId": string; + readonly "providerId": string } -export type PaymentProviderConfigurationsListPaymentProviderConfigurations200 = - ReadonlyArray; +export type PaymentProviderConfigurationsListPaymentProviderConfigurations200 = ReadonlyArray -export type ApiPaymentProviderConfigurationServiceErrorTag = - "Api/PaymentProviderConfigurationServiceError"; +export type ApiPaymentProviderConfigurationServiceErrorTag = "Api/PaymentProviderConfigurationServiceError" export interface ApiPaymentProviderConfigurationServiceError { - readonly _tag: ApiPaymentProviderConfigurationServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiPaymentProviderConfigurationServiceErrorTag; + readonly "cause": string } -export type PaymentProviderConfigurationsListPaymentProviderConfigurations500 = - | ApiPaymentProviderConfigurationServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type PaymentProviderConfigurationsListPaymentProviderConfigurations500 = ApiPaymentProviderConfigurationServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface PaymentProviderProduct { - readonly configuration: Record; - readonly id: string; - readonly paymentProviderConfigurationId: string; - readonly productId: string; - readonly providerId: string; + readonly "configuration": Record; + readonly "id": string; + readonly "paymentProviderConfigurationId": string; + readonly "productId": string; + readonly "providerId": string } -export type PaymentProviderProductsListPaymentProviderProducts200 = - ReadonlyArray; +export type PaymentProviderProductsListPaymentProviderProducts200 = ReadonlyArray -export type ApiPaymentProviderProductServiceErrorTag = "Api/PaymentProviderProductServiceError"; +export type ApiPaymentProviderProductServiceErrorTag = "Api/PaymentProviderProductServiceError" export interface ApiPaymentProviderProductServiceError { - readonly _tag: ApiPaymentProviderProductServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiPaymentProviderProductServiceErrorTag; + readonly "cause": string } -export type PaymentProviderProductsListPaymentProviderProducts500 = - | ApiPaymentProviderProductServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type PaymentProviderProductsListPaymentProviderProducts500 = ApiPaymentProviderProductServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhookEndpointConsecutiveFailuresEnum = "-Infinity"; +export type WebhookEndpointConsecutiveFailuresEnum = "Infinity" | "-Infinity" | "NaN" -export type WebhookEndpointStatusEnum = "failed"; +export type WebhookEndpointStatus = "active" | "disabled" | "failed" export interface WebhookEndpoint { - readonly consecutiveFailures: - | number - | WebhookEndpointConsecutiveFailuresEnum - | WebhookEndpointConsecutiveFailuresEnum - | WebhookEndpointConsecutiveFailuresEnum; - readonly createdAt: string | null; - readonly description: string | null; - readonly events: ReadonlyArray< - | "person.created" - | "person.updated" - | "person.deleted" - | "subscription.created" - | "subscription.renewed" - | "subscription.cancelled" - | "subscription.expired" - | "purchase.completed" - | "purchase.refunded" - >; - readonly id: string; - readonly lastSuccessAt: string | null; - readonly name: string; - readonly projectId: string; - readonly secret: string; - readonly status: - | WebhookEndpointStatusEnum - | WebhookEndpointStatusEnum - | WebhookEndpointStatusEnum; - readonly url: string; -} - -export type WebhooksListWebhookEndpoints200 = ReadonlyArray; - -export type ApiWebhookServiceErrorTag = "Api/WebhookServiceError"; + readonly "consecutiveFailures": number | "NaN" | "Infinity" | "-Infinity" | WebhookEndpointConsecutiveFailuresEnum; + readonly "createdAt": string | null; + readonly "description": string | null; + readonly "events": ReadonlyArray<"person.created" | "person.updated" | "person.deleted" | "subscription.created" | "subscription.renewed" | "subscription.cancelled" | "subscription.expired" | "purchase.completed" | "purchase.refunded">; + readonly "id": string; + readonly "lastSuccessAt": string | null; + readonly "name": string; + readonly "projectId": string; + readonly "secret": string; + readonly "status": WebhookEndpointStatus; + readonly "url": string +} + +export type WebhooksListWebhookEndpoints200 = ReadonlyArray + +export type ApiWebhookServiceErrorTag = "Api/WebhookServiceError" export interface ApiWebhookServiceError { - readonly _tag: ApiWebhookServiceErrorTag; - readonly cause: string; + readonly "_tag": ApiWebhookServiceErrorTag; + readonly "cause": string } -export type WebhooksListWebhookEndpoints500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksListWebhookEndpoints500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export interface CreateWebhookEndpointBody { - readonly description?: string | null | undefined; - readonly events: ReadonlyArray; - readonly name: string; - readonly url: string; + readonly "description"?: string | null | undefined; + readonly "events": ReadonlyArray; + readonly "name": string; + readonly "url": string } -export type ApiWebhookValidationErrorTag = "Api/WebhookValidationError"; +export type ApiWebhookValidationErrorTag = "Api/WebhookValidationError" export interface ApiWebhookValidationError { - readonly _tag: ApiWebhookValidationErrorTag; - readonly message: string; + readonly "_tag": ApiWebhookValidationErrorTag; + readonly "message": string } -export type WebhooksCreateWebhookEndpoint400 = ApiWebhookValidationError | EffectHttpApiSchemaError; +export type WebhooksCreateWebhookEndpoint500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhooksCreateWebhookEndpoint500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; - -export type ApiWebhookEndpointNotFoundErrorTag = "Api/WebhookEndpointNotFoundError"; +export type ApiWebhookEndpointNotFoundErrorTag = "Api/WebhookEndpointNotFoundError" export interface ApiWebhookEndpointNotFoundError { - readonly _tag: ApiWebhookEndpointNotFoundErrorTag; - readonly endpointId: string; + readonly "_tag": ApiWebhookEndpointNotFoundErrorTag; + readonly "endpointId": string } -export type WebhooksGetWebhookEndpoint500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksGetWebhookEndpoint500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhooksDeleteWebhookEndpoint500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksDeleteWebhookEndpoint500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type UpdateWebhookEndpointBodyStatusEnum = "disabled"; +export type UpdateWebhookEndpointBodyStatusEnum = "active" | "disabled" export interface UpdateWebhookEndpointBody { - readonly description?: string | null | null | undefined; - readonly events?: ReadonlyArray | null | undefined; - readonly name?: string | null | undefined; - readonly status?: - | UpdateWebhookEndpointBodyStatusEnum - | UpdateWebhookEndpointBodyStatusEnum - | null - | undefined; - readonly url?: string | null | undefined; + readonly "description"?: string | null | null | undefined; + readonly "events"?: ReadonlyArray | null | undefined; + readonly "name"?: string | null | undefined; + readonly "status"?: UpdateWebhookEndpointBodyStatusEnum | null | undefined; + readonly "url"?: string | null | undefined } -export type WebhooksUpdateWebhookEndpoint400 = ApiWebhookValidationError | EffectHttpApiSchemaError; - -export type WebhooksUpdateWebhookEndpoint500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksUpdateWebhookEndpoint500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhooksRotateWebhookSecret500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksRotateWebhookSecret500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhookDeliveryAttemptCountEnum = "-Infinity"; +export type WebhookDeliveryAttemptCountEnum = "Infinity" | "-Infinity" | "NaN" -export type WebhookDeliveryMaxAttemptsEnum = "-Infinity"; +export type WebhookDeliveryMaxAttemptsEnum = "Infinity" | "-Infinity" | "NaN" -export type WebhookDeliveryStatusEnum = "exhausted"; +export type WebhookDeliveryStatus = "pending" | "in_progress" | "succeeded" | "failed" | "exhausted" export interface WebhookDelivery { - readonly attemptCount: - | number - | WebhookDeliveryAttemptCountEnum - | WebhookDeliveryAttemptCountEnum - | WebhookDeliveryAttemptCountEnum; - readonly completedAt: string | null; - readonly createdAt: string | null; - readonly eventOccurredAt: string; - readonly eventType: string; - readonly id: string; - readonly maxAttempts: - | number - | WebhookDeliveryMaxAttemptsEnum - | WebhookDeliveryMaxAttemptsEnum - | WebhookDeliveryMaxAttemptsEnum; - readonly nextAttemptAt: string | null; - readonly payload: null; - readonly projectId: string; - readonly status: - | WebhookDeliveryStatusEnum - | WebhookDeliveryStatusEnum - | WebhookDeliveryStatusEnum - | WebhookDeliveryStatusEnum - | WebhookDeliveryStatusEnum; - readonly webhookEndpointId: string; -} - -export type WebhooksTestWebhookEndpoint500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; - -export type WebhooksListWebhookDeliveries200 = ReadonlyArray; - -export type WebhooksListWebhookDeliveries500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; - -export type WebhookDeliveryWithAttemptsAttemptCountEnum = "-Infinity"; - -export type WebhookDeliveryAttemptAttemptNumberEnum = "-Infinity"; - -export type WebhookDeliveryAttemptDurationMsEnum = "-Infinity"; - -export type WebhookDeliveryAttemptStatusCodeEnum = "-Infinity"; + readonly "attemptCount": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryAttemptCountEnum; + readonly "completedAt": string | null; + readonly "createdAt": string | null; + readonly "eventOccurredAt": string; + readonly "eventType": string; + readonly "id": string; + readonly "maxAttempts": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryMaxAttemptsEnum; + readonly "nextAttemptAt": string | null; + readonly "projectId": string; + readonly "status": WebhookDeliveryStatus; + readonly "webhookEndpointId": string +} + +export type WebhooksTestWebhookEndpoint500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type WebhooksListWebhookDeliveries200 = ReadonlyArray + +export type WebhooksListWebhookDeliveries500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError + +export type WebhookDeliveryWithAttemptsAttemptCountEnum = "Infinity" | "-Infinity" | "NaN" + +export type WebhookDeliveryAttemptAttemptNumberEnum = "Infinity" | "-Infinity" | "NaN" + +export type WebhookDeliveryAttemptDurationMsEnum = "Infinity" | "-Infinity" | "NaN" + +export type WebhookDeliveryAttemptStatusCodeEnum = "Infinity" | "-Infinity" | "NaN" export interface WebhookDeliveryAttempt { - readonly attemptNumber: - | number - | WebhookDeliveryAttemptAttemptNumberEnum - | WebhookDeliveryAttemptAttemptNumberEnum - | WebhookDeliveryAttemptAttemptNumberEnum; - readonly createdAt: string | null; - readonly durationMs: - | number - | WebhookDeliveryAttemptDurationMsEnum - | WebhookDeliveryAttemptDurationMsEnum - | WebhookDeliveryAttemptDurationMsEnum - | null; - readonly errorMessage: string | null; - readonly id: string; - readonly responseBody: string | null; - readonly statusCode: - | number - | WebhookDeliveryAttemptStatusCodeEnum - | WebhookDeliveryAttemptStatusCodeEnum - | WebhookDeliveryAttemptStatusCodeEnum - | null; - readonly succeeded: boolean; -} - -export type WebhookDeliveryWithAttemptsMaxAttemptsEnum = "-Infinity"; - -export type WebhookDeliveryWithAttemptsStatusEnum = "exhausted"; + readonly "attemptNumber": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryAttemptAttemptNumberEnum; + readonly "createdAt": string | null; + readonly "durationMs": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryAttemptDurationMsEnum | null; + readonly "errorMessage": string | null; + readonly "id": string; + readonly "responseBody": string | null; + readonly "statusCode": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryAttemptStatusCodeEnum | null; + readonly "succeeded": boolean +} + +export type WebhookDeliveryWithAttemptsMaxAttemptsEnum = "Infinity" | "-Infinity" | "NaN" + +export type WebhookDeliveryWithAttemptsStatus = "pending" | "in_progress" | "succeeded" | "failed" | "exhausted" export interface WebhookDeliveryWithAttempts { - readonly attemptCount: - | number - | WebhookDeliveryWithAttemptsAttemptCountEnum - | WebhookDeliveryWithAttemptsAttemptCountEnum - | WebhookDeliveryWithAttemptsAttemptCountEnum; - readonly attempts: ReadonlyArray; - readonly completedAt: string | null; - readonly createdAt: string | null; - readonly eventOccurredAt: string; - readonly eventType: string; - readonly id: string; - readonly maxAttempts: - | number - | WebhookDeliveryWithAttemptsMaxAttemptsEnum - | WebhookDeliveryWithAttemptsMaxAttemptsEnum - | WebhookDeliveryWithAttemptsMaxAttemptsEnum; - readonly nextAttemptAt: string | null; - readonly payload: null; - readonly projectId: string; - readonly status: - | WebhookDeliveryWithAttemptsStatusEnum - | WebhookDeliveryWithAttemptsStatusEnum - | WebhookDeliveryWithAttemptsStatusEnum - | WebhookDeliveryWithAttemptsStatusEnum - | WebhookDeliveryWithAttemptsStatusEnum; - readonly webhookEndpointId: string; -} - -export type ApiWebhookDeliveryNotFoundErrorTag = "Api/WebhookDeliveryNotFoundError"; + readonly "attemptCount": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryWithAttemptsAttemptCountEnum; + readonly "attempts": ReadonlyArray; + readonly "completedAt": string | null; + readonly "createdAt": string | null; + readonly "eventOccurredAt": string; + readonly "eventType": string; + readonly "id": string; + readonly "maxAttempts": number | "NaN" | "Infinity" | "-Infinity" | WebhookDeliveryWithAttemptsMaxAttemptsEnum; + readonly "nextAttemptAt": string | null; + readonly "projectId": string; + readonly "status": WebhookDeliveryWithAttemptsStatus; + readonly "webhookEndpointId": string +} + +export type ApiWebhookDeliveryNotFoundErrorTag = "Api/WebhookDeliveryNotFoundError" export interface ApiWebhookDeliveryNotFoundError { - readonly _tag: ApiWebhookDeliveryNotFoundErrorTag; - readonly deliveryId: string; + readonly "_tag": ApiWebhookDeliveryNotFoundErrorTag; + readonly "deliveryId": string } -export type WebhooksGetWebhookDelivery500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; - -export type WebhooksRetryWebhookDelivery400 = ApiWebhookValidationError | EffectHttpApiSchemaError; +export type WebhooksGetWebhookDelivery500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError -export type WebhooksRetryWebhookDelivery500 = - | ApiWebhookServiceError - | ApiAuthenticationError - | ApiNotAuthenticatedError; +export type WebhooksRetryWebhookDelivery500 = ApiWebhookServiceError | ApiAuthenticationError | ApiNotAuthenticatedError export const make = ( httpClient: HttpClient.HttpClient, options: { - readonly transformClient?: - | ((client: HttpClient.HttpClient) => Effect.Effect) - | undefined; - } = {}, + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} ): VoidhashCoreClient => { const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( @@ -1275,898 +1297,295 @@ export const make = ( request: response.request, response, description: - typeof description === "string" ? description : JSON.stringify(description), + typeof description === "string" + ? description + : JSON.stringify(description), }), }), ), - ); + ) const withResponse: ( f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, - ) => (request: HttpClientRequest.HttpClientRequest) => Effect.Effect = - options.transformClient - ? (f) => (request) => - Effect.flatMap( - Effect.flatMap(options.transformClient!(httpClient), (client) => - client.execute(request), - ), - f, - ) - : (f) => (request) => Effect.flatMap(httpClient.execute(request), f); + ) => ( + request: HttpClientRequest.HttpClientRequest, + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => + client.execute(request), + ), + f, + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) const decodeSuccess = (response: HttpClientResponse.HttpClientResponse) => - response.json as Effect.Effect; - const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) => Effect.void; + response.json as Effect.Effect + const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) => + Effect.void const decodeError = (tag: Tag) => ( response: HttpClientResponse.HttpClientResponse, - ): Effect.Effect | HttpClientError.HttpClientError> => - Effect.flatMap(response.json as Effect.Effect, (cause) => - Effect.fail(VoidhashCoreClientError(tag, cause, response)), - ); - const onRequest = (successCodes: ReadonlyArray, errorCodes?: Record) => { - const cases: any = { orElse: unexpectedStatus }; + ): Effect.Effect< + never, + VoidhashCoreClientError | HttpClientError.HttpClientError + > => + Effect.flatMap( + response.json as Effect.Effect, + (cause) => Effect.fail(VoidhashCoreClientError(tag, cause, response)), + ) + const onRequest = ( + successCodes: ReadonlyArray, + errorCodes?: Record, + ) => { + const cases: any = { orElse: unexpectedStatus } for (const code of successCodes) { - cases[code] = decodeSuccess; + cases[code] = decodeSuccess } if (errorCodes) { for (const [code, tag] of Object.entries(errorCodes)) { - cases[code] = decodeError(tag); + cases[code] = decodeError(tag) } } if (successCodes.length === 0) { - cases["2xx"] = decodeVoid; + cases["2xx"] = decodeVoid } - return withResponse(HttpClientResponse.matchStatus(cases) as any); - }; + return withResponse(HttpClientResponse.matchStatus(cases) as any) + } return { httpClient, - authSession: () => - HttpClientRequest.get(`/api/v1/auth/session`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "AuthSession500", - }), - ), - apiKeysListApiKeys: () => - HttpClientRequest.get(`/api/v1/api-keys`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "ApiKeysListApiKeys500", - }), - ), - apiKeysCreateSecretKey: (options) => - HttpClientRequest.post(`/api/v1/api-keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "ApiKeysCreateSecretKey500", - }), - ), - apiKeysGetApiKeyById: (apiKeyId) => - HttpClientRequest.get(`/api/v1/api-keys/${apiKeyId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiApiKeyNotFoundError", - "500": "ApiKeysGetApiKeyById500", - }), - ), - apiKeysDeleteApiKey: (apiKeyId) => - HttpClientRequest.delete(`/api/v1/api-keys/${apiKeyId}`).pipe( - onRequest([], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiApiKeyNotFoundError", - "500": "ApiKeysDeleteApiKey500", - }), - ), - apiKeysRotateSecretKey: (apiKeyId) => - HttpClientRequest.post(`/api/v1/api-keys/${apiKeyId}/rotate`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiApiKeyNotFoundError", - "500": "ApiKeysRotateSecretKey500", - }), - ), - personsListPersons: () => - HttpClientRequest.get(`/api/v1/persons`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "PersonsListPersons500", - }), - ), - personsCreatePerson: (options) => - HttpClientRequest.post(`/api/v1/persons`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "PersonsCreatePerson400", - "403": "ApiActionForbiddenError", - "500": "PersonsCreatePerson500", - }), - ), - personsGetPersonById: (personId) => - HttpClientRequest.get(`/api/v1/persons/${personId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiPersonNotFoundError", - "500": "PersonsGetPersonById500", - }), - ), - personsGetPersonByDistinctId: (distinctId) => - HttpClientRequest.get(`/api/v1/persons/by-distinct-id/${distinctId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiPersonNotFoundError", - "500": "PersonsGetPersonByDistinctId500", - }), - ), - organizationsCreateOrganization: (options) => - HttpClientRequest.post(`/api/v1/organizations`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "500": "OrganizationsCreateOrganization500", - }), - ), - perksListPerks: () => - HttpClientRequest.get(`/api/v1/perks`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "PerksListPerks500", - }), - ), - paywallLocationsListPaywallLocations: () => - HttpClientRequest.get(`/api/v1/paywall-locations`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "PaywallLocationsListPaywallLocations500", - }), - ), - schemaGetSchema: () => - HttpClientRequest.get(`/api/v1/schema`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "SchemaGetSchema500", - }), - ), - schemaGetSchemaVersion: () => - HttpClientRequest.get(`/api/v1/schema/version`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "SchemaGetSchemaVersion500", - }), - ), - projectsCreateProject: (options) => - HttpClientRequest.post(`/api/v1/projects`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "ProjectsCreateProject500", - }), - ), - projectsListProjects: (organizationId) => - HttpClientRequest.get(`/api/v1/projects/${organizationId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "ProjectsListProjects500", - }), - ), - productsListProducts: () => - HttpClientRequest.get(`/api/v1/products`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "ProductsListProducts500", - }), - ), - productPerksListProductPerksByProductId: (productId) => - HttpClientRequest.get(`/api/v1/product-perks/by-product-id/${productId}`).pipe( - onRequest(["2xx"], { - "400": "ProductPerksListProductPerksByProductId400", - "403": "ApiActionForbiddenError", - "500": "ProductPerksListProductPerksByProductId500", - }), - ), - sdkGetPerson: (options) => - HttpClientRequest.get(`/api/v1/sdk/person`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options?.["x-client-locale"] ?? undefined, - "x-client-version": options?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options?.["x-is-debug-build"] ?? undefined, - "x-nonce": options?.["x-nonce"] ?? undefined, - "x-observer-mode": options?.["x-observer-mode"] ?? undefined, - "x-platform": options?.["x-platform"] ?? undefined, - "x-platform-brand": options?.["x-platform-brand"] ?? undefined, - "x-platform-device": options?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options?.["x-preferred-locales"] ?? undefined, - "x-sdk": options?.["x-sdk"] ?? undefined, - "x-sdk-version": options?.["x-sdk-version"] ?? undefined, - "x-storefront": options?.["x-storefront"] ?? undefined, - }), - onRequest(["2xx"], { - "400": "SdkGetPerson400", - "404": "ApiSdkPersonNotFoundError", - "500": "SdkGetPerson500", - }), - ), - sdkIdentifyPerson: (options) => - HttpClientRequest.post(`/api/v1/sdk/identify`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options.params?.["x-client-locale"] ?? undefined, - "x-client-version": options.params?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, - "x-nonce": options.params?.["x-nonce"] ?? undefined, - "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, - "x-platform": options.params?.["x-platform"] ?? undefined, - "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, - "x-platform-device": options.params?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options.params?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, - "x-sdk": options.params?.["x-sdk"] ?? undefined, - "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, - "x-storefront": options.params?.["x-storefront"] ?? undefined, - }), - HttpClientRequest.bodyJsonUnsafe(options.payload), - onRequest(["2xx"], { - "400": "SdkIdentifyPerson400", - "404": "ApiSdkPersonNotFoundError", - "409": "ApiSdkPersonAlreadyIdentifiedError", - "500": "SdkIdentifyPerson500", - }), - ), - sdkSyncPersonAttributes: (options) => - HttpClientRequest.post(`/api/v1/sdk/person/traits`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options.params?.["x-client-locale"] ?? undefined, - "x-client-version": options.params?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, - "x-nonce": options.params?.["x-nonce"] ?? undefined, - "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, - "x-platform": options.params?.["x-platform"] ?? undefined, - "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, - "x-platform-device": options.params?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options.params?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, - "x-sdk": options.params?.["x-sdk"] ?? undefined, - "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, - "x-storefront": options.params?.["x-storefront"] ?? undefined, - }), - HttpClientRequest.bodyJsonUnsafe(options.payload), - onRequest(["2xx"], { - "400": "SdkSyncPersonAttributes400", - "404": "ApiSdkPersonNotFoundError", - "500": "SdkSyncPersonAttributes500", - }), - ), - sdkSyncTransaction: (options) => - HttpClientRequest.post(`/api/v1/sdk/sync-transaction`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options.params?.["x-client-locale"] ?? undefined, - "x-client-version": options.params?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, - "x-nonce": options.params?.["x-nonce"] ?? undefined, - "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, - "x-platform": options.params?.["x-platform"] ?? undefined, - "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, - "x-platform-device": options.params?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options.params?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, - "x-sdk": options.params?.["x-sdk"] ?? undefined, - "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, - "x-storefront": options.params?.["x-storefront"] ?? undefined, - }), - HttpClientRequest.bodyJsonUnsafe(options.payload), - onRequest(["2xx"], { "400": "SdkSyncTransaction400", "500": "SdkSyncTransaction500" }), - ), - sdkEvaluateFeatureFlags: (options) => - HttpClientRequest.post(`/api/v1/sdk/evaluate-flags`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options.params?.["x-client-locale"] ?? undefined, - "x-client-version": options.params?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, - "x-nonce": options.params?.["x-nonce"] ?? undefined, - "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, - "x-platform": options.params?.["x-platform"] ?? undefined, - "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, - "x-platform-device": options.params?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options.params?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, - "x-sdk": options.params?.["x-sdk"] ?? undefined, - "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, - "x-storefront": options.params?.["x-storefront"] ?? undefined, - }), - HttpClientRequest.bodyJsonUnsafe(options.payload), - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "500": "SdkEvaluateFeatureFlags500", - }), - ), - sdkResolvePaywall: (options) => - HttpClientRequest.post(`/api/v1/sdk/resolve-paywall`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options.params?.["x-client-locale"] ?? undefined, - "x-client-version": options.params?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, - "x-nonce": options.params?.["x-nonce"] ?? undefined, - "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, - "x-platform": options.params?.["x-platform"] ?? undefined, - "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, - "x-platform-device": options.params?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options.params?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, - "x-sdk": options.params?.["x-sdk"] ?? undefined, - "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, - "x-storefront": options.params?.["x-storefront"] ?? undefined, - }), - HttpClientRequest.bodyJsonUnsafe(options.payload), - onRequest(["2xx"], { "400": "SdkResolvePaywall400", "500": "SdkResolvePaywall500" }), - ), - sdkGetSchema: (options) => - HttpClientRequest.get(`/api/v1/sdk/schema`).pipe( - HttpClientRequest.setHeaders({ - "x-distinct-id": options?.["x-distinct-id"] ?? undefined, - "x-publishable-key": options?.["x-publishable-key"] ?? undefined, - "x-client-bundle-id": options?.["x-client-bundle-id"] ?? undefined, - "x-client-locale": options?.["x-client-locale"] ?? undefined, - "x-client-version": options?.["x-client-version"] ?? undefined, - "x-is-backgrounded": options?.["x-is-backgrounded"] ?? undefined, - "x-is-debug-build": options?.["x-is-debug-build"] ?? undefined, - "x-nonce": options?.["x-nonce"] ?? undefined, - "x-observer-mode": options?.["x-observer-mode"] ?? undefined, - "x-platform": options?.["x-platform"] ?? undefined, - "x-platform-brand": options?.["x-platform-brand"] ?? undefined, - "x-platform-device": options?.["x-platform-device"] ?? undefined, - "x-platform-flavor": options?.["x-platform-flavor"] ?? undefined, - "x-platform-flavor-version": options?.["x-platform-flavor-version"] ?? undefined, - "x-platform-version": options?.["x-platform-version"] ?? undefined, - "x-preferred-locales": options?.["x-preferred-locales"] ?? undefined, - "x-sdk": options?.["x-sdk"] ?? undefined, - "x-sdk-version": options?.["x-sdk-version"] ?? undefined, - "x-storefront": options?.["x-storefront"] ?? undefined, - }), - onRequest(["2xx"], { "400": "EffectHttpApiSchemaError", "500": "SdkGetSchema500" }), - ), - usersGetUser: () => - HttpClientRequest.get(`/api/v1/users/current`).pipe( - onRequest(["2xx"], { "400": "EffectHttpApiSchemaError", "500": "UsersGetUser500" }), - ), - paymentProviderConfigurationsListPaymentProviderConfigurations: () => - HttpClientRequest.get(`/api/v1/payment-provider-configurations`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "PaymentProviderConfigurationsListPaymentProviderConfigurations500", - }), - ), - paymentProviderProductsListPaymentProviderProducts: () => - HttpClientRequest.get(`/api/v1/payment-provider-products`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "PaymentProviderProductsListPaymentProviderProducts500", - }), - ), - webhooksListWebhookEndpoints: () => - HttpClientRequest.get(`/api/v1/webhooks/endpoints`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "WebhooksListWebhookEndpoints500", - }), - ), - webhooksCreateWebhookEndpoint: (options) => - HttpClientRequest.post(`/api/v1/webhooks/endpoints`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "WebhooksCreateWebhookEndpoint400", - "403": "ApiActionForbiddenError", - "500": "WebhooksCreateWebhookEndpoint500", - }), - ), - webhooksGetWebhookEndpoint: (endpointId) => - HttpClientRequest.get(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookEndpointNotFoundError", - "500": "WebhooksGetWebhookEndpoint500", - }), - ), - webhooksDeleteWebhookEndpoint: (endpointId) => - HttpClientRequest.delete(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( - onRequest([], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookEndpointNotFoundError", - "500": "WebhooksDeleteWebhookEndpoint500", - }), - ), - webhooksUpdateWebhookEndpoint: (endpointId, options) => - HttpClientRequest.patch(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "WebhooksUpdateWebhookEndpoint400", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookEndpointNotFoundError", - "500": "WebhooksUpdateWebhookEndpoint500", - }), - ), - webhooksRotateWebhookSecret: (endpointId) => - HttpClientRequest.post(`/api/v1/webhooks/endpoints/${endpointId}/rotate-secret`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookEndpointNotFoundError", - "500": "WebhooksRotateWebhookSecret500", - }), - ), - webhooksTestWebhookEndpoint: (endpointId) => - HttpClientRequest.post(`/api/v1/webhooks/endpoints/${endpointId}/test`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookEndpointNotFoundError", - "500": "WebhooksTestWebhookEndpoint500", - }), - ), - webhooksListWebhookDeliveries: () => - HttpClientRequest.get(`/api/v1/webhooks/deliveries`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "500": "WebhooksListWebhookDeliveries500", - }), - ), - webhooksGetWebhookDelivery: (deliveryId) => - HttpClientRequest.get(`/api/v1/webhooks/deliveries/${deliveryId}`).pipe( - onRequest(["2xx"], { - "400": "EffectHttpApiSchemaError", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookDeliveryNotFoundError", - "500": "WebhooksGetWebhookDelivery500", - }), - ), - webhooksRetryWebhookDelivery: (deliveryId) => - HttpClientRequest.post(`/api/v1/webhooks/deliveries/${deliveryId}/retry`).pipe( - onRequest(["2xx"], { - "400": "WebhooksRetryWebhookDelivery400", - "403": "ApiActionForbiddenError", - "404": "ApiWebhookDeliveryNotFoundError", - "500": "WebhooksRetryWebhookDelivery500", - }), - ), - }; -}; + "authSession": () => HttpClientRequest.get(`/api/v1/auth/session`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"AuthSession500"}) + ), + "apiKeysListApiKeys": () => HttpClientRequest.get(`/api/v1/api-keys`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"ApiKeysListApiKeys500"}) + ), + "apiKeysCreateSecretKey": (options) => HttpClientRequest.post(`/api/v1/api-keys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"ApiKeysCreateSecretKey500"}) + ), + "apiKeysGetApiKeyById": (apiKeyId) => HttpClientRequest.get(`/api/v1/api-keys/${apiKeyId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiApiKeyNotFoundError","500":"ApiKeysGetApiKeyById500"}) + ), + "apiKeysDeleteApiKey": (apiKeyId) => HttpClientRequest.delete(`/api/v1/api-keys/${apiKeyId}`).pipe( + onRequest([], {"403":"ApiActionForbiddenError","404":"ApiApiKeyNotFoundError","500":"ApiKeysDeleteApiKey500"}) + ), + "apiKeysRotateSecretKey": (apiKeyId) => HttpClientRequest.post(`/api/v1/api-keys/${apiKeyId}/rotate`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiApiKeyNotFoundError","500":"ApiKeysRotateSecretKey500"}) + ), + "personsListPersons": () => HttpClientRequest.get(`/api/v1/persons`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"PersonsListPersons500"}) + ), + "personsCreatePerson": (options) => HttpClientRequest.post(`/api/v1/persons`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"ApiPersonInvalidAnonymousIdError","403":"ApiActionForbiddenError","500":"PersonsCreatePerson500"}) + ), + "personsGetPersonById": (personId) => HttpClientRequest.get(`/api/v1/persons/${personId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiPersonNotFoundError","500":"PersonsGetPersonById500"}) + ), + "personsGetPersonByDistinctId": (distinctId) => HttpClientRequest.get(`/api/v1/persons/by-distinct-id/${distinctId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiPersonNotFoundError","500":"PersonsGetPersonByDistinctId500"}) + ), + "notificationsSendNotification": (options) => HttpClientRequest.post(`/api/v1/notifications/send`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"ApiPushDeviceValidationError","403":"ApiActionForbiddenError","409":"ApiPushSendNotEnabledError","500":"NotificationsSendNotification500"}) + ), + "organizationsCreateOrganization": (options) => HttpClientRequest.post(`/api/v1/organizations`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"500":"OrganizationsCreateOrganization500"}) + ), + "perksListPerks": () => HttpClientRequest.get(`/api/v1/perks`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"PerksListPerks500"}) + ), + "paywallDeploysCreateDeploy": (options) => HttpClientRequest.post(`/api/v1/paywall-deploys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"ApiPaywallDeployUpgradeRequiredError","403":"ApiActionForbiddenError","422":"ApiPaywallDeployValidationError","500":"PaywallDeploysCreateDeploy500"}) + ), + "paywallDeploysUploadBlob": (deployId, sha256) => HttpClientRequest.put(`/api/v1/paywall-deploys/${deployId}/blobs/${sha256}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"PaywallDeploysUploadBlob404","409":"ApiPaywallDeployNotPendingError","422":"PaywallDeploysUploadBlob422","500":"PaywallDeploysUploadBlob500"}) + ), + "paywallDeploysFinalizeDeploy": (deployId) => HttpClientRequest.post(`/api/v1/paywall-deploys/${deployId}/finalize`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiPaywallDeployNotFoundError","409":"ApiIncompleteDeployError","422":"ApiPaywallDeployValidationError","500":"PaywallDeploysFinalizeDeploy500"}) + ), + "paywallLocationsListPaywallLocations": () => HttpClientRequest.get(`/api/v1/paywall-locations`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"PaywallLocationsListPaywallLocations500"}) + ), + "schemaGetSchema": () => HttpClientRequest.get(`/api/v1/schema`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"SchemaGetSchema500"}) + ), + "schemaGetSchemaVersion": () => HttpClientRequest.get(`/api/v1/schema/version`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"SchemaGetSchemaVersion500"}) + ), + "projectsCreateProject": (options) => HttpClientRequest.post(`/api/v1/projects`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"ProjectsCreateProject500"}) + ), + "projectsListProjects": (organizationId) => HttpClientRequest.get(`/api/v1/projects/${organizationId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"ProjectsListProjects500"}) + ), + "productsListProducts": () => HttpClientRequest.get(`/api/v1/products`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"ProductsListProducts500"}) + ), + "productPerksListProductPerksByProductId": (productId) => HttpClientRequest.get(`/api/v1/product-perks/by-product-id/${productId}`).pipe( + onRequest(["2xx"], {"400":"ApiProductPerkValidationError","403":"ApiActionForbiddenError","500":"ProductPerksListProductPerksByProductId500"}) + ), + "sdkGetPerson": (options) => HttpClientRequest.get(`/api/v1/sdk/person`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options?.["x-distinct-id"] ?? undefined, "x-publishable-key": options?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options?.["x-client-locale"] ?? undefined, "x-client-version": options?.["x-client-version"] ?? undefined, "x-is-backgrounded": options?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options?.["x-is-debug-build"] ?? undefined, "x-nonce": options?.["x-nonce"] ?? undefined, "x-observer-mode": options?.["x-observer-mode"] ?? undefined, "x-platform": options?.["x-platform"] ?? undefined, "x-platform-brand": options?.["x-platform-brand"] ?? undefined, "x-platform-device": options?.["x-platform-device"] ?? undefined, "x-platform-flavor": options?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options?.["x-platform-version"] ?? undefined, "x-preferred-locales": options?.["x-preferred-locales"] ?? undefined, "x-sdk": options?.["x-sdk"] ?? undefined, "x-sdk-version": options?.["x-sdk-version"] ?? undefined, "x-storefront": options?.["x-storefront"] ?? undefined }), + onRequest(["2xx"], {"400":"ApiSdkValidationError","404":"ApiSdkPersonNotFoundError","500":"SdkGetPerson500"}) + ), + "sdkIdentifyPerson": (options) => HttpClientRequest.post(`/api/v1/sdk/identify`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"400":"ApiSdkValidationError","404":"ApiSdkPersonNotFoundError","409":"ApiSdkPersonAlreadyIdentifiedError","500":"SdkIdentifyPerson500"}) + ), + "sdkSyncPersonAttributes": (options) => HttpClientRequest.post(`/api/v1/sdk/person/traits`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"400":"ApiSdkValidationError","404":"ApiSdkPersonNotFoundError","500":"SdkSyncPersonAttributes500"}) + ), + "sdkSyncTransaction": (options) => HttpClientRequest.post(`/api/v1/sdk/sync-transaction`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"400":"ApiSdkValidationError","500":"SdkSyncTransaction500"}) + ), + "sdkEvaluateFeatureFlags": (options) => HttpClientRequest.post(`/api/v1/sdk/evaluate-flags`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"500":"SdkEvaluateFeatureFlags500"}) + ), + "sdkResolvePaywall": (options) => HttpClientRequest.post(`/api/v1/sdk/resolve-paywall`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"400":"ApiSdkValidationError","500":"SdkResolvePaywall500"}) + ), + "sdkGetSchema": (options) => HttpClientRequest.get(`/api/v1/sdk/schema`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options?.["x-distinct-id"] ?? undefined, "x-publishable-key": options?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options?.["x-client-locale"] ?? undefined, "x-client-version": options?.["x-client-version"] ?? undefined, "x-is-backgrounded": options?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options?.["x-is-debug-build"] ?? undefined, "x-nonce": options?.["x-nonce"] ?? undefined, "x-observer-mode": options?.["x-observer-mode"] ?? undefined, "x-platform": options?.["x-platform"] ?? undefined, "x-platform-brand": options?.["x-platform-brand"] ?? undefined, "x-platform-device": options?.["x-platform-device"] ?? undefined, "x-platform-flavor": options?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options?.["x-platform-version"] ?? undefined, "x-preferred-locales": options?.["x-preferred-locales"] ?? undefined, "x-sdk": options?.["x-sdk"] ?? undefined, "x-sdk-version": options?.["x-sdk-version"] ?? undefined, "x-storefront": options?.["x-storefront"] ?? undefined }), + onRequest(["2xx"], {"500":"SdkGetSchema500"}) + ), + "sdkRegisterDevice": (options) => HttpClientRequest.post(`/api/v1/sdk/push-devices/register`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest(["2xx"], {"400":"ApiPushDeviceValidationError","403":"ApiActionForbiddenError","404":"ApiPushDeviceNotFoundError","500":"SdkRegisterDevice500"}) + ), + "sdkRefreshDevice": (options) => HttpClientRequest.post(`/api/v1/sdk/push-devices/refresh`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest([], {"400":"ApiPushDeviceValidationError","403":"ApiActionForbiddenError","404":"ApiPushDeviceNotFoundError","500":"SdkRefreshDevice500"}) + ), + "sdkUnregisterDevice": (options) => HttpClientRequest.post(`/api/v1/sdk/push-devices/unregister`).pipe( + HttpClientRequest.setHeaders({ "x-distinct-id": options.params?.["x-distinct-id"] ?? undefined, "x-publishable-key": options.params?.["x-publishable-key"] ?? undefined, "x-client-bundle-id": options.params?.["x-client-bundle-id"] ?? undefined, "x-client-locale": options.params?.["x-client-locale"] ?? undefined, "x-client-version": options.params?.["x-client-version"] ?? undefined, "x-is-backgrounded": options.params?.["x-is-backgrounded"] ?? undefined, "x-is-debug-build": options.params?.["x-is-debug-build"] ?? undefined, "x-nonce": options.params?.["x-nonce"] ?? undefined, "x-observer-mode": options.params?.["x-observer-mode"] ?? undefined, "x-platform": options.params?.["x-platform"] ?? undefined, "x-platform-brand": options.params?.["x-platform-brand"] ?? undefined, "x-platform-device": options.params?.["x-platform-device"] ?? undefined, "x-platform-flavor": options.params?.["x-platform-flavor"] ?? undefined, "x-platform-flavor-version": options.params?.["x-platform-flavor-version"] ?? undefined, "x-platform-version": options.params?.["x-platform-version"] ?? undefined, "x-preferred-locales": options.params?.["x-preferred-locales"] ?? undefined, "x-sdk": options.params?.["x-sdk"] ?? undefined, "x-sdk-version": options.params?.["x-sdk-version"] ?? undefined, "x-storefront": options.params?.["x-storefront"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + onRequest([], {"403":"ApiActionForbiddenError","404":"ApiPushDeviceNotFoundError","500":"SdkUnregisterDevice500"}) + ), + "usersGetUser": () => HttpClientRequest.get(`/api/v1/users/current`).pipe( + onRequest(["2xx"], {"500":"UsersGetUser500"}) + ), + "paymentProviderConfigurationsListPaymentProviderConfigurations": () => HttpClientRequest.get(`/api/v1/payment-provider-configurations`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"PaymentProviderConfigurationsListPaymentProviderConfigurations500"}) + ), + "paymentProviderProductsListPaymentProviderProducts": () => HttpClientRequest.get(`/api/v1/payment-provider-products`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"PaymentProviderProductsListPaymentProviderProducts500"}) + ), + "webhooksListWebhookEndpoints": () => HttpClientRequest.get(`/api/v1/webhooks/endpoints`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"WebhooksListWebhookEndpoints500"}) + ), + "webhooksCreateWebhookEndpoint": (options) => HttpClientRequest.post(`/api/v1/webhooks/endpoints`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"ApiWebhookValidationError","403":"ApiActionForbiddenError","500":"WebhooksCreateWebhookEndpoint500"}) + ), + "webhooksGetWebhookEndpoint": (endpointId) => HttpClientRequest.get(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiWebhookEndpointNotFoundError","500":"WebhooksGetWebhookEndpoint500"}) + ), + "webhooksDeleteWebhookEndpoint": (endpointId) => HttpClientRequest.delete(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( + onRequest([], {"403":"ApiActionForbiddenError","404":"ApiWebhookEndpointNotFoundError","500":"WebhooksDeleteWebhookEndpoint500"}) + ), + "webhooksUpdateWebhookEndpoint": (endpointId, options) => HttpClientRequest.patch(`/api/v1/webhooks/endpoints/${endpointId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"ApiWebhookValidationError","403":"ApiActionForbiddenError","404":"ApiWebhookEndpointNotFoundError","500":"WebhooksUpdateWebhookEndpoint500"}) + ), + "webhooksRotateWebhookSecret": (endpointId) => HttpClientRequest.post(`/api/v1/webhooks/endpoints/${endpointId}/rotate-secret`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiWebhookEndpointNotFoundError","500":"WebhooksRotateWebhookSecret500"}) + ), + "webhooksTestWebhookEndpoint": (endpointId) => HttpClientRequest.post(`/api/v1/webhooks/endpoints/${endpointId}/test`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiWebhookEndpointNotFoundError","500":"WebhooksTestWebhookEndpoint500"}) + ), + "webhooksListWebhookDeliveries": () => HttpClientRequest.get(`/api/v1/webhooks/deliveries`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","500":"WebhooksListWebhookDeliveries500"}) + ), + "webhooksGetWebhookDelivery": (deliveryId) => HttpClientRequest.get(`/api/v1/webhooks/deliveries/${deliveryId}`).pipe( + onRequest(["2xx"], {"403":"ApiActionForbiddenError","404":"ApiWebhookDeliveryNotFoundError","500":"WebhooksGetWebhookDelivery500"}) + ), + "webhooksRetryWebhookDelivery": (deliveryId) => HttpClientRequest.post(`/api/v1/webhooks/deliveries/${deliveryId}/retry`).pipe( + onRequest(["2xx"], {"400":"ApiWebhookValidationError","403":"ApiActionForbiddenError","404":"ApiWebhookDeliveryNotFoundError","500":"WebhooksRetryWebhookDelivery500"}) + ) + } +} export interface VoidhashCoreClient { - readonly httpClient: HttpClient.HttpClient; - readonly authSession: () => Effect.Effect< - AuthSession200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"AuthSession500", AuthSession500> - >; - readonly apiKeysListApiKeys: () => Effect.Effect< - ApiKeysListApiKeys200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiKeysListApiKeys500", ApiKeysListApiKeys500> - >; - readonly apiKeysCreateSecretKey: ( - options: CreateSecretKeyBody, - ) => Effect.Effect< - ApiKeyWithRawKey, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiKeysCreateSecretKey500", ApiKeysCreateSecretKey500> - >; - readonly apiKeysGetApiKeyById: ( - apiKeyId: string, - ) => Effect.Effect< - ApiKey, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> - | VoidhashCoreClientError<"ApiKeysGetApiKeyById500", ApiKeysGetApiKeyById500> - >; - readonly apiKeysDeleteApiKey: ( - apiKeyId: string, - ) => Effect.Effect< - void, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> - | VoidhashCoreClientError<"ApiKeysDeleteApiKey500", ApiKeysDeleteApiKey500> - >; - readonly apiKeysRotateSecretKey: ( - apiKeyId: string, - ) => Effect.Effect< - ApiKeyWithRawKey, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> - | VoidhashCoreClientError<"ApiKeysRotateSecretKey500", ApiKeysRotateSecretKey500> - >; - readonly personsListPersons: () => Effect.Effect< - PersonsListPersons200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"PersonsListPersons500", PersonsListPersons500> - >; - readonly personsCreatePerson: ( - options: CreatePersonBody, - ) => Effect.Effect< - Person, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"PersonsCreatePerson400", PersonsCreatePerson400> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"PersonsCreatePerson500", PersonsCreatePerson500> - >; - readonly personsGetPersonById: ( - personId: string, - ) => Effect.Effect< - Person, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiPersonNotFoundError", ApiPersonNotFoundError> - | VoidhashCoreClientError<"PersonsGetPersonById500", PersonsGetPersonById500> - >; - readonly personsGetPersonByDistinctId: ( - distinctId: string, - ) => Effect.Effect< - Person, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiPersonNotFoundError", ApiPersonNotFoundError> - | VoidhashCoreClientError<"PersonsGetPersonByDistinctId500", PersonsGetPersonByDistinctId500> - >; - readonly organizationsCreateOrganization: ( - options: CreateOrganizationBody, - ) => Effect.Effect< - Organization, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError< - "OrganizationsCreateOrganization500", - OrganizationsCreateOrganization500 - > - >; - readonly perksListPerks: () => Effect.Effect< - PerksListPerks200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"PerksListPerks500", PerksListPerks500> - >; - readonly paywallLocationsListPaywallLocations: () => Effect.Effect< - PaywallLocationsListPaywallLocations200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError< - "PaywallLocationsListPaywallLocations500", - PaywallLocationsListPaywallLocations500 - > - >; - readonly schemaGetSchema: () => Effect.Effect< - ProjectSchemaResponse, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"SchemaGetSchema500", SchemaGetSchema500> - >; - readonly schemaGetSchemaVersion: () => Effect.Effect< - SchemaVersion, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"SchemaGetSchemaVersion500", SchemaGetSchemaVersion500> - >; - readonly projectsCreateProject: ( - options: CreateProjectBody, - ) => Effect.Effect< - Project, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ProjectsCreateProject500", ProjectsCreateProject500> - >; - readonly projectsListProjects: ( - organizationId: string, - ) => Effect.Effect< - ProjectsListProjects200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ProjectsListProjects500", ProjectsListProjects500> - >; - readonly productsListProducts: () => Effect.Effect< - ProductsListProducts200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ProductsListProducts500", ProductsListProducts500> - >; - readonly productPerksListProductPerksByProductId: ( - productId: string, - ) => Effect.Effect< - ProductPerksListProductPerksByProductId200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError< - "ProductPerksListProductPerksByProductId400", - ProductPerksListProductPerksByProductId400 - > - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError< - "ProductPerksListProductPerksByProductId500", - ProductPerksListProductPerksByProductId500 - > - >; - readonly sdkGetPerson: ( - options: SdkGetPersonParams, - ) => Effect.Effect< - SdkPerson, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"SdkGetPerson400", SdkGetPerson400> - | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> - | VoidhashCoreClientError<"SdkGetPerson500", SdkGetPerson500> - >; - readonly sdkIdentifyPerson: (options: { - readonly params: SdkIdentifyPersonParams; - readonly payload: SdkIdentifyBody; - }) => Effect.Effect< - SdkPerson, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"SdkIdentifyPerson400", SdkIdentifyPerson400> - | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> - | VoidhashCoreClientError< - "ApiSdkPersonAlreadyIdentifiedError", - ApiSdkPersonAlreadyIdentifiedError - > - | VoidhashCoreClientError<"SdkIdentifyPerson500", SdkIdentifyPerson500> - >; - readonly sdkSyncPersonAttributes: (options: { - readonly params: SdkSyncPersonAttributesParams; - readonly payload: SdkSyncPersonAttributesBody; - }) => Effect.Effect< - SdkPerson, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"SdkSyncPersonAttributes400", SdkSyncPersonAttributes400> - | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> - | VoidhashCoreClientError<"SdkSyncPersonAttributes500", SdkSyncPersonAttributes500> - >; - readonly sdkSyncTransaction: (options: { - readonly params: SdkSyncTransactionParams; - readonly payload: SdkSyncTransactionRequest; - }) => Effect.Effect< - SdkSyncTransactionResponse, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"SdkSyncTransaction400", SdkSyncTransaction400> - | VoidhashCoreClientError<"SdkSyncTransaction500", SdkSyncTransaction500> - >; - readonly sdkEvaluateFeatureFlags: (options: { - readonly params: SdkEvaluateFeatureFlagsParams; - readonly payload: EvaluateFeatureFlagsBody; - }) => Effect.Effect< - SdkFeatureFlagsResponse, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"SdkEvaluateFeatureFlags500", SdkEvaluateFeatureFlags500> - >; - readonly sdkResolvePaywall: (options: { - readonly params: SdkResolvePaywallParams; - readonly payload: SdkResolvePaywallBody; - }) => Effect.Effect< - SdkResolvePaywall200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"SdkResolvePaywall400", SdkResolvePaywall400> - | VoidhashCoreClientError<"SdkResolvePaywall500", SdkResolvePaywall500> - >; - readonly sdkGetSchema: ( - options: SdkGetSchemaParams, - ) => Effect.Effect< - SdkSchema, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"SdkGetSchema500", SdkGetSchema500> - >; - readonly usersGetUser: () => Effect.Effect< - User, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"UsersGetUser500", UsersGetUser500> - >; - readonly paymentProviderConfigurationsListPaymentProviderConfigurations: () => Effect.Effect< - PaymentProviderConfigurationsListPaymentProviderConfigurations200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError< - "PaymentProviderConfigurationsListPaymentProviderConfigurations500", - PaymentProviderConfigurationsListPaymentProviderConfigurations500 - > - >; - readonly paymentProviderProductsListPaymentProviderProducts: () => Effect.Effect< - PaymentProviderProductsListPaymentProviderProducts200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError< - "PaymentProviderProductsListPaymentProviderProducts500", - PaymentProviderProductsListPaymentProviderProducts500 - > - >; - readonly webhooksListWebhookEndpoints: () => Effect.Effect< - WebhooksListWebhookEndpoints200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"WebhooksListWebhookEndpoints500", WebhooksListWebhookEndpoints500> - >; - readonly webhooksCreateWebhookEndpoint: ( - options: CreateWebhookEndpointBody, - ) => Effect.Effect< - WebhookEndpoint, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"WebhooksCreateWebhookEndpoint400", WebhooksCreateWebhookEndpoint400> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"WebhooksCreateWebhookEndpoint500", WebhooksCreateWebhookEndpoint500> - >; - readonly webhooksGetWebhookEndpoint: ( - endpointId: string, - ) => Effect.Effect< - WebhookEndpoint, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> - | VoidhashCoreClientError<"WebhooksGetWebhookEndpoint500", WebhooksGetWebhookEndpoint500> - >; - readonly webhooksDeleteWebhookEndpoint: ( - endpointId: string, - ) => Effect.Effect< - void, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> - | VoidhashCoreClientError<"WebhooksDeleteWebhookEndpoint500", WebhooksDeleteWebhookEndpoint500> - >; - readonly webhooksUpdateWebhookEndpoint: ( - endpointId: string, - options: UpdateWebhookEndpointBody, - ) => Effect.Effect< - WebhookEndpoint, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"WebhooksUpdateWebhookEndpoint400", WebhooksUpdateWebhookEndpoint400> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> - | VoidhashCoreClientError<"WebhooksUpdateWebhookEndpoint500", WebhooksUpdateWebhookEndpoint500> - >; - readonly webhooksRotateWebhookSecret: ( - endpointId: string, - ) => Effect.Effect< - WebhookEndpoint, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> - | VoidhashCoreClientError<"WebhooksRotateWebhookSecret500", WebhooksRotateWebhookSecret500> - >; - readonly webhooksTestWebhookEndpoint: ( - endpointId: string, - ) => Effect.Effect< - WebhookDelivery, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> - | VoidhashCoreClientError<"WebhooksTestWebhookEndpoint500", WebhooksTestWebhookEndpoint500> - >; - readonly webhooksListWebhookDeliveries: () => Effect.Effect< - WebhooksListWebhookDeliveries200, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"WebhooksListWebhookDeliveries500", WebhooksListWebhookDeliveries500> - >; - readonly webhooksGetWebhookDelivery: ( - deliveryId: string, - ) => Effect.Effect< - WebhookDeliveryWithAttempts, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"EffectHttpApiSchemaError", EffectHttpApiSchemaError> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookDeliveryNotFoundError", ApiWebhookDeliveryNotFoundError> - | VoidhashCoreClientError<"WebhooksGetWebhookDelivery500", WebhooksGetWebhookDelivery500> - >; - readonly webhooksRetryWebhookDelivery: ( - deliveryId: string, - ) => Effect.Effect< - WebhookDelivery, - | HttpClientError.HttpClientError - | VoidhashCoreClientError<"WebhooksRetryWebhookDelivery400", WebhooksRetryWebhookDelivery400> - | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> - | VoidhashCoreClientError<"ApiWebhookDeliveryNotFoundError", ApiWebhookDeliveryNotFoundError> - | VoidhashCoreClientError<"WebhooksRetryWebhookDelivery500", WebhooksRetryWebhookDelivery500> - >; + readonly httpClient: HttpClient.HttpClient + readonly "authSession": () => Effect.Effect | VoidhashCoreClientError<"AuthSession500", AuthSession500>> + readonly "apiKeysListApiKeys": () => Effect.Effect | VoidhashCoreClientError<"ApiKeysListApiKeys500", ApiKeysListApiKeys500>> + readonly "apiKeysCreateSecretKey": (options: CreateSecretKeyBody) => Effect.Effect | VoidhashCoreClientError<"ApiKeysCreateSecretKey500", ApiKeysCreateSecretKey500>> + readonly "apiKeysGetApiKeyById": (apiKeyId: string) => Effect.Effect | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> | VoidhashCoreClientError<"ApiKeysGetApiKeyById500", ApiKeysGetApiKeyById500>> + readonly "apiKeysDeleteApiKey": (apiKeyId: string) => Effect.Effect | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> | VoidhashCoreClientError<"ApiKeysDeleteApiKey500", ApiKeysDeleteApiKey500>> + readonly "apiKeysRotateSecretKey": (apiKeyId: string) => Effect.Effect | VoidhashCoreClientError<"ApiApiKeyNotFoundError", ApiApiKeyNotFoundError> | VoidhashCoreClientError<"ApiKeysRotateSecretKey500", ApiKeysRotateSecretKey500>> + readonly "personsListPersons": () => Effect.Effect | VoidhashCoreClientError<"PersonsListPersons500", PersonsListPersons500>> + readonly "personsCreatePerson": (options: CreatePersonBody) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"PersonsCreatePerson500", PersonsCreatePerson500>> + readonly "personsGetPersonById": (personId: string) => Effect.Effect | VoidhashCoreClientError<"ApiPersonNotFoundError", ApiPersonNotFoundError> | VoidhashCoreClientError<"PersonsGetPersonById500", PersonsGetPersonById500>> + readonly "personsGetPersonByDistinctId": (distinctId: string) => Effect.Effect | VoidhashCoreClientError<"ApiPersonNotFoundError", ApiPersonNotFoundError> | VoidhashCoreClientError<"PersonsGetPersonByDistinctId500", PersonsGetPersonByDistinctId500>> + readonly "notificationsSendNotification": (options: SendNotificationBody) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiPushSendNotEnabledError", ApiPushSendNotEnabledError> | VoidhashCoreClientError<"NotificationsSendNotification500", NotificationsSendNotification500>> + readonly "organizationsCreateOrganization": (options: CreateOrganizationBody) => Effect.Effect> + readonly "perksListPerks": () => Effect.Effect | VoidhashCoreClientError<"PerksListPerks500", PerksListPerks500>> + readonly "paywallDeploysCreateDeploy": (options: PaywallDeploysCreateDeployRequest) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiPaywallDeployValidationError", ApiPaywallDeployValidationError> | VoidhashCoreClientError<"PaywallDeploysCreateDeploy500", PaywallDeploysCreateDeploy500>> + readonly "paywallDeploysUploadBlob": (deployId: string, sha256: string) => Effect.Effect | VoidhashCoreClientError<"PaywallDeploysUploadBlob404", PaywallDeploysUploadBlob404> | VoidhashCoreClientError<"ApiPaywallDeployNotPendingError", ApiPaywallDeployNotPendingError> | VoidhashCoreClientError<"PaywallDeploysUploadBlob422", PaywallDeploysUploadBlob422> | VoidhashCoreClientError<"PaywallDeploysUploadBlob500", PaywallDeploysUploadBlob500>> + readonly "paywallDeploysFinalizeDeploy": (deployId: string) => Effect.Effect | VoidhashCoreClientError<"ApiPaywallDeployNotFoundError", ApiPaywallDeployNotFoundError> | VoidhashCoreClientError<"ApiIncompleteDeployError", ApiIncompleteDeployError> | VoidhashCoreClientError<"ApiPaywallDeployValidationError", ApiPaywallDeployValidationError> | VoidhashCoreClientError<"PaywallDeploysFinalizeDeploy500", PaywallDeploysFinalizeDeploy500>> + readonly "paywallLocationsListPaywallLocations": () => Effect.Effect | VoidhashCoreClientError<"PaywallLocationsListPaywallLocations500", PaywallLocationsListPaywallLocations500>> + readonly "schemaGetSchema": () => Effect.Effect | VoidhashCoreClientError<"SchemaGetSchema500", SchemaGetSchema500>> + readonly "schemaGetSchemaVersion": () => Effect.Effect | VoidhashCoreClientError<"SchemaGetSchemaVersion500", SchemaGetSchemaVersion500>> + readonly "projectsCreateProject": (options: CreateProjectBody) => Effect.Effect | VoidhashCoreClientError<"ProjectsCreateProject500", ProjectsCreateProject500>> + readonly "projectsListProjects": (organizationId: string) => Effect.Effect | VoidhashCoreClientError<"ProjectsListProjects500", ProjectsListProjects500>> + readonly "productsListProducts": () => Effect.Effect | VoidhashCoreClientError<"ProductsListProducts500", ProductsListProducts500>> + readonly "productPerksListProductPerksByProductId": (productId: string) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ProductPerksListProductPerksByProductId500", ProductPerksListProductPerksByProductId500>> + readonly "sdkGetPerson": (options: SdkGetPersonParams) => Effect.Effect | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> | VoidhashCoreClientError<"SdkGetPerson500", SdkGetPerson500>> + readonly "sdkIdentifyPerson": (options: { readonly params: SdkIdentifyPersonParams; readonly payload: SdkIdentifyBody }) => Effect.Effect | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> | VoidhashCoreClientError<"ApiSdkPersonAlreadyIdentifiedError", ApiSdkPersonAlreadyIdentifiedError> | VoidhashCoreClientError<"SdkIdentifyPerson500", SdkIdentifyPerson500>> + readonly "sdkSyncPersonAttributes": (options: { readonly params: SdkSyncPersonAttributesParams; readonly payload: SdkSyncPersonAttributesBody }) => Effect.Effect | VoidhashCoreClientError<"ApiSdkPersonNotFoundError", ApiSdkPersonNotFoundError> | VoidhashCoreClientError<"SdkSyncPersonAttributes500", SdkSyncPersonAttributes500>> + readonly "sdkSyncTransaction": (options: { readonly params: SdkSyncTransactionParams; readonly payload: SdkSyncTransactionRequest }) => Effect.Effect | VoidhashCoreClientError<"SdkSyncTransaction500", SdkSyncTransaction500>> + readonly "sdkEvaluateFeatureFlags": (options: { readonly params: SdkEvaluateFeatureFlagsParams; readonly payload: EvaluateFeatureFlagsBody }) => Effect.Effect> + readonly "sdkResolvePaywall": (options: { readonly params: SdkResolvePaywallParams; readonly payload: SdkResolvePaywallBody }) => Effect.Effect | VoidhashCoreClientError<"SdkResolvePaywall500", SdkResolvePaywall500>> + readonly "sdkGetSchema": (options: SdkGetSchemaParams) => Effect.Effect> + readonly "sdkRegisterDevice": (options: { readonly params: SdkRegisterDeviceParams; readonly payload: RegisterDeviceBody }) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiPushDeviceNotFoundError", ApiPushDeviceNotFoundError> | VoidhashCoreClientError<"SdkRegisterDevice500", SdkRegisterDevice500>> + readonly "sdkRefreshDevice": (options: { readonly params: SdkRefreshDeviceParams; readonly payload: RefreshDeviceBody }) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiPushDeviceNotFoundError", ApiPushDeviceNotFoundError> | VoidhashCoreClientError<"SdkRefreshDevice500", SdkRefreshDevice500>> + readonly "sdkUnregisterDevice": (options: { readonly params: SdkUnregisterDeviceParams; readonly payload: UnregisterDeviceBody }) => Effect.Effect | VoidhashCoreClientError<"ApiPushDeviceNotFoundError", ApiPushDeviceNotFoundError> | VoidhashCoreClientError<"SdkUnregisterDevice500", SdkUnregisterDevice500>> + readonly "usersGetUser": () => Effect.Effect> + readonly "paymentProviderConfigurationsListPaymentProviderConfigurations": () => Effect.Effect | VoidhashCoreClientError<"PaymentProviderConfigurationsListPaymentProviderConfigurations500", PaymentProviderConfigurationsListPaymentProviderConfigurations500>> + readonly "paymentProviderProductsListPaymentProviderProducts": () => Effect.Effect | VoidhashCoreClientError<"PaymentProviderProductsListPaymentProviderProducts500", PaymentProviderProductsListPaymentProviderProducts500>> + readonly "webhooksListWebhookEndpoints": () => Effect.Effect | VoidhashCoreClientError<"WebhooksListWebhookEndpoints500", WebhooksListWebhookEndpoints500>> + readonly "webhooksCreateWebhookEndpoint": (options: CreateWebhookEndpointBody) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"WebhooksCreateWebhookEndpoint500", WebhooksCreateWebhookEndpoint500>> + readonly "webhooksGetWebhookEndpoint": (endpointId: string) => Effect.Effect | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> | VoidhashCoreClientError<"WebhooksGetWebhookEndpoint500", WebhooksGetWebhookEndpoint500>> + readonly "webhooksDeleteWebhookEndpoint": (endpointId: string) => Effect.Effect | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> | VoidhashCoreClientError<"WebhooksDeleteWebhookEndpoint500", WebhooksDeleteWebhookEndpoint500>> + readonly "webhooksUpdateWebhookEndpoint": (endpointId: string, options: UpdateWebhookEndpointBody) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> | VoidhashCoreClientError<"WebhooksUpdateWebhookEndpoint500", WebhooksUpdateWebhookEndpoint500>> + readonly "webhooksRotateWebhookSecret": (endpointId: string) => Effect.Effect | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> | VoidhashCoreClientError<"WebhooksRotateWebhookSecret500", WebhooksRotateWebhookSecret500>> + readonly "webhooksTestWebhookEndpoint": (endpointId: string) => Effect.Effect | VoidhashCoreClientError<"ApiWebhookEndpointNotFoundError", ApiWebhookEndpointNotFoundError> | VoidhashCoreClientError<"WebhooksTestWebhookEndpoint500", WebhooksTestWebhookEndpoint500>> + readonly "webhooksListWebhookDeliveries": () => Effect.Effect | VoidhashCoreClientError<"WebhooksListWebhookDeliveries500", WebhooksListWebhookDeliveries500>> + readonly "webhooksGetWebhookDelivery": (deliveryId: string) => Effect.Effect | VoidhashCoreClientError<"ApiWebhookDeliveryNotFoundError", ApiWebhookDeliveryNotFoundError> | VoidhashCoreClientError<"WebhooksGetWebhookDelivery500", WebhooksGetWebhookDelivery500>> + readonly "webhooksRetryWebhookDelivery": (deliveryId: string) => Effect.Effect | VoidhashCoreClientError<"ApiActionForbiddenError", ApiActionForbiddenError> | VoidhashCoreClientError<"ApiWebhookDeliveryNotFoundError", ApiWebhookDeliveryNotFoundError> | VoidhashCoreClientError<"WebhooksRetryWebhookDelivery500", WebhooksRetryWebhookDelivery500>> } export interface VoidhashCoreClientError extends Error { - readonly _tag: Tag; - readonly request: HttpClientRequest.HttpClientRequest; - readonly response: HttpClientResponse.HttpClientResponse; - readonly data: E; - readonly message: string; + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly data: E + readonly message: string } class VoidhashCoreClientErrorImpl extends Data.Error<{ - _tag: string; - data: any; - message: string; - request: HttpClientRequest.HttpClientRequest; - response: HttpClientResponse.HttpClientResponse; + _tag: string + data: any + message: string + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse }> { - name = "VoidhashCoreClientError"; + name = "VoidhashCoreClientError" } export const VoidhashCoreClientError = ( @@ -2180,4 +1599,4 @@ export const VoidhashCoreClientError = ( message: JSON.stringify(data), response, request: response.request, - }) as any; + }) as any diff --git a/packages/generated-clients/src/event-capture/generated.ts b/packages/generated-clients/src/event-capture/generated.ts index 37ba34cf8..d3cb9f621 100644 --- a/packages/generated-clients/src/event-capture/generated.ts +++ b/packages/generated-clients/src/event-capture/generated.ts @@ -1,120 +1,202 @@ -import type * as HttpClient from "effect/unstable/http/HttpClient"; -import * as HttpClientError from "effect/unstable/http/HttpClientError"; -import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; -import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; -import * as Data from "effect/Data"; -import * as Effect from "effect/Effect"; +import type * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" export interface EventCaptureCaptureRequest { - readonly uuid: string; - readonly event: string; - readonly context: Record; - readonly properties: Record; - readonly distinct_id: string; - readonly session_id?: string | null | undefined; - readonly timestamp?: string | null | undefined; - readonly sent_at: string; - readonly token: string; + readonly "uuid": string; + readonly "event": string; + readonly "context": Record; + readonly "properties": Record; + readonly "distinct_id": string; + readonly "session_id"?: string | null | undefined; + readonly "timestamp"?: string | null | undefined; + readonly "sent_at": string; + readonly "token": string +} + +export type CaptureRejectedRecordReason = "malformed_envelope" | "unsupported_schema_version" | "payload_too_large" | "invalid_project_scope" | "duplicate" | "invalid_context" | "reserved_event" | "policy_rejected" + +export interface CaptureRejectedRecord { + readonly "recordId": string; + readonly "reason": CaptureRejectedRecordReason } export interface CaptureAcceptedResponse { - readonly accepted: number; - readonly rejected: number; + readonly "accepted": ReadonlyArray; + readonly "rejected": ReadonlyArray } -export type CaptureInvalidRequestErrorTag = "CaptureInvalidRequestError"; +export type CaptureInvalidRequestErrorTag = "CaptureInvalidRequestError" -export type CaptureInvalidRequestErrorCode = "invalid_request"; +export type CaptureInvalidRequestErrorCode = "invalid_request" export interface CaptureInvalidRequestError { - readonly _tag: CaptureInvalidRequestErrorTag; - readonly error: string; - readonly code: CaptureInvalidRequestErrorCode; + readonly "_tag": CaptureInvalidRequestErrorTag; + readonly "error": string; + readonly "code": CaptureInvalidRequestErrorCode } -export type EffectHttpApiSchemaErrorTag = "HttpApiSchemaError"; - -export interface EffectHttpApiSchemaError { - readonly _tag: EffectHttpApiSchemaErrorTag; - readonly message: string; -} +export type CaptureUnauthorizedErrorTag = "CaptureUnauthorizedError" -export type EventCaptureCapture400 = CaptureInvalidRequestError | EffectHttpApiSchemaError; - -export type CaptureUnauthorizedErrorTag = "CaptureUnauthorizedError"; - -export type CaptureUnauthorizedErrorCode = "unauthorized"; +export type CaptureUnauthorizedErrorCode = "unauthorized" export interface CaptureUnauthorizedError { - readonly _tag: CaptureUnauthorizedErrorTag; - readonly error: string; - readonly code: CaptureUnauthorizedErrorCode; + readonly "_tag": CaptureUnauthorizedErrorTag; + readonly "error": string; + readonly "code": CaptureUnauthorizedErrorCode } -export type CapturePayloadTooLargeErrorTag = "CapturePayloadTooLargeError"; +export type CapturePayloadTooLargeErrorTag = "CapturePayloadTooLargeError" -export type CapturePayloadTooLargeErrorCode = "payload_too_large"; +export type CapturePayloadTooLargeErrorCode = "payload_too_large" export interface CapturePayloadTooLargeError { - readonly _tag: CapturePayloadTooLargeErrorTag; - readonly error: string; - readonly code: CapturePayloadTooLargeErrorCode; + readonly "_tag": CapturePayloadTooLargeErrorTag; + readonly "error": string; + readonly "code": CapturePayloadTooLargeErrorCode } -export type CaptureRateLimitedErrorTag = "CaptureRateLimitedError"; +export type CaptureRateLimitedErrorTag = "CaptureRateLimitedError" -export type CaptureRateLimitedErrorCode = "rate_limited"; +export type CaptureRateLimitedErrorCode = "rate_limited" export interface CaptureRateLimitedError { - readonly _tag: CaptureRateLimitedErrorTag; - readonly error: string; - readonly code: CaptureRateLimitedErrorCode; - readonly retry_after_ms?: number | null | undefined; + readonly "_tag": CaptureRateLimitedErrorTag; + readonly "error": string; + readonly "code": CaptureRateLimitedErrorCode; + readonly "retry_after_ms"?: number | null | undefined } -export type CaptureInternalServerErrorTag = "CaptureInternalServerError"; +export type CaptureInternalServerErrorTag = "CaptureInternalServerError" -export type CaptureInternalServerErrorCode = "internal_error"; +export type CaptureInternalServerErrorCode = "internal_error" export interface CaptureInternalServerError { - readonly _tag: CaptureInternalServerErrorTag; - readonly error: string; - readonly code: CaptureInternalServerErrorCode; + readonly "_tag": CaptureInternalServerErrorTag; + readonly "error": string; + readonly "code": CaptureInternalServerErrorCode } -export type CaptureDependencyUnavailableErrorTag = "CaptureDependencyUnavailableError"; +export type CaptureDependencyUnavailableErrorTag = "CaptureDependencyUnavailableError" -export type CaptureDependencyUnavailableErrorCode = "dependency_unavailable"; +export type CaptureDependencyUnavailableErrorCode = "dependency_unavailable" export interface CaptureDependencyUnavailableError { - readonly _tag: CaptureDependencyUnavailableErrorTag; - readonly error: string; - readonly code: CaptureDependencyUnavailableErrorCode; + readonly "_tag": CaptureDependencyUnavailableErrorTag; + readonly "error": string; + readonly "code": CaptureDependencyUnavailableErrorCode } export interface EventCaptureBatchRequest { - readonly events: ReadonlyArray<{ - readonly uuid: string; - readonly event: string; - readonly context: Record; - readonly properties: Record; - readonly distinct_id: string; - readonly session_id?: string | null | undefined; - readonly timestamp?: string | null | undefined; - }>; - readonly sent_at: string; - readonly token: string; + readonly "events": ReadonlyArray<{ + readonly "uuid": string; + readonly "event": string; + readonly "context": Record; + readonly "properties": Record; + readonly "distinct_id": string; + readonly "session_id"?: string | null | undefined; + readonly "timestamp"?: string | null | undefined +}>; + readonly "sent_at": string; + readonly "token": string +} + +export type EventCaptureProtectedRequestDeletionState = "active" | "deletion-requested" | "deleted" + +export type EventCaptureProtectedRequestPurpose = "advertising-identifier" | "diagnostic-authorization" | "email" | "install-referrer" | "link-capture" | "partner-context" | "phone" | "purchase-receipt" | "push-token" + +export type EventCaptureProtectedRequestRetentionClass = "ephemeral" | "installation" | "legal" | "transaction" + +export interface EventCaptureProtectedRequest { + readonly "blobId": string; + readonly "ciphertext": string; + readonly "consentRevision": number; + readonly "deletionState": EventCaptureProtectedRequestDeletionState; + readonly "encryptionKeyVersion": number; + readonly "installationId": string; + readonly "purpose": EventCaptureProtectedRequestPurpose; + readonly "retentionClass": EventCaptureProtectedRequestRetentionClass; + readonly "token": string +} + +export type ProtectedEvidenceAcceptedResponseAccepted = true + +export interface ProtectedEvidenceAcceptedResponse { + readonly "accepted": ProtectedEvidenceAcceptedResponseAccepted; + readonly "blobId": string +} + +export type ProtectedEvidenceConflictErrorTag = "ProtectedEvidenceConflictError" + +export type ProtectedEvidenceConflictErrorCode = "protected_evidence_conflict" + +export interface ProtectedEvidenceConflictError { + readonly "_tag": ProtectedEvidenceConflictErrorTag; + readonly "error": string; + readonly "code": ProtectedEvidenceConflictErrorCode } -export type EventCaptureBatch400 = CaptureInvalidRequestError | EffectHttpApiSchemaError; +export interface EventCaptureDeleteMeasurementDataRequest { + readonly "installationId": string; + readonly "personId"?: string | null | undefined; + readonly "requestId": string; + readonly "requestedAt": string; + readonly "token": string +} + +export type MeasurementDeletionAcceptedResponseAccepted = true + +export type MeasurementDeletionAcceptedResponseStatus = "completed" + +export interface MeasurementDeletionAcceptedResponse { + readonly "accepted": MeasurementDeletionAcceptedResponseAccepted; + readonly "deletedProtectedEvidence": number; + readonly "requestId": string; + readonly "status": MeasurementDeletionAcceptedResponseStatus +} + +export interface EventCaptureGetMeasurementConfigurationParams { + readonly "x-publishable-key": string +} + +export type SignedMeasurementConfigurationResponsePayloadSchemaVersion = 1 + +export interface SignedMeasurementConfigurationResponse { + readonly "expiresAt": string; + readonly "keyId": string; + readonly "payload": { + readonly "collectors": { + readonly "appleAttributionEnabled": boolean; + readonly "linkAllowedDomains": ReadonlyArray +}; + readonly "conversionRules": ReadonlyArray<{ + readonly "coarseValue"?: "low" | "medium" | "high" | null | undefined; + readonly "eventName": string; + readonly "fineValue": number; + readonly "lockWindow"?: boolean | null | undefined; + readonly "minimumCount": number; + readonly "window": number +}>; + readonly "schemaVersion": SignedMeasurementConfigurationResponsePayloadSchemaVersion; + readonly "storage": { + readonly "maxOutboxBytes": number; + readonly "maxOutboxRecords": number; + readonly "maxProtectedBytes": number +} +}; + readonly "projectId": string; + readonly "signature": string; + readonly "version": number +} export const make = ( httpClient: HttpClient.HttpClient, options: { - readonly transformClient?: - | ((client: HttpClient.HttpClient) => Effect.Effect) - | undefined; - } = {}, + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} ): VoidhashEventCaptureClient => { const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( @@ -126,26 +208,30 @@ export const make = ( request: response.request, response, description: - typeof description === "string" ? description : JSON.stringify(description), + typeof description === "string" + ? description + : JSON.stringify(description), }), }), ), - ); + ) const withResponse: ( f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, - ) => (request: HttpClientRequest.HttpClientRequest) => Effect.Effect = - options.transformClient - ? (f) => (request) => - Effect.flatMap( - Effect.flatMap(options.transformClient!(httpClient), (client) => - client.execute(request), - ), - f, - ) - : (f) => (request) => Effect.flatMap(httpClient.execute(request), f); + ) => ( + request: HttpClientRequest.HttpClientRequest, + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => + client.execute(request), + ), + f, + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) const decodeSuccess = (response: HttpClientResponse.HttpClientResponse) => - response.json as Effect.Effect; - const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) => Effect.void; + response.json as Effect.Effect + const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) => + Effect.void const decodeError = (tag: Tag) => ( @@ -154,103 +240,78 @@ export const make = ( never, VoidhashEventCaptureClientError | HttpClientError.HttpClientError > => - Effect.flatMap(response.json as Effect.Effect, (cause) => - Effect.fail(VoidhashEventCaptureClientError(tag, cause, response)), - ); - const onRequest = (successCodes: ReadonlyArray, errorCodes?: Record) => { - const cases: any = { orElse: unexpectedStatus }; + Effect.flatMap( + response.json as Effect.Effect, + (cause) => Effect.fail(VoidhashEventCaptureClientError(tag, cause, response)), + ) + const onRequest = ( + successCodes: ReadonlyArray, + errorCodes?: Record, + ) => { + const cases: any = { orElse: unexpectedStatus } for (const code of successCodes) { - cases[code] = decodeSuccess; + cases[code] = decodeSuccess } if (errorCodes) { for (const [code, tag] of Object.entries(errorCodes)) { - cases[code] = decodeError(tag); + cases[code] = decodeError(tag) } } if (successCodes.length === 0) { - cases["2xx"] = decodeVoid; + cases["2xx"] = decodeVoid } - return withResponse(HttpClientResponse.matchStatus(cases) as any); - }; + return withResponse(HttpClientResponse.matchStatus(cases) as any) + } return { httpClient, - eventCaptureCapture: (options) => - HttpClientRequest.post(`/i/v1/capture`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "EventCaptureCapture400", - "401": "CaptureUnauthorizedError", - "413": "CapturePayloadTooLargeError", - "429": "CaptureRateLimitedError", - "500": "CaptureInternalServerError", - "503": "CaptureDependencyUnavailableError", - }), - ), - eventCaptureBatch: (options) => - HttpClientRequest.post(`/i/v1/batch`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - onRequest(["2xx"], { - "400": "EventCaptureBatch400", - "401": "CaptureUnauthorizedError", - "413": "CapturePayloadTooLargeError", - "429": "CaptureRateLimitedError", - "500": "CaptureInternalServerError", - "503": "CaptureDependencyUnavailableError", - }), - ), - }; -}; + "eventCaptureCapture": (options) => HttpClientRequest.post(`/i/v1/capture`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"CaptureInvalidRequestError","401":"CaptureUnauthorizedError","413":"CapturePayloadTooLargeError","429":"CaptureRateLimitedError","500":"CaptureInternalServerError","503":"CaptureDependencyUnavailableError"}) + ), + "eventCaptureBatch": (options) => HttpClientRequest.post(`/i/v1/batch`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"CaptureInvalidRequestError","401":"CaptureUnauthorizedError","413":"CapturePayloadTooLargeError","429":"CaptureRateLimitedError","500":"CaptureInternalServerError","503":"CaptureDependencyUnavailableError"}) + ), + "eventCaptureProtected": (options) => HttpClientRequest.post(`/i/v1/measurement/protected`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"CaptureInvalidRequestError","401":"CaptureUnauthorizedError","409":"ProtectedEvidenceConflictError","413":"CapturePayloadTooLargeError","500":"CaptureInternalServerError","503":"CaptureDependencyUnavailableError"}) + ), + "eventCaptureDeleteMeasurementData": (options) => HttpClientRequest.post(`/i/v1/measurement/delete`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"CaptureInvalidRequestError","401":"CaptureUnauthorizedError","429":"CaptureRateLimitedError","500":"CaptureInternalServerError","503":"CaptureDependencyUnavailableError"}) + ), + "eventCaptureGetMeasurementConfiguration": (options) => HttpClientRequest.get(`/i/v1/measurement/config`).pipe( + HttpClientRequest.setHeaders({ "x-publishable-key": options?.["x-publishable-key"] ?? undefined }), + onRequest(["2xx"], {"401":"CaptureUnauthorizedError","500":"CaptureInternalServerError","503":"CaptureDependencyUnavailableError"}) + ) + } +} export interface VoidhashEventCaptureClient { - readonly httpClient: HttpClient.HttpClient; - readonly eventCaptureCapture: ( - options: EventCaptureCaptureRequest, - ) => Effect.Effect< - CaptureAcceptedResponse, - | HttpClientError.HttpClientError - | VoidhashEventCaptureClientError<"EventCaptureCapture400", EventCaptureCapture400> - | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> - | VoidhashEventCaptureClientError<"CapturePayloadTooLargeError", CapturePayloadTooLargeError> - | VoidhashEventCaptureClientError<"CaptureRateLimitedError", CaptureRateLimitedError> - | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> - | VoidhashEventCaptureClientError< - "CaptureDependencyUnavailableError", - CaptureDependencyUnavailableError - > - >; - readonly eventCaptureBatch: ( - options: EventCaptureBatchRequest, - ) => Effect.Effect< - CaptureAcceptedResponse, - | HttpClientError.HttpClientError - | VoidhashEventCaptureClientError<"EventCaptureBatch400", EventCaptureBatch400> - | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> - | VoidhashEventCaptureClientError<"CapturePayloadTooLargeError", CapturePayloadTooLargeError> - | VoidhashEventCaptureClientError<"CaptureRateLimitedError", CaptureRateLimitedError> - | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> - | VoidhashEventCaptureClientError< - "CaptureDependencyUnavailableError", - CaptureDependencyUnavailableError - > - >; + readonly httpClient: HttpClient.HttpClient + readonly "eventCaptureCapture": (options: EventCaptureCaptureRequest) => Effect.Effect | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> | VoidhashEventCaptureClientError<"CapturePayloadTooLargeError", CapturePayloadTooLargeError> | VoidhashEventCaptureClientError<"CaptureRateLimitedError", CaptureRateLimitedError> | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> | VoidhashEventCaptureClientError<"CaptureDependencyUnavailableError", CaptureDependencyUnavailableError>> + readonly "eventCaptureBatch": (options: EventCaptureBatchRequest) => Effect.Effect | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> | VoidhashEventCaptureClientError<"CapturePayloadTooLargeError", CapturePayloadTooLargeError> | VoidhashEventCaptureClientError<"CaptureRateLimitedError", CaptureRateLimitedError> | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> | VoidhashEventCaptureClientError<"CaptureDependencyUnavailableError", CaptureDependencyUnavailableError>> + readonly "eventCaptureProtected": (options: EventCaptureProtectedRequest) => Effect.Effect | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> | VoidhashEventCaptureClientError<"ProtectedEvidenceConflictError", ProtectedEvidenceConflictError> | VoidhashEventCaptureClientError<"CapturePayloadTooLargeError", CapturePayloadTooLargeError> | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> | VoidhashEventCaptureClientError<"CaptureDependencyUnavailableError", CaptureDependencyUnavailableError>> + readonly "eventCaptureDeleteMeasurementData": (options: EventCaptureDeleteMeasurementDataRequest) => Effect.Effect | VoidhashEventCaptureClientError<"CaptureUnauthorizedError", CaptureUnauthorizedError> | VoidhashEventCaptureClientError<"CaptureRateLimitedError", CaptureRateLimitedError> | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> | VoidhashEventCaptureClientError<"CaptureDependencyUnavailableError", CaptureDependencyUnavailableError>> + readonly "eventCaptureGetMeasurementConfiguration": (options: EventCaptureGetMeasurementConfigurationParams) => Effect.Effect | VoidhashEventCaptureClientError<"CaptureInternalServerError", CaptureInternalServerError> | VoidhashEventCaptureClientError<"CaptureDependencyUnavailableError", CaptureDependencyUnavailableError>> } export interface VoidhashEventCaptureClientError extends Error { - readonly _tag: Tag; - readonly request: HttpClientRequest.HttpClientRequest; - readonly response: HttpClientResponse.HttpClientResponse; - readonly data: E; - readonly message: string; + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly data: E + readonly message: string } class VoidhashEventCaptureClientErrorImpl extends Data.Error<{ - _tag: string; - data: any; - message: string; - request: HttpClientRequest.HttpClientRequest; - response: HttpClientResponse.HttpClientResponse; + _tag: string + data: any + message: string + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse }> { - name = "VoidhashEventCaptureClientError"; + name = "VoidhashEventCaptureClientError" } export const VoidhashEventCaptureClientError = ( @@ -264,4 +325,4 @@ export const VoidhashEventCaptureClientError = ( message: JSON.stringify(data), response, request: response.request, - }) as any; + }) as any diff --git a/packages/generated-clients/src/index.ts b/packages/generated-clients/src/index.ts index 2d643a3e2..f28cc9efc 100644 --- a/packages/generated-clients/src/index.ts +++ b/packages/generated-clients/src/index.ts @@ -1,2 +1,3 @@ export * from "./core"; export * as EventCapture from "./event-capture"; +export * as Links from "./links"; diff --git a/packages/generated-clients/src/links/generated.ts b/packages/generated-clients/src/links/generated.ts new file mode 100644 index 000000000..15f00e8a3 --- /dev/null +++ b/packages/generated-clients/src/links/generated.ts @@ -0,0 +1,304 @@ +import type * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" + +export type LinksCreateLinkRequestBrandedDomainEnum = string + +export type LinksCreateLinkRequestCampaignEnumAdEnum = string + +export type LinksCreateLinkRequestCampaignEnumAdSetEnum = string + +export type LinksCreateLinkRequestCampaignEnumCampaignEnum = string + +export type LinksCreateLinkRequestCampaignEnumChannelEnum = string + +export type LinksCreateLinkRequestCampaignEnumMediaSourceEnum = string + +export type LinksCreateLinkRequestDestinationAndroidStoreUrlEnum = string + +export type LinksCreateLinkRequestDestinationAppleAppIdEnum = string + +export type LinksCreateLinkRequestDestinationBaseDeepLinkEnum = string + +export type LinksCreateLinkRequestDestinationDeepLinkValue = string + +export type LinksCreateLinkRequestDestinationIosStoreUrlEnum = string + +export type LinksCreateLinkRequestDestinationWebFallbackUrlEnum = string + +export type LinksCreateLinkRequestIdempotencyKeyEnum = string + +export type LinksCreateLinkRequestReferrerCustomerIdEnum = string + +export type LinksCreateLinkRequestReferrerImageUrlEnum = string + +export type LinksCreateLinkRequestReferrerNameEnum = string + +export type LinksCreateLinkRequestReferrerUidEnum = string + +export type LinksCreateLinkRequestTemplateIdEnum = string + +export type LinksCreateLinkRequestToken = string + +export interface LinksCreateLinkRequest { + readonly "brandedDomain"?: string | null | undefined; + readonly "campaign"?: { + readonly "ad"?: string | null | undefined; + readonly "adSet"?: string | null | undefined; + readonly "campaign"?: string | null | undefined; + readonly "channel"?: string | null | undefined; + readonly "mediaSource"?: string | null | undefined +} | null | undefined; + readonly "customParameters"?: Record | null | undefined; + readonly "destination": { + readonly "androidStoreUrl"?: string | null | undefined; + readonly "appleAppId"?: string | null | undefined; + readonly "baseDeepLink"?: string | null | undefined; + readonly "deepLinkValue": LinksCreateLinkRequestDestinationDeepLinkValue; + readonly "iosStoreUrl"?: string | null | undefined; + readonly "subvalues"?: Record | null | undefined; + readonly "webFallbackUrl"?: string | null | undefined +}; + readonly "expiresAt"?: string | null | undefined; + readonly "idempotencyKey"?: string | null | undefined; + readonly "referrerCustomerId"?: string | null | undefined; + readonly "referrerImageUrl"?: string | null | undefined; + readonly "referrerName"?: string | null | undefined; + readonly "referrerUid"?: string | null | undefined; + readonly "templateId"?: string | null | undefined; + readonly "token": LinksCreateLinkRequestToken +} + +export type CreateLinkResponseLinkId = string + +export type CreateLinkResponseUrl = string + +export interface CreateLinkResponse { + readonly "expiresAt": string; + readonly "linkId": CreateLinkResponseLinkId; + readonly "url": CreateLinkResponseUrl +} + +export type LinkInvalidRequestErrorTag = "LinkInvalidRequestError" + +export type LinkInvalidRequestErrorCode = "invalid_link_request" + +export interface LinkInvalidRequestError { + readonly "_tag": LinkInvalidRequestErrorTag; + readonly "code": LinkInvalidRequestErrorCode; + readonly "error": string +} + +export type LinkUnauthorizedErrorTag = "LinkUnauthorizedError" + +export type LinkUnauthorizedErrorCode = "unauthorized" + +export interface LinkUnauthorizedError { + readonly "_tag": LinkUnauthorizedErrorTag; + readonly "code": LinkUnauthorizedErrorCode; + readonly "error": string +} + +export type LinkRateLimitedErrorTag = "LinkRateLimitedError" + +export type LinkRateLimitedErrorCode = "rate_limited" + +export interface LinkRateLimitedError { + readonly "_tag": LinkRateLimitedErrorTag; + readonly "code": LinkRateLimitedErrorCode; + readonly "error": string +} + +export type LinkServiceUnavailableErrorTag = "LinkServiceUnavailableError" + +export type LinkServiceUnavailableErrorCode = "service_unavailable" + +export interface LinkServiceUnavailableError { + readonly "_tag": LinkServiceUnavailableErrorTag; + readonly "code": LinkServiceUnavailableErrorCode; + readonly "error": string +} + +export type LinksResolveDeferredLinkRequestDeferredToken = string + +export type LinksResolveDeferredLinkRequestInstallationId = string + +export type LinksResolveDeferredLinkRequestPlatform = "ios" | "android" + +export type LinksResolveDeferredLinkRequestToken = string + +export interface LinksResolveDeferredLinkRequest { + readonly "deferredToken": LinksResolveDeferredLinkRequestDeferredToken; + readonly "installationId": LinksResolveDeferredLinkRequestInstallationId; + readonly "platform": LinksResolveDeferredLinkRequestPlatform; + readonly "token": LinksResolveDeferredLinkRequestToken +} + +export type LinksResolveDeferredLink200CampaignEnumAdEnum = string + +export type LinksResolveDeferredLink200CampaignEnumAdSetEnum = string + +export type LinksResolveDeferredLink200CampaignEnumCampaignEnum = string + +export type LinksResolveDeferredLink200CampaignEnumChannelEnum = string + +export type LinksResolveDeferredLink200CampaignEnumMediaSourceEnum = string + +export type LinksResolveDeferredLink200ClickIdEnum = string + +export type LinksResolveDeferredLink200DeferredEnum = true + +export type LinksResolveDeferredLink200LinkIdEnum = string + +export type LinksResolveDeferredLink200ReasonEnum = "expired" | "not-found" | "replayed" | "invalid" + +export type LinksResolveDeferredLink200RouteEnumValue = string + +export type LinksResolveDeferredLink200SignatureEnum = string + +export type LinksResolveDeferredLink200Status = "found" | "notFound" + +export interface LinksResolveDeferredLink200 { + readonly "campaign"?: { + readonly "ad"?: string | null | undefined; + readonly "adSet"?: string | null | undefined; + readonly "campaign"?: string | null | undefined; + readonly "channel"?: string | null | undefined; + readonly "mediaSource"?: string | null | undefined +} | null | undefined; + readonly "clickId"?: string | null | undefined; + readonly "clickedAt"?: string | null | undefined; + readonly "deferred"?: LinksResolveDeferredLink200DeferredEnum | null | undefined; + readonly "expiresAt"?: string | null | undefined; + readonly "linkId"?: string | null | undefined; + readonly "reason"?: LinksResolveDeferredLink200ReasonEnum | null | undefined; + readonly "route"?: { + readonly "subvalues": Record; + readonly "value": string +} | null | undefined; + readonly "signature"?: string | null | undefined; + readonly "status": LinksResolveDeferredLink200Status +} + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): VoidhashLinksClient => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: + typeof description === "string" + ? description + : JSON.stringify(description), + }), + }), + ), + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, + ) => ( + request: HttpClientRequest.HttpClientRequest, + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => + client.execute(request), + ), + f, + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = (response: HttpClientResponse.HttpClientResponse) => + response.json as Effect.Effect + const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) => + Effect.void + const decodeError = + (tag: Tag) => + ( + response: HttpClientResponse.HttpClientResponse, + ): Effect.Effect< + never, + VoidhashLinksClientError | HttpClientError.HttpClientError + > => + Effect.flatMap( + response.json as Effect.Effect, + (cause) => Effect.fail(VoidhashLinksClientError(tag, cause, response)), + ) + const onRequest = ( + successCodes: ReadonlyArray, + errorCodes?: Record, + ) => { + const cases: any = { orElse: unexpectedStatus } + for (const code of successCodes) { + cases[code] = decodeSuccess + } + if (errorCodes) { + for (const [code, tag] of Object.entries(errorCodes)) { + cases[code] = decodeError(tag) + } + } + if (successCodes.length === 0) { + cases["2xx"] = decodeVoid + } + return withResponse(HttpClientResponse.matchStatus(cases) as any) + } + return { + httpClient, + "linksCreateLink": (options) => HttpClientRequest.post(`/l/v1/links`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"LinkInvalidRequestError","401":"LinkUnauthorizedError","429":"LinkRateLimitedError","503":"LinkServiceUnavailableError"}) + ), + "linksResolveDeferredLink": (options) => HttpClientRequest.post(`/l/v1/deferred/resolve`).pipe( + HttpClientRequest.bodyJsonUnsafe(options), + onRequest(["2xx"], {"400":"LinkInvalidRequestError","401":"LinkUnauthorizedError","429":"LinkRateLimitedError","503":"LinkServiceUnavailableError"}) + ) + } +} + +export interface VoidhashLinksClient { + readonly httpClient: HttpClient.HttpClient + readonly "linksCreateLink": (options: LinksCreateLinkRequest) => Effect.Effect | VoidhashLinksClientError<"LinkUnauthorizedError", LinkUnauthorizedError> | VoidhashLinksClientError<"LinkRateLimitedError", LinkRateLimitedError> | VoidhashLinksClientError<"LinkServiceUnavailableError", LinkServiceUnavailableError>> + readonly "linksResolveDeferredLink": (options: LinksResolveDeferredLinkRequest) => Effect.Effect | VoidhashLinksClientError<"LinkUnauthorizedError", LinkUnauthorizedError> | VoidhashLinksClientError<"LinkRateLimitedError", LinkRateLimitedError> | VoidhashLinksClientError<"LinkServiceUnavailableError", LinkServiceUnavailableError>> +} + +export interface VoidhashLinksClientError extends Error { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly data: E + readonly message: string +} + +class VoidhashLinksClientErrorImpl extends Data.Error<{ + _tag: string + data: any + message: string + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> { + name = "VoidhashLinksClientError" +} + +export const VoidhashLinksClientError = ( + tag: Tag, + data: E, + response: HttpClientResponse.HttpClientResponse, +): VoidhashLinksClientError => + new VoidhashLinksClientErrorImpl({ + _tag: tag, + data, + message: JSON.stringify(data), + response, + request: response.request, + }) as any diff --git a/packages/generated-clients/src/links/index.ts b/packages/generated-clients/src/links/index.ts new file mode 100644 index 000000000..e84c86c64 --- /dev/null +++ b/packages/generated-clients/src/links/index.ts @@ -0,0 +1 @@ +export * from "./generated"; diff --git a/scripts/generate-openapi-clients.mjs b/scripts/generate-openapi-clients.mjs index 9cd708ef2..b3d36cdba 100644 --- a/scripts/generate-openapi-clients.mjs +++ b/scripts/generate-openapi-clients.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { mkdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import path from "node:path"; import process from "node:process"; @@ -13,14 +13,15 @@ const generatedClientsRoot = path.join(repoRoot, "packages/generated-clients"); const nodeGeneratedRoot = path.join(repoRoot, "libraries/node/src/generated"); const openapiRoot = path.join(generatedClientsRoot, "openapi"); +const contractMode = process.argv.includes("--contracts"); const rawHost = process.argv .slice(2) - .find((arg) => arg !== "--") + .find((arg) => arg !== "--" && arg !== "--contracts") ?.trim(); -if (!rawHost) { +if (!rawHost && !contractMode) { console.error( - "Usage: node ./scripts/generate-openapi-clients.mjs \nExample: node ./scripts/generate-openapi-clients.mjs localhost:8787", + "Usage: node ./scripts/generate-openapi-clients.mjs |--contracts\nExample: node ./scripts/generate-openapi-clients.mjs localhost:8787", ); process.exit(1); } @@ -36,11 +37,12 @@ const normalizeHost = (host) => { return new URL(`${protocol}${host}`).toString(); }; -const baseUrl = normalizeHost(rawHost); -const coreSpecUrl = new URL("/api/docs/openapi.json", baseUrl).toString(); -const eventCaptureSpecUrl = new URL("/i/docs/openapi.json", baseUrl).toString(); +const baseUrl = rawHost ? normalizeHost(rawHost) : undefined; +const coreSpecUrl = baseUrl && new URL("/api/docs/openapi.json", baseUrl).toString(); +const eventCaptureSpecUrl = baseUrl && new URL("/i/docs/openapi.json", baseUrl).toString(); const coreSpecPath = path.join(openapiRoot, "core.json"); const eventCaptureSpecPath = path.join(openapiRoot, "event-capture.json"); +const linksSpecPath = path.join(openapiRoot, "links.json"); const fetchJson = async (url) => { const response = await fetch(url); @@ -84,19 +86,25 @@ const run = (command, args) => { return result.stdout; }; +const normalizeGeneratedText = (value) => `${value.trimEnd().replace(/[ \t]+$/gm, "")}\n`; + const main = async () => { mkdirSync(openapiRoot, { recursive: true }); - const [coreSpecText, eventCaptureSpecText] = await Promise.all([ - fetchJson(coreSpecUrl), - fetchJson(eventCaptureSpecUrl), - ]); + if (contractMode) { + run("pnpm", ["exec", "tsx", "./scripts/generate-openapi-from-contracts.ts"]); + } + + const [coreSpecText, eventCaptureSpecText, linksSpecText] = contractMode + ? [readFileSync(coreSpecPath, "utf8"), readFileSync(eventCaptureSpecPath, "utf8"), readFileSync(linksSpecPath, "utf8")] + : [...await Promise.all([fetchJson(coreSpecUrl), fetchJson(eventCaptureSpecUrl)]), readFileSync(linksSpecPath, "utf8")]; assertCoreSpec(JSON.parse(coreSpecText)); assertEventCaptureSpec(JSON.parse(eventCaptureSpecText)); - writeFileSync(coreSpecPath, `${coreSpecText}\n`, "utf8"); - writeFileSync(eventCaptureSpecPath, `${eventCaptureSpecText}\n`, "utf8"); + writeFileSync(coreSpecPath, normalizeGeneratedText(coreSpecText), "utf8"); + writeFileSync(eventCaptureSpecPath, normalizeGeneratedText(eventCaptureSpecText), "utf8"); + writeFileSync(linksSpecPath, normalizeGeneratedText(linksSpecText), "utf8"); const coreOutput = run("pnpm", [ "dlx", @@ -106,7 +114,25 @@ const main = async () => { "--name", "VoidhashCoreClient", ]); - writeFileSync(path.join(generatedClientsRoot, "src/core/generated.ts"), coreOutput, "utf8"); + writeFileSync( + path.join(generatedClientsRoot, "src/core/generated.ts"), + normalizeGeneratedText(coreOutput), + "utf8", + ); + + const linksOutput = run("pnpm", [ + "dlx", + "@tim-smart/openapi-gen@1.0.3", + "--spec", + linksSpecPath, + "--name", + "VoidhashLinksClient", + ]); + writeFileSync( + path.join(generatedClientsRoot, "src/links/generated.ts"), + normalizeGeneratedText(linksOutput), + "utf8", + ); const eventCaptureOutput = run("pnpm", [ "dlx", @@ -118,7 +144,7 @@ const main = async () => { ]); writeFileSync( path.join(generatedClientsRoot, "src/event-capture/generated.ts"), - eventCaptureOutput, + normalizeGeneratedText(eventCaptureOutput), "utf8", ); diff --git a/scripts/generate-openapi-from-contracts.ts b/scripts/generate-openapi-from-contracts.ts new file mode 100644 index 000000000..ea09439e5 --- /dev/null +++ b/scripts/generate-openapi-from-contracts.ts @@ -0,0 +1,17 @@ +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { VoidhashV1Api } from "../packages/api-contracts/src/Api.ts"; +import { EventCaptureApi } from "../packages/api-contracts/src/EventCapture.ts"; +import { LinksApi } from "../packages/api-contracts/src/Links.ts"; +import * as OpenApi from "effect/unstable/httpapi/OpenApi"; + +const root = resolve(import.meta.dirname, ".."); + +const writeSpec = (path: string, api: Parameters[0]) => { + writeFileSync(resolve(root, path), `${JSON.stringify(OpenApi.fromApi(api), null, 2)}\n`, "utf8"); +}; + +writeSpec("packages/generated-clients/openapi/core.json", VoidhashV1Api); +writeSpec("packages/generated-clients/openapi/event-capture.json", EventCaptureApi); +writeSpec("packages/generated-clients/openapi/links.json", LinksApi); diff --git a/selfhost/.env.example b/selfhost/.env.example index 9aebd8c96..ace5348d6 100644 --- a/selfhost/.env.example +++ b/selfhost/.env.example @@ -44,6 +44,9 @@ GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT_EMAIL= VOIDHASH_LICENSE_KEY= VOIDHASH_LICENSE_PUBLIC_KEY= ENCRYPTION_KEY= +MEASUREMENT_CONFIG_KEY_ID=replace-with-a-stable-key-id +MEASUREMENT_CONFIG_PRIVATE_KEY_PKCS8=replace-with-ed25519-pkcs8-base64 +MEASUREMENT_CONFIG_VERSION=1 APNS_DELIVERY_ENABLED=false EXCHANGE_RATE_API_KEY= S3_ACCESS_KEY_ID=voidhash diff --git a/selfhost/README.md b/selfhost/README.md index 0ed1eec3e..6c3f82059 100644 --- a/selfhost/README.md +++ b/selfhost/README.md @@ -85,6 +85,16 @@ email claim, and service-account identity before reading the Pub/Sub envelope; missing authentication is rejected and missing server configuration fails closed with a retryable response. +### Measurement configuration signing + +SDK collector configuration is signed with the Ed25519 PKCS#8 key configured by +`MEASUREMENT_CONFIG_PRIVATE_KEY_PKCS8`. Set a stable public identifier in +`MEASUREMENT_CONFIG_KEY_ID` and a positive monotonic +`MEASUREMENT_CONFIG_VERSION`. To rotate keys, deploy clients trusting both the +old and new public keys, switch the server key and key ID, increment the version, +then remove the old client trust only after the supported client window has +elapsed. Never reuse or decrease a version, including after a rollback. + ## Smoke test From a workspace checkout with dependencies installed: diff --git a/selfhost/entry/src/backend/Analytics.ts b/selfhost/entry/src/backend/Analytics.ts index 84aab08c4..315cc9969 100644 --- a/selfhost/entry/src/backend/Analytics.ts +++ b/selfhost/entry/src/backend/Analytics.ts @@ -14,6 +14,13 @@ import { } from "@voidhash/core/services/analyticsIngest/CaptureIngress"; import { DlqProducer } from "@voidhash/core/services/analyticsIngest/DlqProducer"; import { EventCaptureService } from "@voidhash/core/services/analyticsIngest/EventCaptureService"; +import { + makeMeasurementConfigSignerLayer, + MeasurementConfigurationService, +} from "@voidhash/core/services/measurement/MeasurementConfigurationService"; +import { MeasurementDeletionService } from "@voidhash/core/services/measurement/MeasurementDeletionService"; +import { LinkRedirectService } from "@voidhash/core/services/measurement/LinkRedirectService"; +import { ProtectedEvidenceService } from "@voidhash/core/services/measurement/ProtectedEvidenceService"; import { EventProcessorService } from "@voidhash/core/services/analyticsIngest/EventProcessorService"; import { PolicyCounterStore, @@ -168,8 +175,38 @@ export const makeSelfhostAnalyticsRuntimeLive = (config: SelfhostRuntimeConfig) Layer.provide(ingress), Layer.provide(database), ); + const protectedEvidence = ProtectedEvidenceService.layer.pipe(Layer.provide(database)); + const measurementDeletion = MeasurementDeletionService.layer.pipe(Layer.provide(database)); + const measurementConfiguration = MeasurementConfigurationService.layer.pipe( + Layer.provide(database), + Layer.provide( + makeMeasurementConfigSignerLayer( + process.env.MEASUREMENT_CONFIG_KEY_ID?.trim() || "selfhost-development", + process.env.MEASUREMENT_CONFIG_PRIVATE_KEY_PKCS8?.trim() || undefined, + Number(process.env.MEASUREMENT_CONFIG_VERSION?.trim() || "1"), + ), + ), + ); + const links = LinkRedirectService.layer.pipe( + Layer.provide(database), + Layer.provide( + makeMeasurementConfigSignerLayer( + process.env.MEASUREMENT_CONFIG_KEY_ID?.trim() || "selfhost-development", + process.env.MEASUREMENT_CONFIG_PRIVATE_KEY_PKCS8?.trim() || undefined, + Number(process.env.MEASUREMENT_CONFIG_VERSION?.trim() || "1"), + ), + ), + ); const dispatch = AnalyticsDispatchService.layer.pipe(Layer.provide(ingress)); - return Layer.mergeAll(platform, capture, dispatch); + return Layer.mergeAll( + platform, + capture, + protectedEvidence, + measurementDeletion, + measurementConfiguration, + links, + dispatch, + ); }; /** Runs the analytics ingest and dead-letter consumers until their scope closes. */ diff --git a/selfhost/entry/src/config.ts b/selfhost/entry/src/config.ts index d2f00c171..73b218f30 100644 --- a/selfhost/entry/src/config.ts +++ b/selfhost/entry/src/config.ts @@ -73,6 +73,9 @@ export const validateSelfhostSecurityConfig = (): SelfhostMode => { "WORKOS_CLIENT_ID", "WORKOS_COOKIE_PASSWORD", "WORKOS_WEBHOOK_SECRET", + "MEASUREMENT_CONFIG_KEY_ID", + "MEASUREMENT_CONFIG_PRIVATE_KEY_PKCS8", + "MEASUREMENT_CONFIG_VERSION", ]; if (process.env.CLICKHOUSE_URL?.trim()) { requiredSecrets.push( @@ -85,6 +88,11 @@ export const validateSelfhostSecurityConfig = (): SelfhostMode => { for (const name of requiredSecrets) { if (isExampleSecret(process.env[name])) unsafeSettings.push(name); } + try { + positiveIntegerFromEnv("MEASUREMENT_CONFIG_VERSION", 1); + } catch { + unsafeSettings.push("MEASUREMENT_CONFIG_VERSION"); + } if (!process.env.OPENAI_API_KEY?.trim() && !process.env.ANTHROPIC_API_KEY?.trim()) { unsafeSettings.push("OPENAI_API_KEY or ANTHROPIC_API_KEY"); } diff --git a/selfhost/entry/src/main.ts b/selfhost/entry/src/main.ts index 448ff620b..442feb555 100644 --- a/selfhost/entry/src/main.ts +++ b/selfhost/entry/src/main.ts @@ -2,6 +2,7 @@ import { createServer } from "node:http"; import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; import { EventCaptureApi } from "@voidhash/api-contracts/event-capture"; +import { LinksApi } from "@voidhash/api-contracts/links"; import { buildBackendFetch, buildBackendAgentServices, @@ -11,6 +12,7 @@ import { import { RpcAuthLive } from "@voidhash/backend/src/RpcMiddlewares.ts"; import { AuthTokenVerifier } from "@voidhash/core/services/auth/AuthTokenVerifier"; import { EventCaptureService } from "@voidhash/core/services/analyticsIngest/EventCaptureService"; +import { LinkRedirectService } from "@voidhash/core/services/measurement/LinkRedirectService"; import { PushDeliveryDispatch } from "@voidhash/core/services/notifications/PushDeliveryDispatch"; import { PaywallThumbnailService } from "@voidhash/core/services/paywallThumbnails/PaywallThumbnailService"; import { HostServiceTag } from "@voidhash/mimic-db/app/hostService"; @@ -24,6 +26,7 @@ import { HttpRouter } from "effect/unstable/http"; import { HttpApiBuilder } from "effect/unstable/httpapi"; import { EventCaptureGroupLive } from "@voidhash/backend/src/routes/event-capture.ts"; +import { LinksGroupLive } from "@voidhash/backend/src/routes/links.ts"; import { makeSelfhostAnalyticsRuntimeLive, runSelfhostAnalyticsConsumers, @@ -63,6 +66,19 @@ const isCaptureRequest = (url: string | undefined): boolean => { return pathname === "/i" || pathname.startsWith("/i/"); }; +const isLinksRequest = (url: string | undefined): boolean => { + const pathname = new URL(url ?? "/", "http://selfhost.local").pathname; + return pathname === "/l" || pathname.startsWith("/l/"); +}; + +const linkClick = (url: string | undefined): { readonly linkId: string; readonly token: string } | undefined => { + const parsed = new URL(url ?? "/", "http://selfhost.local"); + const matched = /^\/l\/([^/]+)$/.exec(parsed.pathname); + const token = parsed.searchParams.get("token"); + if (!matched?.[1] || !token) return undefined; + return { linkId: decodeURIComponent(matched[1]), token }; +}; + NodeRuntime.runMain( Effect.scoped( Effect.gen(function* () { @@ -177,6 +193,15 @@ NodeRuntime.runMain( Layer.provide(NodeHttpServer.layerHttpServices), HttpRouter.toHttpEffect, ); + const linkService = Context.get(runtimeContext, LinkRedirectService); + const linksEffect = yield* HttpApiBuilder.layer(LinksApi, { + openapiPath: "/l/docs/openapi.json", + }).pipe( + Layer.provide(LinksGroupLive), + Layer.provide(Layer.succeed(LinkRedirectService, linkService)), + Layer.provide(NodeHttpServer.layerHttpServices), + HttpRouter.toHttpEffect, + ); const scope = yield* Effect.scope; const backendHandler = yield* NodeHttpServer.makeHandler( @@ -188,6 +213,7 @@ NodeRuntime.runMain( captureEffect.pipe(Effect.provide(runtimeContext)), { scope }, ); + const linksHandler = yield* NodeHttpServer.makeHandler(linksEffect, { scope }); const wwwServerEntry = process.env.WWW_SERVER_ENTRY?.trim(); const wwwClientDirectory = process.env.WWW_CLIENT_DIRECTORY?.trim(); if ((wwwServerEntry === undefined) !== (wwwClientDirectory === undefined)) { @@ -211,6 +237,39 @@ NodeRuntime.runMain( captureHandler(request, response); return; } + const click = linkClick(request.url); + if (click) { + const header = (name: string): string => { + const value = request.headers[name]; + return Array.isArray(value) ? value[0] ?? "" : value ?? ""; + }; + Effect.runPromise(linkService.click({ + clickId: `click_${crypto.randomUUID()}`, + linkId: click.linkId, + referer: header("referer") || undefined, + token: click.token, + userAgent: header("user-agent"), + })).then((result) => { + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Referrer-Policy", "no-referrer"); + if (!result) { + response.statusCode = 404; + response.end("Link not found"); + return; + } + response.statusCode = 302; + response.setHeader("Location", result.destination); + response.end(); + }).catch(() => { + response.statusCode = 503; + response.end("Link service unavailable"); + }); + return; + } + if (isLinksRequest(request.url)) { + linksHandler(request, response); + return; + } if (wwwHandler !== undefined && isWwwRequest(request.url)) { wwwHandler(request, response).catch((error) => { console.error("WWW request failed", error);