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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +34 -1
- data/app/models/pay/google_play/subscription.rb +18 -0
- data/lib/tillbell/engine.rb +12 -0
- data/lib/tillbell/google_play.rb +9 -0
- data/lib/tillbell/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74b959f39549f19cfaad92af9562bd42d373435241e4d3062ec587b8a3fdae69
|
|
4
|
+
data.tar.gz: c4ed0fcb129893703dbdb273716edb439f312bd88af66062b53eebdaca22bfb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/tillbell/engine.rb
CHANGED
|
@@ -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
|
data/lib/tillbell/google_play.rb
CHANGED
|
@@ -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
|
data/lib/tillbell/version.rb
CHANGED