Compare commits

..

9 Commits

Author SHA1 Message Date
binwiederhier
57a51ab2da Fix tests 2025-08-08 15:16:53 -04:00
binwiederhier
998dbd9054 Undo main.go 2025-08-08 15:02:09 -04:00
binwiederhier
a5a55bd43a Move WebPush tests 2025-08-07 18:54:37 +02:00
binwiederhier
00409d834b Add build flag for webpush 2025-08-07 18:31:42 +02:00
binwiederhier
d9ab7cc78d Add "nowebpush" build tag 2025-08-07 17:39:25 +02:00
binwiederhier
99a2ca8802 Add build tags for Firebase 2025-08-07 17:24:57 +02:00
binwiederhier
ea338ae4fa Make it easy to build without Stripe 2025-08-07 16:41:39 +02:00
binwiederhier
32fa8d43c1 Merge branch 'main' into debian-stripe 2025-08-07 15:34:54 +02:00
binwiederhier
5ccc131e73 Derp 2025-07-04 06:41:14 +02:00
18 changed files with 250 additions and 71 deletions

View File

@@ -16,10 +16,10 @@ import (
"syscall"
"time"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
@@ -279,6 +279,8 @@ func execServe(c *cli.Context) error {
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist")
} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
} else if keepaliveInterval < 5*time.Second {
@@ -320,6 +322,8 @@ func execServe(c *cli.Context) error {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if enableSignup && !enableLogin {
return errors.New("cannot set enable-signup without also setting enable-login")
} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
@@ -329,6 +333,8 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M")
}
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" {
@@ -396,8 +402,7 @@ func execServe(c *cli.Context) error {
// Stripe things
if stripeSecretKey != "" {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
payments.Setup(stripeSecretKey)
}
// Add default forbidden topics

View File

@@ -1,4 +1,4 @@
//go:build !noserver
//go:build !noserver && !nowebpush
package cmd

21
payments/payments.go Normal file
View File

@@ -0,0 +1,21 @@
//go:build !nopayments
package payments
import "github.com/stripe/stripe-go/v74"
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = true
// SubscriptionStatus is an alias for stripe.SubscriptionStatus
type SubscriptionStatus stripe.SubscriptionStatus
// PriceRecurringInterval is an alias for stripe.PriceRecurringInterval
type PriceRecurringInterval stripe.PriceRecurringInterval
// Setup sets the Stripe secret key and disables telemetry
func Setup(stripeSecretKey string) {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
}

View File

@@ -0,0 +1,18 @@
//go:build nopayments
package payments
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = false
// SubscriptionStatus is a dummy type
type SubscriptionStatus string
// PriceRecurringInterval is dummy type
type PriceRecurringInterval string
// Setup is a dummy type
func Setup(stripeSecretKey string) {
// Nothing to see here
}

View File

@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/payments"
"io"
"net"
"net/http"
@@ -165,7 +166,7 @@ func New(conf *Config) (*Server, error) {
mailer = &smtpSender{config: conf}
}
var stripe stripeAPI
if conf.StripeSecretKey != "" {
if payments.Available && conf.StripeSecretKey != "" {
stripe = newStripeAPI()
}
messageCache, err := createMessageCache(conf)

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server
import (
@@ -14,6 +16,10 @@ import (
)
const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = true
fcmMessageLimit = 4000
fcmApnsBodyMessageLimit = 100
)
@@ -73,7 +79,7 @@ type firebaseSenderImpl struct {
client *messaging.Client
}
func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) {
func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
if err != nil {
return nil, err

View File

@@ -0,0 +1,38 @@
//go:build nofirebase
package server
import (
"errors"
"heckel.io/ntfy/v2/user"
)
const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = false
)
var (
errFirebaseNotAvailable = errors.New("Firebase not available")
errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase")
)
type firebaseClient struct {
}
func (c *firebaseClient) Send(v *visitor, m *message) error {
return errFirebaseNotAvailable
}
type firebaseSender interface {
Send(m string) error
}
func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
return nil
}
func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
return nil, errFirebaseNotAvailable
}

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server
import (

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server
import (
@@ -12,6 +14,7 @@ import (
"github.com/stripe/stripe-go/v74/subscription"
"github.com/stripe/stripe-go/v74/webhook"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"io"
@@ -22,7 +25,7 @@ import (
// Payments in ntfy are done via Stripe.
//
// Pretty much all payments related things are in this file. The following processes
// Pretty much all payments-related things are in this file. The following processes
// handle payments:
//
// - Checkout:
@@ -464,8 +467,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
billing := &user.Billing{
StripeCustomerID: customerID,
StripeSubscriptionID: subscriptionID,
StripeSubscriptionStatus: stripe.SubscriptionStatus(status),
StripeSubscriptionInterval: stripe.PriceRecurringInterval(interval),
StripeSubscriptionStatus: payments.SubscriptionStatus(status),
StripeSubscriptionInterval: payments.PriceRecurringInterval(interval),
StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0),
StripeSubscriptionCancelAt: time.Unix(cancelAt, 0),
}

View File

@@ -0,0 +1,47 @@
//go:build nopayments
package server
import (
"net/http"
)
type stripeAPI interface {
CancelSubscription(id string) (string, error)
}
func newStripeAPI() stripeAPI {
return nil
}
func (s *Server) fetchStripePrices() (map[string]int64, error) {
return nil, errHTTPNotFound
}
func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server
import (
@@ -6,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/time/rate"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"io"
@@ -345,8 +348,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Nil(t, u.Tier)
require.Equal(t, "", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users!
@@ -362,8 +365,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval)
require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)
require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages)
@@ -473,8 +476,8 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
billing := &user.Billing{
StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(456, 0),
}
@@ -517,10 +520,10 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due"
require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month"
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due"
require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month"
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
// Verify that reservations were deleted
r, err := s.userManager.Reservations("phil")
@@ -580,8 +583,8 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{
StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(0, 0),
}))
@@ -598,7 +601,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, u.Tier)
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())

View File

@@ -23,7 +23,6 @@ import (
"testing"
"time"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
@@ -281,30 +280,6 @@ func TestServer_WebEnabled(t *testing.T) {
rr = request(t, s2, "GET", "/app.html", "", nil)
require.Equal(t, 200, rr.Code)
}
func TestServer_WebPushEnabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_PublishLargeMessage(t *testing.T) {
c := newTestConfig(t)
c.AttachmentCacheDir = "" // Disable attachments
@@ -3257,17 +3232,6 @@ func newTestConfigWithAuthFile(t *testing.T) *Config {
return conf
}
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}
func newTestServer(t *testing.T, config *Config) *Server {
server, err := New(config)
require.Nil(t, err)

View File

@@ -1,3 +1,5 @@
//go:build !nowebpush
package server
import (
@@ -13,6 +15,10 @@ import (
)
const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = true
webPushTopicSubscribeLimit = 50
)

View File

@@ -0,0 +1,29 @@
//go:build nowebpush
package server
import (
"net/http"
)
const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = false
)
func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
// Nothing to see here
}
func (s *Server) pruneAndNotifyWebPushSubscriptions() {
// Nothing to see here
}

View File

@@ -1,8 +1,11 @@
//go:build !nowebpush
package server
import (
"encoding/json"
"fmt"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
@@ -10,6 +13,7 @@ import (
"net/http"
"net/http/httptest"
"net/netip"
"path/filepath"
"strings"
"sync/atomic"
"testing"
@@ -20,6 +24,28 @@ const (
testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
)
func TestServer_WebPush_Enabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_WebPush_Disabled(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
@@ -254,3 +280,14 @@ func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLen
require.Nil(t, err)
require.Len(t, subs, expectedLength)
}
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}

View File

@@ -7,9 +7,9 @@ import (
"errors"
"fmt"
"github.com/mattn/go-sqlite3"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/util"
"net/netip"
"path/filepath"
@@ -1242,12 +1242,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
Calls: calls,
},
Billing: &Billing{
StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: stripe.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: payments.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
},
Deleted: deleted.Valid,
}

View File

@@ -4,7 +4,6 @@ import (
"database/sql"
"fmt"
"github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/util"
"net/netip"
@@ -164,8 +163,8 @@ func TestManager_AddUser_And_Query(t *testing.T) {
require.Nil(t, a.ChangeBilling("user", &Billing{
StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123",
StripeSubscriptionStatus: stripe.SubscriptionStatusActive,
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
StripeSubscriptionStatus: "active",
StripeSubscriptionInterval: "month",
StripeSubscriptionPaidUntil: time.Now().Add(time.Hour),
StripeSubscriptionCancelAt: time.Unix(0, 0),
}))

View File

@@ -2,8 +2,8 @@ package user
import (
"errors"
"github.com/stripe/stripe-go/v74"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"net/netip"
"strings"
"time"
@@ -140,8 +140,8 @@ type Stats struct {
type Billing struct {
StripeCustomerID string
StripeSubscriptionID string
StripeSubscriptionStatus stripe.SubscriptionStatus
StripeSubscriptionInterval stripe.PriceRecurringInterval
StripeSubscriptionStatus payments.SubscriptionStatus
StripeSubscriptionInterval payments.PriceRecurringInterval
StripeSubscriptionPaidUntil time.Time
StripeSubscriptionCancelAt time.Time
}