Skip to content

v1.78.0

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Oct 16:43
· 3 commits to main since this release
a3a5089

Added

  • OnboardingMetadata carries variantKey and deploymentId (#234).
    Both are optional, nullable and typed only: getSteps already passes the
    server's metadata through untouched, so a payload that carries them now
    reaches useOnboardingStep().onboardingMetadata and onComplete's
    metadata typed, and a payload that does not reads them as undefined.
    variantKey names the variant served; the parent onboarding reports the
    reserved "main", exported as MAIN_VARIANT_KEY. null and absent mean
    the same thing (no variant information) and should be treated identically.
    deploymentId is null on a draft (sandbox) payload, which has no
    deployment. No runtime change.
  • Resolved Paywall: variantKey, deploymentId (#235). Optional and
    nullable (string | null, number | null), resolved per moment like
    audienceId, so two moments in one catalog can carry different values. The
    parent paywall reports the reserved variantKey: "main". Passed through from
    get-paywalls untouched; null and absent are equivalent (no variant or
    deployment information). CustomPaywallScreenProps.paywall carries the same
    two fields, so a custom paywall screen sees them too.
  • PaywallObserver.start() reports the served arm (#237):
    PaywallPresentationInfo.variantKey and deploymentId (a decimal string,
    the contract's type) are now filled from the served paywall, on both
    present() and the inline Paywall step. A purchase or dismiss ends the
    presentation start() opened, so it is attributed to the same arm. Both are
    left unset (never empty or "none") when the server sent no variantKey,
    so an app on an older server sees no new property.
  • Every getSteps and getPaywalls request carries a stable
    assignmentId
    (#236), so the server can keep a person in the same variant
    across launches. Resolved per request: the new assignmentId client option
    (a string, or a sync/async getter called on every request, e.g.
    () => Purchases.getAppUserID()), else an installation id generated once and
    persisted in AsyncStorage under rocapine-assignment-id, else omitted. The
    installation id survives clearCache() and OnboardingStudio.reset(). It is
    omitted, rather than invented, when storage cannot be read or written. A host
    value of undefined, null or "" falls back to the installation id.
    assignmentId: false, or a getter returning false, opts out: nothing is
    sent and the installation id is never read, created or written (e.g. until
    consent). A getter that throws, rejects or takes over 2s fails closed the
    same way. With RevenueCat, Purchases.configure() must run before the first
    request.
    assignmentId is now a reserved user property key. It is not part of any
    cache key. Servers that do not read it ignore it. Changing the id (anonymous
    to identified) can move a person to another variant once; see the API
    reference.
  • timeout is now end-to-end for getSteps and getPaywalls: it covers
    resolving the request identity (including the assignmentId getter) as well
    as the fetch, so fallbackOnboarding arrives on time. A request whose budget
    ran out while the id resolved is not sent afterwards.
  • OnboardingStudio.clearAssignmentId() / client.clearAssignmentId()
    (#236) delete the stored rocapine-assignment-id and forget the in-memory
    id, so a later request mints a new one (e.g. after consent is withdrawn,
    together with a getter returning false). A request already resolving the
    id sends none, even while its assignmentId getter is still pending, and a
    first-launch id still being written is not brought back. Rejects if the
    removal fails or is still waiting after 2s. The caller's wait on each queued
    operation is bounded the same way, but operations still run strictly in
    order, so a removal never overtakes a write in flight. A request stuck behind
    a hung write or removal omits the id at its own 2s bound, which counts
    against timeout. A hung read does not block later requests. A timed-out
    request never mints, unless its write was already in flight (that write
    still lands, ahead of any later removal). reset() does not call it.

Fixed

  • assignmentId is sent as the X-Assignment-Id request header, never as a
    query param
    (#345). Edge invocation logs record the full request URL, and
    the id may be a host's user id (e.g. a RevenueCat App User ID), so on the
    querystring it was logged on every getSteps and getPaywalls request. A
    custom audience param named assignmentId is still dropped from the URL. The
    resolution rules are unchanged: false opts out, a getter that throws,
    rejects or takes over 2s sends no header, the installation id fallback and
    clearAssignmentId() behave as before. Requires a server that reads the
    header; one that reads only the old query param assigns as if no id were
    sent.
  • expoIapProductProvider: an Ask to Buy purchase resolves "pending", not
    "error"
    (#337). openiap reports StoreKit's .pending purchase result as
    the error code deferred-payment, and the adapter only recognised the
    cancellation codes, so the paywall ran onError for a purchase still
    awaiting approval. deferred-payment, pending, and the pre-4.x
    E_DEFERRED_PAYMENT / E_PENDING now resolve { status: "pending" }, from
    the error listener and from a requestPurchase rejection alike, so the
    purchase action's onPending branch runs instead.
  • expoIapProductProvider: a purchase that completes after its
    purchase() call returned now reaches onUnclaimedPurchase
    (#337). The
    approved Ask to Buy transaction, a verdict landing after
    purchaseTimeoutMs, and a Play slow-payment purchase that cleared after
    "pending" all arrive with no caller waiting. They were finished silently,
    so the user was charged and nothing granted the purchase. They are now
    handed to onUnclaimedPurchase and finished only when it returns true,
    the same contract as a launch replay. A delivery whose caller is still
    waiting is unchanged: it is answered "purchased" and never shown to the
    host, and a second copy of it (openiap both emits a transaction and returns
    it from requestPurchase) is treated as a replay, not offered to the host.
    The answer is now decided when the transaction arrives rather than after it
    is acknowledged, so a slow finishTransaction can no longer turn a finished
    purchase into a "pending" answer.
  • expoIapProductProvider acknowledges each transaction once (#337). A
    second copy of a transaction (openiap both emits it and returns it from
    requestPurchase) could land while the first finishTransaction was still
    in flight and start a second acknowledgement, which Play rejects. It now
    joins the acknowledgement already in flight. A transaction the host granted
    whose acknowledgement failed is retried on its next copy, never offered to
    onUnclaimedPurchase a second time.

Changed

  • expoIapProductProvider answers the caller before acknowledging, for
    every host. On the listener path purchase() can resolve "purchased"
    before finishTransaction settles, so a host that reads unfinished
    transactions straight after "purchased" may still see it. When iOS returns
    the transaction from requestPurchase directly, purchase() still waits for
    the acknowledgement, as before.
  • Hosts passing onUnclaimedPurchase to expoIapProductProvider now see
    more transactions.
    Any completed transaction with no live caller is
    offered, including later transactions for a product this process already
    bought (for example, a renewal arriving on the update stream). Those were
    previously finished without asking. Return true only once entitlement is
    granted; a throw is treated as false. A declined transaction is left
    unfinished: iOS re-delivers it next launch, but on Android this provider
    does not re-query it, and Play refunds it after three days unacknowledged.
  • Without onUnclaimedPurchase, a late transaction is still finished but
    no longer silently: a dispatched product's transaction that arrives after its
    purchase() returned is still finished, and the provider now warns once
    that nothing granted it. Pass onUnclaimedPurchase; it is the only place
    an approved Ask to Buy purchase can be granted.