Skip to content

Instantly share code, notes, and snippets.

@danielsilverstone-ct
Last active March 2, 2022 09:41
Show Gist options
  • Save danielsilverstone-ct/51328d9b76508bf78d7b41ae11c1765a to your computer and use it in GitHub Desktop.
Save danielsilverstone-ct/51328d9b76508bf78d7b41ae11c1765a to your computer and use it in GitHub Desktop.

Wallet related API etc.

The 'wallet' APIs are related to making donations/purchases (and receiving money).

A user's wallet may have any of the following in it:

  • Consumer card information - one or more "card"s which correspond to a Stripe payment method.
  • Provider account information - a singular connected Stripe express account

For now, we are not considering provider account information.

As well as cards/provider accounts, a user may have, associated with their wallet, any number of transactions. Transactions are independent of ownership of pay-for-apps etc. They are merely logs of the user's activity. Unlike cards, and provider information, which are stored on Stripe, the transactions are mirrored into the backend's database so that we can store semantic info about them.

If the user is not logged in, all APIs will return a 403. Ditto if a user tries to retrieve a transaction belonging to another user.

We group all wallet related APIs under /wallet/ because that way they're nicely collated. In addition, the fact we're using Stripe is as abstracted as possible from the API, though where there are Stripe-specific bits, they're clearly labelled as Stripe so that if we want to switch to a different provider in the future we can with fairly well constrained and grepable things to deal with at the API level.

GET /wallet/walletinfo

Returns a JSON object with the wallet details

{
  "status": "ok",
  "cards": [],
  "account": {}
}

The cards entry will be an empty list if the user's wallet has no saved cards, which is perfectly permissible as the user may have chosen to not save card details.

The account entry will be absent if the user has not set up a payment account for flatpaks they control. (For now, this will always be absent)

If there are any cards, each card is a dict of the form:

{
  "id": "...",
  "brand": "visa",
  "country": "US",
  "exp_month": 8,
  "exp_year": 2022,
  "last4": "1234"
}

This permits a reasonable render of the cards in the frontend. The country will be an ISO-3166-1 alpha-2 country code. The card's brand will be one of: American Express, Diners Club, Discover, JCB, MasterCard, UnionPay, Visa, or Unknown. For consistency, it will always be lowercased and spaces will be removed, leading to values like americanexpress or jcb.

POST /wallet/removecard

If the user wants to bin a card off from their account's wallet, the frontend will make a POST to the backend, passing the full card details in. If there is not a card which exactly matches what is POSTed in, then an error will be returned. If the card is successfully deleted, a 201 No Content will be returned. Errors will take the form of:

{
  "status": "error",
  "error": "..."
}

The error may be any of a number of issues such as "card not found" or "failure talking to stripe".

GET /wallet/transactions

Because a user could end up with a significant number of transactions over time, this API is designed to support paging. As such, the following query string arguments are available:

  • sort - takes one of: recent, oldest and sorts the transactions in that order. The default if not provided is recent. The sort key is created.
  • since - takes a transaction ID and returns the next transactions after the named transaction
  • limit - the maximum number of transactions to return, if this number is more than 100, the backend will clamp it to 100

The returned data is in the form:

[
  {
    "id": "...",
    "value": 1234,
    "currency": "usd",
    "kind": "donation",
    "status": "success",
    "reason": "...",
    "created": 1646210893,
    "updated": 1646210893
  } // ...
]

The status value will be one of new, pending, retry, success, or cancelled. If the status is cancelled or retry, then there will be a reason field too. If the status is not one of those then there will not be a reason field.

The currency is always a 3 letter ISO currency code in lower case. The value is always shown in the currency's smallest denomination (e.g. number of cents).

The frontend is at liberty to use a browser built-in such as Intl.NumberFormat to then render the currency nicely based not only on the currency, but also the user's locale. It will be useful to know the set of currencies which are integral (have no decimals) Stripe has such info at hand. This information will be especially important when it comes to getting users to make payments because we will need to be careful. Of course, if we limit ourselves to only dollars, things will be easier.

The kind field will be donation if it was a pure donation, or else it will be purchase. In the latter case, some parts of the purchase may end up being donations by default, this will show in the detailed transaction data (see below).

The created field is when the transaction was first created. The updated field is when the transaction was last updated (e.g. when it completed or failed).

GET /wallet/transactions/{id}

Retrieve more details about a given transaction. This might be used in an expansion of a transaction, or on an "Invoice" page. As such, this endpoint will return the same information as the transactions list, but also information about the rows of the transaction

{
  "summary": {
    // Info as above
  },
  "card": {
    // Information about the card which paid this, like the cards in the wallet
  },
  "details": [
    // detail rows
  ]
}

Detail rows provide some information about the transaction's payouts. Each row consists of some information about who received how much from the transaction.

{
  "recipient": "some.flatpak.name",
  "amount": "...",
  "currency": "...",
  "kind": "..."
}

Where the recipient was Flathub itself, it shall be listed as "org.flathub.Flathub", and where the recipient was a platform, that will match the platform specification such as "org.freedesktop.Platform" or "org.gnome.Platform". The frontend may need to special-case these recipients rather than linking to their appinfo from the appstream.

Possible runtimes are listed here: https://docs.flatpak.org/en/latest/available-runtimes.html though we suggest that the frontend limits to the core Platform runtime names for simplicity.

POST /wallet/transactions

When the frontend wishes to make a transaction it should post the transaction in the same shape as would be retrieved from the /wallet/transactions/{id} endpoint, without the card entry or the summary/status field.

The backend will validate the request, compute a relevant payment intent, and prepare an new transaction.

If the transaction is bad, or an error occurs during the preparation of the new transaction then an error message will be returned of the form:

{
  "status": "error",
  "error": "...."
}

Errors might be due to the transaction data being inconsistent, or if the backend cannot construct the transaction with Stripe.

On success the backend will return:

{
  "status": "ok",
  "id": "..."
}

Where the id is the ID of the newly created transaction. At this point, the transaction is ready to be paid.

Making the payment for a transaction.

If there is a transaction in the new or retry state then the frontend can attempt to make payment. Since this involves particular interaction with Stripe, the frontend will need to retrieve the relevant information to be able to make a Stripe payment. To do that the frontend will need to find out from the user if they want to reuse a card already in their wallet, and if not, will have to take card details etc. for the payment.

POST /wallet/transactions/{id}/setcard

If the user opts to reuse a card, then the frontend should post an update to the transaction to set the card:

The transaction must be in new or retry status. The submitted card must be of the form used in the above APIs and must match a card exactly including ID etc. or it will not be accepted by the update.

Result of this API call will be:

{
  "status": "ok",
  // or
  "status": "error",
  "error": "..."
}

GET /wallet/stripedata

This is the only endpoint which doesn't require the user be logged in. It returns the public stripe data necessary for any page using stripe to initialise the stripe API.

{
  "status": "ok",
  "public_key": "...."
}

If the backend isn't configured with stripe data then this will give an error instead:

{
  "status": "error",
  "error": "..."
}

GET /wallet/transactions/{id}/stripe

For transactions handled by stripe, fetching this will return stripe-specific data which can be used by the frontend with Stripe.js elements etc. Only transactions in new or retry state can have their stripe data fetched

Any other transaction will return 400 Bad Request

{
  "client_secret": "...",
  "card": {
    // ...
  }
}

If there is a pre-recorded card for this transaction then we return it here so that it can be part of the UI. If the card previously failed for whatever reason then the user may have to select to enter fresh details.

Stripe webhooks will cause the transaction to move through pending and thence to either retry or success.

POST /wallet/transactions/{id}/cancel

If the user has tried and tried, and doesn't want to try again, then the frontend can cancel a transaction by requesting this endpoint. The transaction must be in new or retry for this to work.

In addition, due to the asynchronous nature of Stripe, it's possible for a cancelled transaction to move back to retry or to success if the transaction is updated by webhook. Cancelling a transaction you have attempted to make will not prevent Stripe from trying to perform the payment if it's already in flight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment