Skip to content

Purchases

Purchases sells in-app products through StoreKit 2 and Google Play Billing, and fake_store stands in for both off device. Enable it with use = ["purchases"] under [plugins]. See the Purchases guide.

In-app purchases: StoreKit 2 on iOS and Google Play Billing on Android.

Enable it in pythonnative.toml:

[plugins]
use = ["purchases"]

Products are defined in App Store Connect and the Play Console, and their ids go here. Off device, and in the browser preview, a fake store stands in; see fake_store.

Example
from pythonnative.purchases import Purchases

async def buy_pro() -> None:
    products = await Purchases.products(["pro"])
    result = await Purchases.purchase(products[0].id)
    if result.status == "purchased" and result.transaction.verified:
        unlock_pro()
        await Purchases.finish(result.transaction)

Always finish a transaction after granting what it paid for. The store keeps re-delivering an unfinished one, and Play refunds one left unacknowledged for three days.

Classes:

Name Description
Product

Something for sale, as the store describes it.

Transaction

A completed payment.

PurchaseResult

The outcome of Purchases.purchase.

FakeStore

The store off device and in the browser preview. It sells only what it's told to.

Purchases

The platform's store.

Attributes:

Name Type Description
ProductKind

What a product is. Play doesn't record whether an in-app product is consumable, so it reports one_time.

PurchaseStatus

How a purchase ended. pending waits on something outside the app, such as a parent's approval.

fake_store

The fake store in use off device.

ProductKind module-attribute

ProductKind = Literal[
    "consumable",
    "non_consumable",
    "subscription",
    "one_time",
]

What a product is. Play doesn't record whether an in-app product is consumable, so it reports one_time.

PurchaseStatus module-attribute

PurchaseStatus = Literal[
    "purchased", "pending", "cancelled"
]

How a purchase ended. pending waits on something outside the app, such as a parent's approval.

fake_store module-attribute

fake_store = FakeStore()

The fake store in use off device.

Product dataclass

Product(
    id: str,
    title: str,
    description: str,
    price: Decimal,
    currency: str,
    display_price: str,
    kind: ProductKind,
)

Something for sale, as the store describes it.

Attributes:

Name Type Description
id str

The product's identifier.

title str

The localized name.

description str

The localized description.

price Decimal

The price in currency.

currency str

The ISO 4217 code.

display_price str

The price formatted for the user's locale.

kind ProductKind

What it is.

Transaction dataclass

Transaction(
    id: str,
    product_id: str,
    purchase_date: datetime,
    token: str,
    verified: bool,
    revoked: bool = False,
)

A completed payment.

Attributes:

Name Type Description
id str

The store's identifier for this transaction.

product_id str

The product it paid for.

purchase_date datetime

When it happened.

token str

What your server sends to the store to check it: the JWS on iOS, and the purchase token on Android.

verified bool

On iOS, whether StoreKit's signature check passed. Always False on Android, where the device has no verdict to give: check token with the Google Play Developer API on your server.

revoked bool

The store took it back, for a refund for example. Only iOS reports this to the device.

PurchaseResult dataclass

PurchaseResult(
    status: PurchaseStatus,
    transaction: Optional[Transaction] = None,
)

The outcome of Purchases.purchase.

Attributes:

Name Type Description
status PurchaseStatus

How it ended.

transaction Optional[Transaction]

The transaction, when status is "purchased".

FakeStore

FakeStore()

The store off device and in the browser preview. It sells only what it's told to.

It isn't a simulator of either real store: it answers immediately and charges nothing. On a device calls never reach it, so setting it up there has no effect. The module's one instance is fake_store.

Example
from decimal import Decimal
from pythonnative.purchases import Product, Purchases, fake_store

fake_store.set_products([
    Product("pro", "Pro", "Everything", Decimal("4.99"), "USD", "$4.99", "non_consumable"),
])
fake_store.set_outcome("cancelled")
result = await Purchases.purchase("pro")
assert result.status == "cancelled"

Methods:

Name Description
reset

Forget all products, outcomes, and transactions.

set_available

Set what is_available answers.

set_products

Set what's for sale.

set_outcome

Set how every later purchase ends. An error is raised from purchase.

push_update

Complete a purchase outside the app, as an approved pending one would, telling on_update.

is_available

Answer what set_available set.

products

Return the ones set_products set.

purchase

End as set_outcome set.

restore

Return every transaction not yet consumed.

finish

Record the id in finished, and consume it only with consume=True.

listen

Do nothing: updates go straight to listeners.

reset

reset() -> None

Forget all products, outcomes, and transactions.

set_available

set_available(available: bool) -> None

Set what is_available answers.

set_products

set_products(products: Sequence[Product]) -> None

Set what's for sale.

set_outcome

set_outcome(
    outcome: Union[PurchaseStatus, NativeModuleError],
) -> None

Set how every later purchase ends. An error is raised from purchase.

push_update

push_update(product_id: str) -> Transaction

Complete a purchase outside the app, as an approved pending one would, telling on_update.

Parameters:

Name Type Description Default
product_id str

The product it paid for.

required

Returns:

Type Description
Transaction

The transaction on_update receives.

is_available

is_available() -> bool

Answer what set_available set.

products

products(ids: List[str]) -> List[Dict[str, Any]]

Return the ones set_products set.

purchase

purchase(product_id: str) -> Dict[str, Any]

End as set_outcome set.

restore

restore() -> List[Dict[str, Any]]

Return every transaction not yet consumed.

finish

finish(id: str, token: str, consume: bool = False) -> None

Record the id in finished, and consume it only with consume=True.

That's Google Play's rule, the stricter one: the App Store also consumes consumables by itself, so code that passes consume here works with both.

listen

listen() -> None

Do nothing: updates go straight to listeners.

Purchases

The platform's store.

Raises:

Type Description
NativeModuleError

If the store fails, or with the code plugin_not_enabled when purchases isn't in the build, or unsupported below iOS 15, which StoreKit 2 needs.

Methods:

Name Description
is_available

Whether this device can buy anything right now.

products

Look up products by id.

purchase

Show the store's purchase sheet and wait for the user.

restore

Sync with the store and return what the user currently owns.

finish

Tell the store the app has delivered what a transaction paid for.

on_update

Call callback for each transaction that didn't come from a purchase call.

is_available async staticmethod

is_available() -> bool

Whether this device can buy anything right now.

Returns:

Type Description
bool

False when purchases are restricted, the store isn't there

bool

(an emulator without Google Play), or iOS is older than 15.

products async staticmethod

products(ids: Sequence[str]) -> List[Product]

Look up products by id.

Parameters:

Name Type Description Default
ids Sequence[str]

The products' identifiers.

required

Returns:

Type Description
List[Product]

The ones the store knows. An id it doesn't know is left out.

purchase async staticmethod

purchase(product_id: str) -> PurchaseResult

Show the store's purchase sheet and wait for the user.

A user who already owns the product gets a purchased result with the existing transaction. Subscriptions buy the product's first offer on Android.

Parameters:

Name Type Description Default
product_id str

The product to buy.

required

Returns:

Type Description
PurchaseResult

The outcome. cancelled and pending are outcomes, not

PurchaseResult

errors. A pending purchase later arrives through

PurchaseResult

Raises:

Type Description
NativeModuleError

With the code unknown_product, or purchase_failed when the store refuses.

restore async staticmethod

restore() -> List[Transaction]

Sync with the store and return what the user currently owns.

On iOS this can prompt the user to sign in, so call it from a "Restore purchases" button. Finished consumables aren't included.

finish async staticmethod

finish(
    transaction: Transaction, *, consume: bool = False
) -> None

Tell the store the app has delivered what a transaction paid for.

Parameters:

Name Type Description Default
transaction Transaction

The transaction.

required
consume bool

On Android, consume the purchase so it can be bought again. Play can't tell which products are consumable, so this is how you say so. iOS knows from App Store Connect and ignores it.

False

on_update staticmethod

on_update(
    callback: Callable[[Transaction], Any],
) -> Callable[[], None]

Call callback for each transaction that didn't come from a purchase call.

This covers pending purchases completing, purchases made outside the app, refunds on iOS (revoked), and whatever a previous launch left unfinished. Subscribe early. The platform holds these for the first subscriber, so nothing is lost before then.

Parameters:

Name Type Description Default
callback Callable[[Transaction], Any]

Receives a Transaction. It may be async.

required

Returns:

Type Description
Callable[[], None]

A function that unsubscribes.