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 @@ -467,10 +467,15 @@ class MethodHandler : FlutterPlugin,
scope.launch {
result.runCatching {
val map = call.arguments as Map<*, *>
val idempotencyKey = map["idempotencyKey"] as? String
if (idempotencyKey.isNullOrBlank()) {
throw IllegalArgumentException("Payment redirect idempotency key is required")
}
val url = Mobile.paymentRedirect(
map["provider"] as String,
map["planId"] as String,
map["email"] as String
map["email"] as String,
idempotencyKey
)
withContext(Dispatchers.Main) {
success(url)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20260506180721-4445a2040ede
github.com/getlantern/radiance v0.0.0-20260506192624-6309530dece6
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.41.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmI
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/radiance v0.0.0-20260506180721-4445a2040ede h1:RCf7U3JiNDMuhbS/lPp4qP6CbA2xmXBXEVk4gvRMysM=
github.com/getlantern/radiance v0.0.0-20260506180721-4445a2040ede/go.mod h1:RG9PNWlBmS4HPFf6JTQ9CnsCD98zy75vvm7PnBIsdQI=
github.com/getlantern/radiance v0.0.0-20260506192624-6309530dece6 h1:8139ZlUwUemO0whyBaGsmRP9kzzIyDENrEmwJsRQIqY=
github.com/getlantern/radiance v0.0.0-20260506192624-6309530dece6/go.mod h1:RG9PNWlBmS4HPFf6JTQ9CnsCD98zy75vvm7PnBIsdQI=
github.com/getlantern/samizdat v0.0.3-0.20260327203406-ef7323341974 h1:k+/qNo5YNO+8M8LVUp6G5Evm1OQdEs3Z4ye8top4AhI=
github.com/getlantern/samizdat v0.0.3-0.20260327203406-ef7323341974/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20260327040646-21845dda05cb h1:c5YM7b3a4r2J8Eh89KkI6M/iTFe6Bi+b8AJlfkKdFq4=
Expand Down
44 changes: 31 additions & 13 deletions lantern-core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ type Payment interface {
StripeBillingPortalUrl() (string, error)
AcknowledgeGooglePurchase(purchaseToken, planId string) (string, error)
AcknowledgeApplePurchase(receipt, planII string) (string, error)
PaymentRedirect(provider, planID, email string) (string, error)
PaymentRedirect(provider, planID, email, idempotencyKey string) (string, error)
ActivationCode(email, resellerCode string) error
SubscriptionPaymentRedirectURL(redirectBody account.PaymentRedirectData) (string, error)
StripeSubscriptionPaymentRedirect(subscriptionType, planID, email string) (string, error)
StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey string) (string, error)
}

type SplitTunnel interface {
Expand Down Expand Up @@ -866,29 +866,47 @@ func (lc *LanternCore) SubscriptionPaymentRedirectURL(redirectBody account.Payme
return lc.client.SubscriptionPaymentRedirectURL(lc.ctx, redirectBody)
}

func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email string) (string, error) {
func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey string) (string, error) {
idempotencyKey, err := normalizePaymentRedirectIdempotencyKey(idempotencyKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atavism Do you think we should just create the idempotencyKey key here, since generation is basically the same, so we don't have to pass it through all the platforms? Is there any issue with that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jigar-f I put the key generation in Flutter to tie the key to the user’s checkout attempt. If core creates a new key every time PaymentRedirect is called, two duplicate calls could still become two different payment requests. Do you think the extra platform plumbing is fine given this context?

if err != nil {
return "", err
}
deviceID := lc.MyDeviceId()
redirectBody := account.PaymentRedirectData{
Provider: "stripe",
Plan: planID,
DeviceName: deviceID,
Email: email,
BillingType: account.SubscriptionType(subscriptionType),
Provider: "stripe",
Plan: planID,
DeviceName: deviceID,
Email: email,
BillingType: account.SubscriptionType(subscriptionType),
IdempotencyKey: idempotencyKey,
}
Comment thread
atavism marked this conversation as resolved.
return lc.SubscriptionPaymentRedirectURL(redirectBody)
}

func (lc *LanternCore) PaymentRedirect(provider, planId, email string) (string, error) {
func (lc *LanternCore) PaymentRedirect(provider, planId, email, idempotencyKey string) (string, error) {
idempotencyKey, err := normalizePaymentRedirectIdempotencyKey(idempotencyKey)
if err != nil {
return "", err
}
deviceName := lc.MyDeviceId()
body := account.PaymentRedirectData{
Provider: provider,
Plan: planId,
DeviceName: deviceName,
Email: email,
Provider: provider,
Plan: planId,
DeviceName: deviceName,
Email: email,
IdempotencyKey: idempotencyKey,
}
Comment thread
atavism marked this conversation as resolved.
return lc.client.PaymentRedirect(lc.ctx, body)
}

func normalizePaymentRedirectIdempotencyKey(idempotencyKey string) (string, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if idempotencyKey == "" {
return "", fmt.Errorf("payment redirect idempotency key is required")
}
return idempotencyKey, nil
}

func (lc *LanternCore) ActivationCode(email, resellerCode string) error {
purchase, err := lc.client.ActivationCode(lc.ctx, email, resellerCode)
if err != nil {
Expand Down
10 changes: 6 additions & 4 deletions lantern-core/ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,16 +633,17 @@ func fetchUserData() *C.char {
// Fetch stipe subscription payment redirect link
//
//export stripeSubscriptionPaymentRedirect
func stripeSubscriptionPaymentRedirect(subType, _planId, _email *C.char) *C.char {
func stripeSubscriptionPaymentRedirect(subType, _planId, _email, _idempotencyKey *C.char) *C.char {
subscriptionType := C.GoString(subType)
planID := C.GoString(_planId)
email := C.GoString(_email)
idempotencyKey := C.GoString(_idempotencyKey)
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
redirect, err := c.StripeSubscriptionPaymentRedirect(subscriptionType, planID, email)
redirect, err := c.StripeSubscriptionPaymentRedirect(subscriptionType, planID, email, idempotencyKey)
if err != nil {
return SendError(err)
}
Expand All @@ -653,16 +654,17 @@ func stripeSubscriptionPaymentRedirect(subType, _planId, _email *C.char) *C.char
// Fetch payment redirect link for providers like alipay
//
//export paymentRedirect
func paymentRedirect(_plan, _provider, _email *C.char) *C.char {
func paymentRedirect(_plan, _provider, _email, _idempotencyKey *C.char) *C.char {
plan := C.GoString(_plan)
provider := C.GoString(_provider)
email := C.GoString(_email)
idempotencyKey := C.GoString(_idempotencyKey)
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
redirect, err := c.PaymentRedirect(provider, plan, email)
redirect, err := c.PaymentRedirect(provider, plan, email, idempotencyKey)
if err != nil {
return SendError(err)
}
Expand Down
11 changes: 6 additions & 5 deletions lantern-core/mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,17 +555,18 @@ func AcknowledgeApplePurchase(receipt, planII string) (string, error) {
})
}

func PaymentRedirect(provider, planId, email string) (string, error) {
return withCoreR(func(c lanterncore.Core) (string, error) { return c.PaymentRedirect(provider, planId, email) })

func PaymentRedirect(provider, planId, email, idempotencyKey string) (string, error) {
return withCoreR(func(c lanterncore.Core) (string, error) {
return c.PaymentRedirect(provider, planId, email, idempotencyKey)
})
}

// /This is specifically for stripe subscriptions that require a redirect to complete the payment
// This is only used for macos
func StripeSubscriptionPaymentRedirect(subType, planId, email string) (string, error) {
func StripeSubscriptionPaymentRedirect(subType, planId, email, idempotencyKey string) (string, error) {
slog.Debug("stripeSubscriptionPaymentRedirect called")
return withCoreR(func(c lanterncore.Core) (string, error) {
return c.StripeSubscriptionPaymentRedirect(subType, planId, email)
return c.StripeSubscriptionPaymentRedirect(subType, planId, email, idempotencyKey)
})
}

Expand Down
8 changes: 8 additions & 0 deletions lib/core/common/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ String generatePassword() {
).join();
}

String generatePaymentRedirectIdempotencyKey() {
final random = Random.secure();
return List.generate(
16,
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
).join();
}

bool isStoreVersion() {
if (!PlatformUtils.isMobile) {
return false;
Expand Down
10 changes: 9 additions & 1 deletion lib/features/plans/provider/payment_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ class PaymentNotifier extends _$PaymentNotifier {
String planId,
String email,
) async {
final idempotencyKey = generatePaymentRedirectIdempotencyKey();
return ref
.read(lanternServiceProvider)
.stipeSubscriptionPaymentRedirect(
type: type,
planId: planId,
email: email,
idempotencyKey: idempotencyKey,
);
}

Expand All @@ -77,9 +79,15 @@ class PaymentNotifier extends _$PaymentNotifier {
required String planId,
required String email,
}) async {
final idempotencyKey = generatePaymentRedirectIdempotencyKey();
return ref
.read(lanternServiceProvider)
.paymentRedirect(provider: provider, planId: planId, email: email);
.paymentRedirect(
provider: provider,
planId: planId,
email: email,
idempotencyKey: idempotencyKey,
);
}

Future<Either<Failure, String?>> startUpgradeFlow({
Expand Down
12 changes: 7 additions & 5 deletions lib/lantern/lantern_core_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,19 @@ abstract class LanternCoreService {

Future<Either<Failure, String>> getOAuthProvider();


///Payments methods
Future<Either<Failure, String>> stipeSubscriptionPaymentRedirect(
{required BillingType type,
required String planId,
required String email});
Future<Either<Failure, String>> stipeSubscriptionPaymentRedirect({
required BillingType type,
required String planId,
required String email,
required String idempotencyKey,
});

Future<Either<Failure, String>> paymentRedirect({
required String provider,
required String planId,
required String email,
required String idempotencyKey,
});

// this is used for stripe subscription
Expand Down
4 changes: 4 additions & 0 deletions lib/lantern/lantern_ffi_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ class LanternFFIService implements LanternCoreService {
required BillingType type,
required String planId,
required String email,
required String idempotencyKey,
}) async {
try {
appLogger.debug('Starting Stripe Subscription Payment Redirect');
Expand All @@ -747,6 +748,7 @@ class LanternFFIService implements LanternCoreService {
type.name.toCharPtr,
planId.toCharPtr,
email.toCharPtr,
idempotencyKey.toCharPtr,
)
.toDartString();
});
Expand Down Expand Up @@ -889,6 +891,7 @@ class LanternFFIService implements LanternCoreService {
required String provider,
required String planId,
required String email,
required String idempotencyKey,
}) async {
try {
final result = await runInBackground<String>(() async {
Expand All @@ -897,6 +900,7 @@ class LanternFFIService implements LanternCoreService {
planId.toCharPtr,
provider.toCharPtr,
email.toCharPtr,
idempotencyKey.toCharPtr,
)
.toDartString();
});
Expand Down
15 changes: 13 additions & 2 deletions lib/lantern/lantern_generated_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5669,8 +5669,14 @@ class LanternBindings {
ffi.Pointer<ffi.Char> subType,
ffi.Pointer<ffi.Char> _planId,
ffi.Pointer<ffi.Char> _email,
ffi.Pointer<ffi.Char> _idempotencyKey,
) {
return _stripeSubscriptionPaymentRedirect(subType, _planId, _email);
return _stripeSubscriptionPaymentRedirect(
subType,
_planId,
_email,
_idempotencyKey,
);
}

late final _stripeSubscriptionPaymentRedirectPtr =
Expand All @@ -5680,6 +5686,7 @@ class LanternBindings {
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>
>('stripeSubscriptionPaymentRedirect');
Expand All @@ -5690,15 +5697,17 @@ class LanternBindings {
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>();

ffi.Pointer<ffi.Char> paymentRedirect(
ffi.Pointer<ffi.Char> _plan,
ffi.Pointer<ffi.Char> _provider,
ffi.Pointer<ffi.Char> _email,
ffi.Pointer<ffi.Char> _idempotencyKey,
) {
return _paymentRedirect(_plan, _provider, _email);
return _paymentRedirect(_plan, _provider, _email, _idempotencyKey);
}

late final _paymentRedirectPtr =
Expand All @@ -5708,6 +5717,7 @@ class LanternBindings {
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>
>('paymentRedirect');
Expand All @@ -5717,6 +5727,7 @@ class LanternBindings {
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>();

Expand Down
16 changes: 14 additions & 2 deletions lib/lantern/lantern_platform_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ class LanternPlatformService implements LanternCoreService {
required BillingType type,
required String planId,
required String email,
required String idempotencyKey,
}) async {
if (!PlatformUtils.isMacOS) {
return left(
Expand All @@ -663,7 +664,12 @@ class LanternPlatformService implements LanternCoreService {
try {
final redirectUrl = await _methodChannel.invokeMethod<String>(
'stripeSubscriptionPaymentRedirect',
{"type": type.name, "planId": planId, "email": email},
{
"type": type.name,
"planId": planId,
"email": email,
"idempotencyKey": idempotencyKey,
},
);
return Right(redirectUrl!);
} catch (e) {
Expand Down Expand Up @@ -758,14 +764,20 @@ class LanternPlatformService implements LanternCoreService {
required String provider,
required String planId,
required String email,
required String idempotencyKey,
}) async {
if (PlatformUtils.isIOS) {
throw UnimplementedError("This not supported on IOS");
}
try {
final redirectUrl = await _methodChannel.invokeMethod<String>(
'paymentRedirect',
{'provider': provider, 'planId': planId, 'email': email},
{
'provider': provider,
'planId': planId,
'email': email,
'idempotencyKey': idempotencyKey,
},
);
return Right(redirectUrl!);
} catch (e, stackTrace) {
Expand Down
Loading
Loading