Current section
Files
Jump to
Current section
Files
lib/receipt_verifier.ex
defmodule ReceiptVerifier do
@moduledoc """
Verify iTunes receipt with the Apple Store
## Example
iex> ReceiptVerifier.verify(base64_encoded_receipt_data)
{:ok, %ReceiptVerifier.ResponseData{app_receipt: %ReceiptVerifier.AppReceipt{adam_id: 0,
app_item_id: 0, application_version: "1241",
bundle_id: "com.sumiapp.GridDiary", download_id: 0,
in_app: [%ReceiptVerifier.IAPReceipt{expires_date: nil,
is_trial_period: false,
original_purchase_date: %DateTime{calendar: Calendar.ISO, day: 17, hour: 6,
microsecond: {491000, 6}, minute: 52, month: 1, second: 13, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
original_transaction_id: "1000000118990828",
product_id: "com.sumiapp.GridDiary.pro",
purchase_date: %DateTime{calendar: Calendar.ISO, day: 17, hour: 7,
microsecond: {546000, 6}, minute: 33, month: 1, second: 48, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
quantity: 1, transaction_id: "1000000118990828",
web_order_line_item_id: nil},
%ReceiptVerifier.IAPReceipt{expires_date: nil, is_trial_period: false,
original_purchase_date: %DateTime{calendar: Calendar.ISO, day: 17, hour: 7,
microsecond: {546000, 6}, minute: 33, month: 1, second: 48, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
original_transaction_id: "1000000122102348",
product_id: "com.sumiapp.griddiary.test",
purchase_date: %DateTime{calendar: Calendar.ISO, day: 17, hour: 7,
microsecond: {546000, 6}, minute: 33, month: 1, second: 48, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
quantity: 1, transaction_id: "1000000122102348",
web_order_line_item_id: nil}], original_application_version: "1.0",
original_purchase_date: %DateTime{calendar: Calendar.ISO, day: 16, hour: 22,
microsecond: {400000, 6}, minute: 2, month: 1, second: 20, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
receipt_creation_date: %DateTime{calendar: Calendar.ISO, day: 17, hour: 7,
microsecond: {546000, 6}, minute: 33, month: 1, second: 48, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
receipt_type: "ProductionSandbox",
request_date: %DateTime{calendar: Calendar.ISO, day: 18, hour: 2,
microsecond: {590831, 6}, minute: 47, month: 1, second: 30, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 1970, zone_abbr: "UTC"},
version_external_identifier: 0}, base64_latest_app_receipt: nil,
latest_iap_receipts: []}}
"""
alias ReceiptVerifier.Client
alias ReceiptVerifier.Parser
alias ReceiptVerifier.ResponseData
alias ReceiptVerifier.Error
@typedoc """
- `env` - *(Optional)* The environment, default to `:auto`
- `:production` - production environment
- `:sandbox` - sandbox environment
- `:auto` - choose the environment automatically, in this mode,
if you send sandbox receipt to production server, it will be
automatically resend to test server.
Same for the production receipt.
- `exclude_old_transactions` - *(Optional)* Exclude the old transactions
- `password` - *(Optional)* the shared secret used for auto-renewable subscriptions
"""
@type options :: [
env: :production | :sandbox | :auto,
exclude_old_transactions: boolean(),
password: String.t()
]
@default_options [
env: :auto
]
@doc """
Verify Base64-encoded receipt with the Apple Store
"""
@spec verify(binary(), options) :: {:ok, ResponseData.t()} | {:error, Error.t()}
def verify(receipt, opts \\ []) when is_binary(receipt) do
options =
@default_options
|> Keyword.merge(opts)
|> Enum.into(%{})
do_verify(receipt, options)
end
defp do_verify(receipt, options) when is_map(options) do
with {:ok, json} <- Client.request(receipt, options),
{:ok, data} <- Parser.parse_response(json) do
{:ok, data}
else
{:error, %Error{code: code} = error} when code in [21_007, 21_008] ->
maybe_retry(receipt, error, options)
{:error, %Error{code: code, meta: meta} = err} when code in 21_100..21_199 ->
if Keyword.get(meta, :retry?, false) do
do_verify(receipt, options)
else
{:error, err}
end
{:error, reason} ->
{:error, reason}
end
end
defp maybe_retry(receipt, error, opts) do
with :auto <- opts.env do
case error do
%Error{code: 21_007} ->
retry_in_env(receipt, :sandbox, opts)
%Error{code: 21_008} ->
retry_in_env(receipt, :production, opts)
end
else
env when env in [:sandbox, :production] ->
{:error, error}
end
end
defp retry_in_env(receipt, env, opts) do
opts =
opts
|> Map.merge(%{env: env})
do_verify(receipt, opts)
end
end