Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ enum class Methods(val method: String) {
//Device
RemoveDevice("removeDevice"),
AttachReferralCode("attachReferralCode"),
AttachReferralCodeV2("attachReferralCodeV2"),

// Ad blocking
IsBlockAdsEnabled("isBlockAdsEnabled"),
Expand Down Expand Up @@ -439,7 +440,8 @@ class MethodHandler : FlutterPlugin,
val map = call.arguments as Map<*, *>
val subscriptionData = Mobile.stripeSubscription(
map["email"] as String,
map["planId"] as String
map["planId"] as String,
map["couponCode"] as? String ?: ""
)
withContext(Dispatchers.Main) {
success(subscriptionData)
Expand All @@ -460,7 +462,8 @@ class MethodHandler : FlutterPlugin,
val map = call.arguments as Map<*, *>
val subscriptionData = Mobile.acknowledgeGooglePurchase(
map["purchaseToken"] as String,
map["planId"] as String
map["planId"] as String,
map["couponCode"] as? String ?: ""
)
withContext(Dispatchers.Main) {
success(subscriptionData.toByteArray(Charsets.UTF_8))
Expand Down Expand Up @@ -507,7 +510,8 @@ class MethodHandler : FlutterPlugin,
map["provider"] as String,
map["planId"] as String,
map["email"] as String,
idempotencyKey
idempotencyKey,
map["couponCode"] as? String ?: ""
)
withContext(Dispatchers.Main) {
success(url)
Expand Down Expand Up @@ -831,6 +835,27 @@ class MethodHandler : FlutterPlugin,
}
}

Methods.AttachReferralCodeV2.method -> {
scope.launch {
result.runCatching {
val code = call.argument<String>("code") ?: error("Missing code")
val distributionChannel =
call.argument<String>("distributionChannel") ?: ""
val response =
Mobile.referralAttachmentV2(code, distributionChannel)
withContext(Dispatchers.Main) {
success(response)
}
}.onFailure { e ->
result.error(
"AttachReferralCodeV2",
e.localizedMessage ?: "Please try again",
e
)
}
}
}

// Ad blocking
Methods.SetBlockAdsEnabled.method -> {
scope.handleResult(result, "set_block_ads_enabled") {
Expand Down
17 changes: 16 additions & 1 deletion assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,9 @@ msgstr "Server alias cannot be empty"
msgid "referral_code_applied"
msgstr "Your referral code has been applied!"

msgid "discount_applied"
msgstr "%s discount applied"

msgid "referral_message_1m"
msgstr "+ 15 additional days free"

Expand Down Expand Up @@ -1423,7 +1426,7 @@ msgid "daily_data_cap_reached_message"
msgstr "Speed reduced to 128 kb/sec - Resets at %s."

msgid "subscription_renewal_info"
msgstr "Payment is charged to your Apple ID at purchase. Subscriptions renew automatically unless canceled at least 24 hours before the end of the current period. Manage or cancel in App Store Settings."
msgstr "Payment is charged to your Apple ID at purchase. Subscriptions renew automatically unless canceled at least 24 hours before the end of the current period."

msgid "failed_to_update_routing_mode"
msgstr "Failed to update routing mode. Please try again."
Expand Down Expand Up @@ -1653,6 +1656,18 @@ msgstr "The VPN needs to be off before you can refresh your configuration. Turn
msgid "configuration_message"
msgstr "Your old configuration has been cleared. Turn the VPN on to automatically download the latest."

msgid "promo_referral_code"
msgstr "Promo / Referral Code"

msgid "promo_code"
msgstr "Promo Code"

msgid "promo_code_with"
msgstr "Promo Code (%s)"

msgid "order_total"
msgstr "Order Total"

msgid "smart_routing_mode_description"
msgstr "Smart Location picks the fastest server and switches automatically as it finds better routes."

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/getlantern/lantern

go 1.26.2

// replace github.com/getlantern/radiance => ../radiance
//replace github.com/getlantern/radiance => ../radiance

// replace github.com/getlantern/lantern-server-provisioner => ../lantern-server-provisioner

Expand All @@ -25,7 +25,7 @@ replace github.com/quic-go/qpack => github.com/quic-go/qpack v0.5.1
require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20260706185153-bce252479f03
github.com/getlantern/radiance v0.0.0-20260710102102-8616f916e1e3
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.45.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmIqQ+JnjiYNm+UyUDalK3WUmVyecFwmV5g=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/radiance v0.0.0-20260706185153-bce252479f03 h1:yKLQK9wMXlghg4ksSDenZVYbUBphblutaFMomS6ZqQU=
github.com/getlantern/radiance v0.0.0-20260706185153-bce252479f03/go.mod h1:p6wcyzCrsFfrdrLNuQirB0ugeg7OjdFlZmBgNTu4D1w=
github.com/getlantern/radiance v0.0.0-20260710102102-8616f916e1e3 h1:YCc0+wtDuFJOHWinASKi+yg7shPyP5FsZwUJ0+aV888=
github.com/getlantern/radiance v0.0.0-20260710102102-8616f916e1e3/go.mod h1:rXbNFXzQvbnlIlaIF/6EJTwxK9DrAyKowkpUq/5udkI=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf h1:KxiMF+oG0rTtuBi7GiIaHfccYOf69rLJ/VnO5myoYc4=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20260327040646-21845dda05cb h1:c5YM7b3a4r2J8Eh89KkI6M/iTFe6Bi+b8AJlfkKdFq4=
Expand Down
35 changes: 32 additions & 3 deletions ios/Runner/Handlers/MethodHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ class MethodHandler {
)
return
}
self.acknowledgeInAppPurchase(token: token, planId: planId, result: result)
let couponCode = map["couponCode"] as? String ?? ""
self.acknowledgeInAppPurchase(
token: token, planId: planId, couponCode: couponCode, result: result)

case "restoreInAppPurchase":
guard
Expand Down Expand Up @@ -171,6 +173,13 @@ class MethodHandler {
let code = call.arguments as? String ?? ""
self.referralAttach(result: result, code: code)

case "attachReferralCodeV2":
let data = call.arguments as? [String: Any] ?? [:]
let code = data["code"] as? String ?? ""
let distributionChannel = data["distributionChannel"] as? String ?? ""
self.referralAttachV2(
result: result, code: code, distributionChannel: distributionChannel)

// Private server methods
case "digitalOcean":
self.digitalOcean(result: result)
Expand Down Expand Up @@ -564,10 +573,12 @@ class MethodHandler {
}
}

func acknowledgeInAppPurchase(token: String, planId: String, result: @escaping FlutterResult) {
func acknowledgeInAppPurchase(
token: String, planId: String, couponCode: String, result: @escaping FlutterResult
) {
Task {
var error: NSError?
let json = MobileAcknowledgeApplePurchase(token, planId, &error)
let json = MobileAcknowledgeApplePurchase(token, planId, couponCode, &error)
if let error {
await self.handleFlutterError(error, result: result, code: "ACKNOWLEDGE_FAILED")
return
Expand Down Expand Up @@ -811,6 +822,24 @@ class MethodHandler {
}
}

func referralAttachV2(
result: @escaping FlutterResult, code: String, distributionChannel: String
) {
Task {
var error: NSError?
let json = MobileReferralAttachmentV2(code, distributionChannel, &error)
if let error {
appLogger.error("Failed to attach referral code v2: \(error.localizedDescription)")
await self.handleFlutterError(error, result: result, code: "ATTACH_REFERRAL_CODE_V2_FAILED")
return
}
await MainActor.run {
appLogger.info("Referral code attached successfully (v2).")
result(json)
}
}
}

// MARK: - Private server methods

func digitalOcean(result: @escaping FlutterResult) {
Expand Down
44 changes: 33 additions & 11 deletions lantern-core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type App interface {
UpdateConfig() error
ClearTunnelCache() error
ReferralAttachment(referralCode string) (bool, error)
ReferralAttachmentV2(referralCode, channel string) ([]byte, error)
UpdateLocale(locale string) error
UpdateTelemetryConsent(consent bool) error
IsTelemetryEnabled() bool
Expand Down Expand Up @@ -120,17 +121,17 @@ type PrivateServer interface {
}

type Payment interface {
StripeSubscription(email, planID string) (string, error)
StripeSubscription(email, planID, couponCode string) (string, error)
Plans(channel string) (string, error)
StripeBillingPortalUrl() (string, error)
AcknowledgeGooglePurchase(purchaseToken, planId string) (string, error)
AcknowledgeApplePurchase(receipt, planII string) (string, error)
AcknowledgeGooglePurchase(purchaseToken, planId, couponCode string) (string, error)
AcknowledgeApplePurchase(receipt, planII, couponCode string) (string, error)
RestoreGooglePlayPurchase(purchaseToken string) (string, error)
RestoreApplePurchase(receipt string) (string, error)
PaymentRedirect(provider, planID, email, idempotencyKey string) (string, error)
PaymentRedirect(provider, planID, email, idempotencyKey, couponCode string) (string, error)
ActivationCode(email, resellerCode string) error
SubscriptionPaymentRedirectURL(redirectBody account.PaymentRedirectData) (string, error)
StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey string) (string, error)
StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey, couponCode string) (string, error)
}

type SplitTunnel interface {
Expand Down Expand Up @@ -863,12 +864,22 @@ func (lc *LanternCore) ReferralAttachment(referralCode string) (bool, error) {
return lc.client.ReferralAttach(lc.ctx, referralCode)
}

// ReferralAttachmentV2 attaches a referral code and returns the resulting
// plans, providers, code, and discount marshalled as JSON.
func (lc *LanternCore) ReferralAttachmentV2(referralCode, channel string) ([]byte, error) {
resp, err := lc.client.ReferralAttachV2(lc.ctx, referralCode, channel)
if err != nil {
return nil, err
}
return json.Marshal(resp)
}

/////////////////
// Payments //
/////////////////

func (lc *LanternCore) StripeSubscription(email, planID string) (string, error) {
return lc.client.NewStripeSubscription(lc.ctx, email, planID)
func (lc *LanternCore) StripeSubscription(email, planID, couponCode string) (string, error) {
return lc.client.NewStripeSubscription(lc.ctx, email, planID, couponCode)
}

func (lc *LanternCore) Plans(channel string) (string, error) {
Expand All @@ -879,19 +890,28 @@ func (lc *LanternCore) StripeBillingPortalUrl() (string, error) {
return lc.client.StripeBillingPortalURL(lc.ctx)
}

func (lc *LanternCore) AcknowledgeGooglePurchase(purchaseToken, planId string) (string, error) {
func (lc *LanternCore) AcknowledgeGooglePurchase(purchaseToken, planId, couponCode string) (string, error) {
params := map[string]string{
"purchaseToken": purchaseToken,
"planId": planId,
}
// Affiliate/referral attribution: forward the applied code so the backend
// can credit the purchase to the affiliate. Google Play already applies the
// price discount via the offer SKU; this only carries attribution.
if couponCode != "" {
params["couponCode"] = couponCode
}
return lc.client.VerifySubscription(lc.ctx, account.GoogleService, params)
}

func (lc *LanternCore) AcknowledgeApplePurchase(receipt, planII string) (string, error) {
func (lc *LanternCore) AcknowledgeApplePurchase(receipt, planII, couponCode string) (string, error) {
params := map[string]string{
"receipt": receipt,
"planId": planII,
}
if couponCode != "" {
params["couponCode"] = couponCode
}
return lc.client.VerifySubscription(lc.ctx, account.AppleService, params)
}

Expand Down Expand Up @@ -931,7 +951,7 @@ func (lc *LanternCore) SubscriptionPaymentRedirectURL(redirectBody account.Payme
return lc.client.SubscriptionPaymentRedirectURL(lc.ctx, redirectBody)
}

func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey string) (string, error) {
func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey, couponCode string) (string, error) {
idempotencyKey, err := normalizePaymentRedirectIdempotencyKey(idempotencyKey)
if err != nil {
return "", err
Expand All @@ -944,11 +964,12 @@ func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planI
Email: email,
BillingType: account.SubscriptionType(subscriptionType),
IdempotencyKey: idempotencyKey,
CouponCode: couponCode,
}
return lc.SubscriptionPaymentRedirectURL(redirectBody)
}

func (lc *LanternCore) PaymentRedirect(provider, planId, email, idempotencyKey string) (string, error) {
func (lc *LanternCore) PaymentRedirect(provider, planId, email, idempotencyKey, couponCode string) (string, error) {
idempotencyKey, err := normalizePaymentRedirectIdempotencyKey(idempotencyKey)
if err != nil {
return "", err
Expand All @@ -960,6 +981,7 @@ func (lc *LanternCore) PaymentRedirect(provider, planId, email, idempotencyKey s
DeviceName: deviceName,
Email: email,
IdempotencyKey: idempotencyKey,
CouponCode: couponCode,
}
return lc.client.PaymentRedirect(lc.ctx, body)
}
Expand Down
30 changes: 26 additions & 4 deletions lantern-core/ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,17 +645,18 @@ func fetchUserData() *C.char {
// Fetch stipe subscription payment redirect link
//
//export stripeSubscriptionPaymentRedirect
func stripeSubscriptionPaymentRedirect(subType, _planId, _email, _idempotencyKey *C.char) *C.char {
func stripeSubscriptionPaymentRedirect(subType, _planId, _email, _idempotencyKey, _couponCode *C.char) *C.char {
subscriptionType := C.GoString(subType)
planID := C.GoString(_planId)
email := C.GoString(_email)
idempotencyKey := C.GoString(_idempotencyKey)
couponCode := C.GoString(_couponCode)
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
redirect, err := c.StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey)
redirect, err := c.StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey, couponCode)
if err != nil {
return SendError(err)
}
Expand All @@ -666,17 +667,18 @@ func stripeSubscriptionPaymentRedirect(subType, _planId, _email, _idempotencyKey
// Fetch payment redirect link for providers like alipay
//
//export paymentRedirect
func paymentRedirect(_plan, _provider, _email, _idempotencyKey *C.char) *C.char {
func paymentRedirect(_plan, _provider, _email, _idempotencyKey, _couponCode *C.char) *C.char {
plan := C.GoString(_plan)
provider := C.GoString(_provider)
email := C.GoString(_email)
idempotencyKey := C.GoString(_idempotencyKey)
couponCode := C.GoString(_couponCode)
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
redirect, err := c.PaymentRedirect(provider, plan, email, idempotencyKey)
redirect, err := c.PaymentRedirect(provider, plan, email, idempotencyKey, couponCode)
if err != nil {
return SendError(err)
}
Expand Down Expand Up @@ -910,6 +912,26 @@ func verifyPassword(_email, _password *C.char) *C.char {
})
}

// referralAttachmentV2 attaches a referral code to the user's account and
// returns the resulting plans, providers, code, and discount as JSON.
//
//export referralAttachmentV2
func referralAttachmentV2(_referralCode, _channel *C.char) *C.char {
referralCode := C.GoString(_referralCode)
channel := C.GoString(_channel)
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
bytes, err := c.ReferralAttachmentV2(referralCode, channel)
if err != nil {
return SendError(err)
}
return C.CString(string(bytes))
})
}

// startChangeEmail initiates the process of changing the user's email address.
//
//export startChangeEmail
Expand Down
Loading
Loading