Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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 @@ -82,6 +82,7 @@ enum class Methods(val method: String) {
//Device
RemoveDevice("removeDevice"),
AttachReferralCode("attachReferralCode"),
AttachReferralCodeV2("attachReferralCodeV2"),

// Ad blocking
IsBlockAdsEnabled("isBlockAdsEnabled"),
Expand Down Expand Up @@ -438,7 +439,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 @@ -459,7 +461,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 @@ -506,7 +509,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 @@ -830,6 +834,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
18 changes: 17 additions & 1 deletion assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,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 @@ -1444,7 +1447,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 @@ -1674,6 +1677,19 @@ 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"




10 changes: 5 additions & 5 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-20260617195940-99d3ff55fef1
github.com/getlantern/radiance v0.0.0-20260707114451-d023bb34053a
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.42.0
Expand Down Expand Up @@ -168,10 +168,10 @@ require (
github.com/getlantern/broflake v0.0.0-20260612203837-c4d1516de8dc // indirect
github.com/getlantern/common v1.2.1-0.20260326210434-cb69537aaf46 // indirect
github.com/getlantern/dnstt v0.0.0-20260603191204-3b860502c0ac // indirect
github.com/getlantern/domainfront v0.0.0-20260419161617-0bff0b2169f4 // indirect
github.com/getlantern/domainfront v0.0.0-20260625001429-518c0256669b // indirect
github.com/getlantern/keepcurrent v0.0.0-20260616120552-f204338b01a3 // indirect
github.com/getlantern/kindling v0.0.0-20260611181428-9a360f63ad5a // indirect
github.com/getlantern/lantern-box v0.0.93 // indirect
github.com/getlantern/kindling v0.0.0-20260625002640-7cdf7184420c // indirect
github.com/getlantern/lantern-box v0.0.95 // indirect
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 // indirect
github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 // indirect
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wd
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
github.com/getlantern/dnstt v0.0.0-20260603191204-3b860502c0ac h1:TMvkNgLVyIYAfu1dYrOuRPYMVY+cHEo1C0CYWhtSw2A=
github.com/getlantern/dnstt v0.0.0-20260603191204-3b860502c0ac/go.mod h1:0+wlia2hljruM7rhjzBMFhve011lGRO0OxjVSOcXBoQ=
github.com/getlantern/domainfront v0.0.0-20260419161617-0bff0b2169f4 h1:/Q9FJvKPyuXfH6tfA+C+t9/AbvGWs3Yp9iqI74FYvb4=
github.com/getlantern/domainfront v0.0.0-20260419161617-0bff0b2169f4/go.mod h1:nsdIvgenGUqPKnRFjkssbfxnV/WYWyC0c/t15qGym/A=
github.com/getlantern/domainfront v0.0.0-20260625001429-518c0256669b h1:cM+Cwgg6EN279knxvHc4vd3MyNOpZk/49TLlg2F78Rk=
github.com/getlantern/domainfront v0.0.0-20260625001429-518c0256669b/go.mod h1:eOcE66YcVTxLiASh77rngh83B9PQtm4gSr7ZxWnaxZU=
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
Expand All @@ -243,10 +243,10 @@ github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2y
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
github.com/getlantern/keepcurrent v0.0.0-20260616120552-f204338b01a3 h1:YPBbuyvdWv+YvXDqADVwjxM0DyABg2x4UgLVKU9McKI=
github.com/getlantern/keepcurrent v0.0.0-20260616120552-f204338b01a3/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
github.com/getlantern/kindling v0.0.0-20260611181428-9a360f63ad5a h1:w62TGPHwGqPDKq43pbwOkbvCofMY4Ldd5d17QBF9jnY=
github.com/getlantern/kindling v0.0.0-20260611181428-9a360f63ad5a/go.mod h1:9EDX+ZFoMVcd8M14LeE7ULn/TRuV9g5GijNUDgJyw2A=
github.com/getlantern/lantern-box v0.0.93 h1:R0c7NlT5M7fSp316KqKVw5KnDAk38xqbmQEue8Q+3pc=
github.com/getlantern/lantern-box v0.0.93/go.mod h1:yQhnLpnDY17+c/s+4WiiUhun3HtoHNj5NFb355ThKQk=
github.com/getlantern/kindling v0.0.0-20260625002640-7cdf7184420c h1:F4/LE+5zehr6AUfuJBab3iubscOOTdFZxwYldIpLgg8=
github.com/getlantern/kindling v0.0.0-20260625002640-7cdf7184420c/go.mod h1:rN6O8pIQ9nc7Gyvtf2GGSjiWEFIApGsRCHkgYz0h3Ak=
github.com/getlantern/lantern-box v0.0.95 h1:tsgjKQjWjPy3ZyCIYijT91W3XJY+8XENxb6mUogthI0=
github.com/getlantern/lantern-box v0.0.95/go.mod h1:yQhnLpnDY17+c/s+4WiiUhun3HtoHNj5NFb355ThKQk=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9/go.mod h1:s0VKrlJf/z+M0U8IKHFL2hfuflocRw3SINmMacrTlMA=
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 h1:grfGavAUp2E9w9ZoJuM3FyWyQ0sCJ64V4ZMKtZKRqTc=
Expand All @@ -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-20260617195940-99d3ff55fef1 h1:hs8vJYiizCTCPM8n6slL89vAVY/GAER6evgPwsrqzOg=
github.com/getlantern/radiance v0.0.0-20260617195940-99d3ff55fef1/go.mod h1:S4D2odUJoqxrEosHh6UUwFoUoY4PK55zluLUQgETFOY=
github.com/getlantern/radiance v0.0.0-20260707114451-d023bb34053a h1:5Wx+JLjWjunE52gU3G38yeH+znbQx54KGYDXNKwWZwc=
github.com/getlantern/radiance v0.0.0-20260707114451-d023bb34053a/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 @@ -165,6 +167,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 @@ -558,10 +567,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 @@ -776,6 +787,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 @@ -119,17 +120,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 @@ -858,12 +859,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 @@ -874,19 +885,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 @@ -926,7 +946,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 @@ -939,11 +959,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 @@ -955,6 +976,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
Loading