tillbell 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e2e56c0874935103736d6f90d868ec525033d4abe32437269b39ea2772d224b
4
- data.tar.gz: dde35b5c71bee8fee12aef4ba5fd99c5870d034da7150a8fbcaea55d4dfc5a6c
3
+ metadata.gz: 74b959f39549f19cfaad92af9562bd42d373435241e4d3062ec587b8a3fdae69
4
+ data.tar.gz: c4ed0fcb129893703dbdb273716edb439f312bd88af66062b53eebdaca22bfb9
5
5
  SHA512:
6
- metadata.gz: c951a6879b9603ae4202f731714c2568381f7761d5a86b96fe7566df3e79e7d9c334faf82936582e7f27bb9151f4d614e875c65f725fc99f72f9b133296e3e0d
7
- data.tar.gz: fe33d1a43adf91d7a8efdfdad0bbd5acba86536cdcaab1fdf265f6b22491eb3963f9e1029094adc639bf40add424afa360ac6b7666deeda9059a64f43b0ea90e
6
+ metadata.gz: cffefbca74044761044ef78a5b1f8dddea337106f14a23e5fd61c9339c1ce1de023c092f57b9ec27359f32efd0a8daf7598a4c2be4bc5ee43e7fa23256bca38d
7
+ data.tar.gz: ec89c1e8edfd610c70510e51b8b9dc5a20e52f5d0c78c58541dafc5c601b3f49a43072f214a9f60e1b71067aae8d510ac7216935793543adbd6bdf8ed019e274
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ All notable changes to this project are documented in this file. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-11-02
10
+
11
+ ### Added
12
+
13
+ - `grants_access?` on every Pay subscription: it bills and is not paused, and a Google Play
14
+ subscription on hold gives no access (Google requires it), while a Stripe payment being
15
+ retried does.
16
+ - `Pay::GooglePlay::Subscription#owned?`: Google still counts the purchase as the customer's
17
+ (a paused subscription and a pending purchase included), so a new purchase would be answered
18
+ `ALREADY_OWNED`.
19
+ - `Tillbell::GooglePlay.store_url`: the Play Store page of the app.
20
+
21
+ ### Changed
22
+
23
+ - README: an "Upgrading" section, starting with 0.4. Documentation comments on `google_play?` (subscriptions),
24
+ `Tillbell::GooglePlay.package_name` and `reset!`.
25
+
9
26
  ## [0.4.0] - 2026-10-30
10
27
 
11
28
  ### Added
data/README.md CHANGED
@@ -23,6 +23,7 @@ subscription.active? # same rules as for Stripe
23
23
  - [How it works](#how-it-works)
24
24
  - [Requirements](#requirements)
25
25
  - [Installation](#installation)
26
+ - [Upgrading](#upgrading)
26
27
  - [Configuration](#configuration)
27
28
  - [The paywall](#the-paywall)
28
29
  - [Subscriptions and statuses](#subscriptions-and-statuses)
@@ -157,6 +158,31 @@ class User < ApplicationRecord
157
158
  end
158
159
  ```
159
160
 
161
+ ## Upgrading
162
+
163
+ Every release is listed in the [CHANGELOG](CHANGELOG.md). No release so far has added a migration.
164
+
165
+ ### To 0.4
166
+
167
+ Nothing is required, but one behavior changes: `cancel` and `cancel_now!` on a Google Play subscription now raise a `Tillbell::Error` (a `Pay::Error`, with Google's error as its `cause`) instead of a `Google::Apis::Error` or a `Signet::AuthorizationError`. Code that rescued the Google errors around a cancellation, to close or delete an account for example, must rescue `Pay::Error`, which also covers Stripe:
168
+
169
+ ```ruby
170
+ begin
171
+ user.cancel_billing_subscriptions!
172
+ rescue Pay::Error => error
173
+ # Stripe or Google Play refused: keep the account open and say why
174
+ end
175
+ ```
176
+
177
+ Then, to drop the code you wrote around the gem:
178
+
179
+ - `include Tillbell::Owner` in your Pay customer model for `current_subscription`, `google_play_subscription`, `google_play_on_hold?`, `billing_subscriptions` and `cancel_billing_subscriptions!`;
180
+ - declare the plans you sell in `config.plans`: `tillbell:check` verifies them without `PLANS`, and `paywall.plan_change` chooses the replacement mode from their order;
181
+ - subscribe to `plan_changed`, `payment_failed`, `payment_recovered` and `subscription_renewed` instead of reading `saved_changes` in `subscription_updated`;
182
+ - send `subscription.token_digest` to analytics instead of hashing the purchase token yourself.
183
+
184
+ The existing events are unchanged. The paywall texts now exist in French, Spanish and German: if your app set `config.i18n.available_locales`, add the ones you want.
185
+
160
186
  ## Configuration
161
187
 
162
188
  ```ruby
@@ -295,7 +321,8 @@ A `Pay::GooglePlay::Subscription` stores:
295
321
  | `replacement?`, `linked_purchase_token` | Whether this purchase replaces an earlier one, and the token of that one. Google sets it for a plan change, for a customer who signs up again for a subscription that was canceled but had not lapsed, and for prepaid plans converted to or topped up as auto-renewing. Compare `processor_plan` with the replaced subscription's to tell a plan change. |
296
322
  | `replaced?` | Whether a newer purchase replaced this subscription. Set when the replacement is recorded, so it is already true inside the `subscription_expired` callback; use it to tell a replaced subscription from a customer who left (see [Events](#events)). Subscriptions replaced before this was added are not flagged. |
297
323
  | `plan_change_pending?`, `deferred_product_id` | A `DEFERRED` plan change waiting for the next renewal, and the product it goes to. |
298
- | `manage_url` | The Play Store page where the customer manages this subscription (update the payment method, cancel, resubscribe). Same page as `paywall.manage`, for web pages and emails. `Tillbell::GooglePlay.manage_url(product_id)` gives it for any product, and without one the list of all subscriptions. |
324
+ | `manage_url` | The Play Store page where the customer manages this subscription (update the payment method, cancel, resubscribe). Same page as `paywall.manage`, for web pages and emails. `Tillbell::GooglePlay.manage_url(product_id)` gives it for any product, and without one the list of all subscriptions. `Tillbell::GooglePlay.store_url` is the page of the app itself, for a "Get it on Google Play" link. |
325
+ | `owned?` | Whether Google still counts the purchase as the customer's, even without access: a paused subscription or a pending purchase (a cash payment) too. Buying the same product again would be answered `ALREADY_OWNED`, so show `manage_url` instead of a paywall. False once refunded, replaced or expired. |
299
326
  | `in_grace_period?`, `on_hold?`, `revoked?` | The Google states that Pay statuses merge. |
300
327
 
301
328
  **Plans.** Declare what you sell in `config.plans` and Tillbell knows the rest:
@@ -334,6 +361,12 @@ A plan change from the settings page is then one option in a paywall:
334
361
  | `billing_subscriptions` | What still bills or resumes by itself (`subscription.billing?`), Google Play first. |
335
362
  | `cancel_billing_subscriptions!` | Cancels them: call it before closing or deleting an account. |
336
363
 
364
+ **Who has access.** `subscription.grants_access?`, on every Pay subscription, is true when it bills (`billing?`) and is not paused. A payment the processor retries keeps access, so the customer can reach the page where they fix it, except on Google Play: a subscription on hold comes after Google's grace period, and Google requires access to be suspended until the payment succeeds.
365
+
366
+ ```ruby
367
+ def subscribed? = !!current_user.current_subscription&.grants_access?
368
+ ```
369
+
337
370
  A customer can have a Stripe subscription and a Google Play one (a web customer who later subscribes in the app, or the reverse). `Pay::GooglePlay::Customer` is never the owner's default payment processor, so `owner.payment_processor` does not see Google Play subscriptions: look at `owner.pay_subscriptions`, or at the Google Play customer (`owner.pay_customers.google_play.first`), to decide who has access.
338
371
 
339
372
  Pay only knows its own processors, so Tillbell adds `google_play?` to customers and subscriptions, and a `google_play` scope next to Pay's `stripe` one (`Pay::Subscription.google_play`).
@@ -45,6 +45,14 @@ module Pay
45
45
  # Google states of a payment that failed and is being retried.
46
46
  PAYMENT_PROBLEM_STATES = %w[SUBSCRIPTION_STATE_IN_GRACE_PERIOD SUBSCRIPTION_STATE_ON_HOLD].freeze
47
47
 
48
+ # Google states in which the purchase still belongs to the customer (see +owned?+): every
49
+ # state but an expiry and a pending purchase that was canceled. A whitelist, so an unknown
50
+ # or missing state (a row older than the state column) never blocks a new purchase.
51
+ OWNED_STATES = %w[
52
+ SUBSCRIPTION_STATE_PENDING SUBSCRIPTION_STATE_ACTIVE SUBSCRIPTION_STATE_IN_GRACE_PERIOD
53
+ SUBSCRIPTION_STATE_ON_HOLD SUBSCRIPTION_STATE_PAUSED SUBSCRIPTION_STATE_CANCELED
54
+ ].freeze
55
+
48
56
  store_accessor :data, :subscription_state, :base_plan_id, :auto_renewing, :revoked_at, :replaced_by
49
57
 
50
58
  class << self
@@ -199,6 +207,16 @@ module Pay
199
207
  status == "paused"
200
208
  end
201
209
 
210
+ # True while Google still counts the purchase as the customer's: buying the same product
211
+ # again would be answered +ALREADY_OWNED+, so offer +manage_url+ rather than a paywall. It
212
+ # covers states that give no access (see +grants_access?+): a paused subscription resumes
213
+ # by itself and a pending purchase (a cash payment) completes or lapses at Google. A
214
+ # refunded, replaced or expired purchase is no longer owned, whatever Google still reports
215
+ # for its token.
216
+ def owned?
217
+ !revoked? && !replaced? && OWNED_STATES.include?(subscription_state)
218
+ end
219
+
202
220
  # The Play Store page where the customer manages this subscription (update the payment
203
221
  # method, cancel, resubscribe). See Tillbell::GooglePlay.manage_url.
204
222
  def manage_url
@@ -36,8 +36,20 @@ module Tillbell
36
36
  !ended? && (active? || past_due? || unpaid? || paused?)
37
37
  end
38
38
 
39
+ # True when the subscription should give access to what it pays for: it bills
40
+ # (+billing?+) and is not paused. A payment the processor retries keeps access, so the
41
+ # customer can still reach the page where they fix it, except on Google Play: a
42
+ # subscription on hold (past_due) comes after Google's grace period, and Google requires
43
+ # access to be suspended until the payment succeeds. Combine it with
44
+ # +Tillbell::Owner#current_subscription+ to decide what an owner may use.
45
+ def grants_access?
46
+ billing? && !paused? && !(google_play? && past_due?)
47
+ end
48
+
39
49
  scope :google_play, -> { joins(:customer).where(pay_customers: { processor: "google_play" }) }
40
50
 
51
+ # True for a subscription bought with Google Play, whatever the owner's default
52
+ # processor is.
41
53
  def google_play?
42
54
  customer.processor == "google_play"
43
55
  end
@@ -96,6 +96,14 @@ module Tillbell
96
96
  "#{page}?sku=#{ERB::Util.url_encode(product_id)}&package=#{ERB::Util.url_encode(package_name)}"
97
97
  end
98
98
 
99
+ # The Play Store page of the app itself (+config.google.package_name+), for a "Get it on
100
+ # Google Play" link on the website.
101
+ def store_url
102
+ "https://play.google.com/store/apps/details?id=#{ERB::Util.url_encode(package_name)}"
103
+ end
104
+
105
+ # The Android package name Google Play knows the app by, from +config.google.package_name+.
106
+ # Raises +Tillbell::Error+ when it is not set.
99
107
  def package_name
100
108
  Tillbell.google.package_name.presence || raise(Error, "Tillbell.google.package_name is not set")
101
109
  end
@@ -109,6 +117,7 @@ module Tillbell
109
117
  end
110
118
  end
111
119
 
120
+ # Forgets the memoized client so the next call builds it from the current credentials.
112
121
  def reset!
113
122
  @service = nil
114
123
  end
@@ -1,3 +1,3 @@
1
1
  module Tillbell
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tillbell
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pierre Filstroff