superscribe

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2019 License: MIT Imports: 9 Imported by: 2

README

Superscribe

Build Status GoDoc

An easier way to handle App Store subscriptions

Overview

Getting App Store Status Update Notifications (now Server-to-Server Notifications) for in-app subscriptions can be tricky. RENEWAL notifications may not occur as expected, CANCEL does not indicate when auto_renew_status was switched on or off, and it can seem arbitrary when you get older iOS 6 style of receipts or the new style.

Others have described these challenges too:

Superscribe intends to provide a basic “just works”, correct solution for the main subscription use cases.

Get started

Install
go get github.com/carpenterscode/superscribe
Configure

Superscribe provides a server that both

  • scans for expiring subscriptions to check for state changes (like successful renewals) and
  • listens for App Store notifications for a limited number of subscription events.

The server needs

  1. Your App Store shared secret
  2. A func you define to retrieve expiring subscriptions from the database, called during a scan operation
  3. A listener to update the database after the scan

See how to connect listeners to Superscribe in example/main.go. This shows how to use the included AppsFlyer listener that attributes events using server-to-server API.

Optionally you can provide

  • More listeners, such as for server-side conversion analytics, etc.
srv.AddListener(AnalyticsListener{db, tracker})
  • HTTP server request handlers, such as for 200 OK responses to /healthz pings
srv.HandleFunc("/healthz", func(writer http.ResponseWriter, req *http.Request) {
	writer.Write([]byte("OK"))
})

You cannot currently

  • Modify the App Store Status Update Notification endpoint. It's currently hardcoded to /superscribe, but we can change that in the future.
  • Use anything more sophisticated than a Go time.Ticker.
Usage
Run automated tests

Generate mocks first

go generate

Test

go test ./... ./receipt

Caveats

Currently, Superscribe should only be run in a single instance setup. I personally run it on production in a single-pod Kubernetes deployment, but we should figure out how to solve for redundancy and performance by adding some kind of scalability.

Future work

There's a lot of unfortunate complexity to subscription management, so the longer term goal is to increase extensibility and robustness.

Most important: Let’s gather real use-cases and requirements to draft a prioritized roadmap.

  • Distinguish among first, first year's worth of, and remaining payments. The paid at event could be made more versatile and track 30% vs 15% App Store fee. Or to filter out renewals from first payments.
  • Track plan upgrade responses from customers. For instance, moving all monthly subscriptions from 7.99/mo to 9.99/mo.
  • Offer a scalable solution. Subscriptions in the local database should only be scanned by a single process, but multiple instances of listeners should be able to coexist. The current 1:1 model limits Superscribe to one instance.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewServer

func NewServer(addr, secret string, matcher ExpiringSubscriptions,
	fetch SubscriptionFetch, interval time.Duration) *server

Types

type AutoRenewEvent

type AutoRenewEvent interface {
	Subscription
	AutoRenewProduct() string
	AutoRenewChangedAt() time.Time
}

type Env added in v0.3.0

type Env string
const (
	Sandbox Env = "Sandbox"
	Prod    Env = "PROD"
)

type Event

type Event struct {
	// contains filtered or unexported fields
}

func (Event) AdvertisingID added in v0.3.0

func (evt Event) AdvertisingID() string

func (Event) AutoRenewChangedAt added in v0.3.0

func (evt Event) AutoRenewChangedAt() time.Time

func (Event) AutoRenewProduct added in v0.3.0

func (evt Event) AutoRenewProduct() string

func (Event) AutoRenewStatus added in v0.3.0

func (evt Event) AutoRenewStatus() bool

func (Event) CancelledAt added in v0.3.0

func (evt Event) CancelledAt() time.Time

func (Event) Currency added in v0.3.0

func (evt Event) Currency() string

func (Event) DeviceIP added in v0.3.0

func (evt Event) DeviceIP() string

func (Event) Email added in v0.3.0

func (evt Event) Email() string

func (Event) ExpiresAt added in v0.3.0

func (evt Event) ExpiresAt() time.Time

func (Event) FacebookID added in v0.3.0

func (evt Event) FacebookID() string

func (Event) FirstName added in v0.3.0

func (evt Event) FirstName() string

func (Event) GetString added in v0.3.0

func (evt Event) GetString(key string) string

func (Event) ImageURL added in v0.3.0

func (evt Event) ImageURL() string

func (Event) IsTrialPeriod added in v0.3.0

func (evt Event) IsTrialPeriod() bool

func (Event) LastName added in v0.3.0

func (evt Event) LastName() string

func (Event) OriginalTransactionID

func (evt Event) OriginalTransactionID() string

func (Event) PaidAt added in v0.3.0

func (evt Event) PaidAt() time.Time

func (Event) PremiumAccess added in v0.3.0

func (evt Event) PremiumAccess() bool

func (Event) Price added in v0.3.0

func (evt Event) Price() float64

func (Event) ProductID

func (evt Event) ProductID() string

func (Event) RefundedAt added in v0.3.0

func (evt Event) RefundedAt() time.Time

func (*Event) SetNote added in v0.3.0

func (evt *Event) SetNote(note Note)

func (*Event) SetReceiptInfo added in v0.3.0

func (evt *Event) SetReceiptInfo(resp receipt.Info)

func (*Event) SetRevenue added in v0.3.0

func (evt *Event) SetRevenue(currency string, price float64)

func (*Event) SetStartedTrialAt added in v0.3.0

func (evt *Event) SetStartedTrialAt(startedTrialAt time.Time)

func (*Event) SetUser added in v0.3.0

func (evt *Event) SetUser(user User)

func (Event) SignedUpAt added in v0.3.0

func (evt Event) SignedUpAt() time.Time

func (Event) StartedTrialAt added in v0.3.0

func (evt Event) StartedTrialAt() time.Time

func (Event) Status added in v0.3.0

func (evt Event) Status() int

func (Event) String added in v0.3.0

func (evt Event) String() string

func (Event) UserID added in v0.3.0

func (evt Event) UserID() string

type EventListener

type EventListener interface {

	// Name describes the listener for identification in the logs
	Name() string

	// ChangedAutoRenewProduct indicates the next renewal period's product ID
	ChangedAutoRenewProduct(AutoRenewEvent) error

	// ChangedAutoRenewStatus indicates new on/off state
	ChangedAutoRenewStatus(AutoRenewEvent) error

	// Paid indicates a successful charge
	Paid(PayEvent) error

	// Refunded indicates App Store customer support issued a subscription refund of some sort
	Refunded(RefundEvent) error

	// StartedTrial indicates a subscription free trial began
	StartedTrial(StartTrialEvent) error
}

type ExpiringSubscriptions

type ExpiringSubscriptions func(time.Time) []string

ExpiringSubscriptions returns a list of App Store receipts for subscriptions nearing expiration for a specified current time.

type MultiEventListener

type MultiEventListener struct {
	// contains filtered or unexported fields
}

func NewMultiEventListener

func NewMultiEventListener() *MultiEventListener

func (*MultiEventListener) Add

func (multi *MultiEventListener) Add(l EventListener, mustSucceed bool)

func (MultiEventListener) ChangedAutoRenewProduct

func (multi MultiEventListener) ChangedAutoRenewProduct(evt AutoRenewEvent) error

func (MultiEventListener) ChangedAutoRenewStatus

func (multi MultiEventListener) ChangedAutoRenewStatus(evt AutoRenewEvent) error

func (*MultiEventListener) Name added in v0.3.0

func (multi *MultiEventListener) Name() string

func (MultiEventListener) Paid

func (multi MultiEventListener) Paid(evt PayEvent) error

func (MultiEventListener) Refunded

func (multi MultiEventListener) Refunded(evt RefundEvent) error

func (MultiEventListener) StartedTrial

func (multi MultiEventListener) StartedTrial(evt StartTrialEvent) error

type Note added in v0.3.0

type Note interface {
	Type() NoteType
	Environment() Env

	receipt.Info

	AutoRenewProduct() string
	AutoRenewChangedAt() time.Time
	RefundedAt() time.Time
	StartedTrialAt() time.Time
}

type NoteType added in v0.3.0

type NoteType string
const (
	Cancel               NoteType = "CANCEL"
	DidChangeRenewalPref NoteType = "DID_CHANGE_RENEWAL_PREF"
	InitialBuy           NoteType = "INITIAL_BUY"
	InteractiveRenewal   NoteType = "INTERACTIVE_RENEWAL"
	Renewal              NoteType = "RENEWAL"

	// Introduced in June 2019 at WWDC
	DidChangeRenewalStatus NoteType = "DID_CHANGE_RENEWAL_STATUS"
)

type Notification

type Notification struct {
	Env              Env      `json:"environment"`
	NotificationType NoteType `json:"notification_type"`
	Password         string   `json:"password"`

	CancellationDate   *receipt.AppleTime `json:"cancellation_date,omitempty"`
	WebOrderLineItemID string             `json:"web_order_line_item_id"`

	LatestReceipt            string                  `json:"latest_receipt,omitempty"`
	LatestReceiptInfo        receipt.ReceiptInfoBody `json:"latest_receipt_info,omitempty"`
	LatestExpiredReceipt     string                  `json:"latest_expired_receipt,omitempty"`
	LatestExpiredReceiptInfo receipt.ReceiptInfoBody `json:"latest_expired_receipt_info,omitempty"`

	AutoRenewStatus          bool              `json:"auto_renew_status,string"`
	AutoRenewStatusChangedAt receipt.AppleTime `json:"auto_renew_status_change_date"`
	AutoRenewAdamID          string            `json:"auto_renew_adam_id"`
	AutoRenewProductID       string            `json:"auto_renew_product_id"`
	ExpirationIntent         string            `json:"expiration_intent"`
}

type PayEvent

type PayEvent interface {
	Subscription
	PaidAt() time.Time
}

type RefundEvent

type RefundEvent interface {
	Subscription
	RefundedAt() time.Time
}

type StartTrialEvent

type StartTrialEvent interface {
	Subscription
	StartedTrialAt() time.Time
}

type Subscription added in v0.3.0

type Subscription interface {
	OriginalTransactionID() string
	ProductID() string

	AutoRenewStatus() bool
	IsTrialPeriod() bool
	ExpiresAt() time.Time

	Currency() string
	Price() float64

	User
}

type SubscriptionFetch added in v0.3.0

type SubscriptionFetch func(string) (Subscription, error)

SubscriptionFetch returns the last known state of a subscription by original transaction ID, which can determine what changes have happened when compared to the latest receipt info.

type User added in v0.3.0

type User interface {
	UserID() string
	FacebookID() string
	SignedUpAt() time.Time

	FirstName() string
	LastName() string
	Email() string
	ImageURL() string

	AdvertisingID() string
	DeviceIP() string
	PremiumAccess() bool
	GetString(string) string
}

Directories

Path Synopsis
receipt module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL