diff --git a/android/java/org/chromium/chrome/browser/billing/LinkSubscriptionUtils.java b/android/java/org/chromium/chrome/browser/billing/LinkSubscriptionUtils.java index 3c199073628..4dda7e5b148 100644 --- a/android/java/org/chromium/chrome/browser/billing/LinkSubscriptionUtils.java +++ b/android/java/org/chromium/chrome/browser/billing/LinkSubscriptionUtils.java @@ -20,7 +20,7 @@ public class LinkSubscriptionUtils { String braveAccountUrl = isLinkSubscriptionOnStaging() ? BRAVE_ACCOUNT_URL_STAGING : BRAVE_ACCOUNT_URL; String linkType = - (SubscriptionProduct.LEO == subscriptionProduct) ? "link-order" : "connect-receipt"; + (SubscriptionProduct.VPN == subscriptionProduct) ? "connect-receipt" : "link-order"; String baseUrl = "https://%s?intent=%s&product=%s&mtm_campaign=linking-android"; return String.format( baseUrl, diff --git a/android/java/org/chromium/chrome/browser/settings/BraveOriginPreferences.java b/android/java/org/chromium/chrome/browser/settings/BraveOriginPreferences.java index 6792426c785..e27dcb2e82e 100644 --- a/android/java/org/chromium/chrome/browser/settings/BraveOriginPreferences.java +++ b/android/java/org/chromium/chrome/browser/settings/BraveOriginPreferences.java @@ -63,6 +63,7 @@ public class BraveOriginPreferences extends BravePreferenceFragment private static final String PREF_WEB_DISCOVERY_PROJECT_SWITCH = "web_discovery_project_switch"; private static final String PREF_RESET_TO_DEFAULTS = "reset_to_defaults"; private static final String PREF_LINK_PURCHASE = "link_purchase"; + private static final String PREF_PURCHASE_SECTION = "origin_purchase_section"; private static final String PREF_BRAVE_ORIGIN = "brave_origin"; private final SettableMonotonicObservableSupplier mPageTitle = @@ -114,6 +115,14 @@ public class BraveOriginPreferences extends BravePreferenceFragment Preference linkPurchase = findPreference(PREF_LINK_PURCHASE); if (linkPurchase != null) { + boolean isLinked = BraveOriginSubscriptionPrefs.isSubscriptionLinked(profile); + linkPurchase.setVisible(!isLinked); + + Preference purchaseSection = findPreference(PREF_PURCHASE_SECTION); + if (purchaseSection != null) { + purchaseSection.setVisible(!isLinked); + } + linkPurchase.setOnPreferenceClickListener( preference -> { TabUtils.openURLWithBraveActivity( diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index c3ccce8df86..90ede7f13d4 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -258,6 +258,8 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #if BUILDFLAG(ENABLE_AI_CHAT) #include "brave/components/ai_chat/core/browser/android/ai_chat_iap_subscription_android.h" #endif +#include "brave/components/brave_origin/features.h" +#include "brave/components/brave_origin/origin_iap_subscription.h" #endif #if BUILDFLAG(ENABLE_TOR) @@ -435,6 +437,24 @@ void BindIAPSubscription( } #endif +#if BUILDFLAG(IS_ANDROID) +void BindOriginIAPSubscription( + content::RenderFrameHost* const frame_host, + mojo::PendingReceiver + receiver) { + const GURL& frame_host_url = frame_host->GetLastCommittedURL(); + if (!skus::IsSafeOrigin(frame_host_url)) { + return; + } + auto* context = frame_host->GetBrowserContext(); + auto* profile = Profile::FromBrowserContext(context); + mojo::MakeSelfOwnedReceiver( + std::make_unique( + profile->GetPrefs()), + std::move(receiver)); +} +#endif + #if BUILDFLAG(ENABLE_BRAVE_VPN) void MaybeBindBraveVpnImpl( content::RenderFrameHost* const frame_host, @@ -973,6 +993,13 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( #endif #endif +#if BUILDFLAG(IS_ANDROID) + if (base::FeatureList::IsEnabled(brave_origin::features::kBraveOrigin)) { + map->Add( + base::BindRepeating(&BindOriginIAPSubscription)); + } +#endif + #if BUILDFLAG(ENABLE_SPEEDREADER) && !BUILDFLAG(IS_ANDROID) content::RegisterWebUIControllerInterfaceBinder< speedreader::mojom::ToolbarFactory, SpeedreaderToolbarUI>(map); diff --git a/components/brave_mobile_subscription/renderer/android/BUILD.gn b/components/brave_mobile_subscription/renderer/android/BUILD.gn index 05b4ae16270..19391f64a1e 100644 --- a/components/brave_mobile_subscription/renderer/android/BUILD.gn +++ b/components/brave_mobile_subscription/renderer/android/BUILD.gn @@ -17,6 +17,8 @@ source_set("android") { "//base", "//brave/components/ai_chat/core/common", "//brave/components/ai_chat/core/common/mojom", + "//brave/components/brave_origin:features", + "//brave/components/brave_origin/mojom", "//brave/components/brave_vpn/common/buildflags", "//brave/components/skus/renderer", "//brave/gin:converter_specializations", @@ -47,6 +49,7 @@ source_set("browser_tests") { deps = [ "//brave/components/ai_chat/core/common", "//brave/components/brave_mobile_subscription/renderer/android", + "//brave/components/brave_origin:features", "//brave/components/brave_vpn/common/buildflags", "//brave/components/skus/common", "//chrome/common", diff --git a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.cc b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.cc index 673561fd0ff..dc0202328d4 100644 --- a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.cc +++ b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.cc @@ -15,6 +15,7 @@ #include "base/strings/strcat.h" #include "base/strings/utf_string_conversions.h" #include "brave/components/ai_chat/core/common/features.h" +#include "brave/components/brave_origin/features.h" #include "brave/components/skus/renderer/skus_utils.h" #include "brave/gin/converter_specializations.h" #include "build/build_config.h" @@ -40,8 +41,9 @@ inline constexpr char kIntentParamTestValue[] = "connect-receipt-test"; inline constexpr char kProductParamName[] = "product"; inline constexpr char kProductVPNParamValue[] = "vpn"; inline constexpr char kProductLeoParamValue[] = "leo"; -inline constexpr char kIntentParamValueLeo[] = "link-order"; -inline constexpr char kResultLandingPagePathLeo[] = "/order-link/"; +inline constexpr char kProductOriginParamValue[] = "origin"; +inline constexpr char kIntentParamValueLinkOrder[] = "link-order"; +inline constexpr char kResultLandingPagePath[] = "/order-link/"; } // namespace @@ -71,6 +73,15 @@ bool SubscriptionRenderFrameObserver::EnsureConnected() { } bound |= ai_chat_subscription_.is_bound(); } + + if (base::FeatureList::IsEnabled(brave_origin::features::kBraveOrigin) && + product_ == Product::kOrigin) { + if (!origin_subscription_.is_bound()) { + render_frame()->GetBrowserInterfaceBroker().GetInterface( + origin_subscription_.BindNewPipeAndPassReceiver()); + } + bound |= origin_subscription_.is_bound(); + } return bound; } @@ -117,6 +128,16 @@ void SubscriptionRenderFrameObserver::DidCreateScriptContext( weak_factory_.GetWeakPtr())); } } + } else if (product_ == Product::kOrigin) { + if (origin_subscription_.is_bound()) { + if (page_ == Page::kResultLandingPage) { + AddJavaScriptObjectToFrame(context); + } else if (page_ == Page::kInitialLandingPage) { + origin_subscription_->GetPurchaseTokenOrderId(base::BindOnce( + &SubscriptionRenderFrameObserver::OnGetPurchaseTokenOrderId, + weak_factory_.GetWeakPtr())); + } + } } } @@ -178,13 +199,17 @@ void SubscriptionRenderFrameObserver::SetLinkStatus( // where value 0 means it's not linked otherwise it's linked. // VPN uses a different way to detect that. // It uses Guardian backend call for that. - if (product_ != Product::kLeo || !ai_chat_subscription_.is_bound() || - status_dict.empty()) { + if (status_dict.empty()) { return; } - ai_chat_subscription_->SetLinkStatus( - status_dict.FindInt("status").value_or(0)); + if (product_ == Product::kLeo && ai_chat_subscription_.is_bound()) { + ai_chat_subscription_->SetLinkStatus( + status_dict.FindInt("status").value_or(0)); + } else if (product_ == Product::kOrigin && origin_subscription_.is_bound()) { + origin_subscription_->SetLinkStatus( + status_dict.FindInt("status").value_or(0)); + } } std::string SubscriptionRenderFrameObserver::GetPurchaseTokenJSString( @@ -198,6 +223,8 @@ std::string SubscriptionRenderFrameObserver::GetPurchaseTokenJSString( receipt_var_name = "braveVpn.receipt"; } else if (product_ == Product::kLeo) { receipt_var_name = "braveLeo.receipt"; + } else if (product_ == Product::kOrigin) { + receipt_var_name = "braveOrigin.receipt"; } return base::StrCat({"window.localStorage.setItem(\"", receipt_var_name, @@ -226,8 +253,14 @@ void SubscriptionRenderFrameObserver::OnGetPurchaseTokenOrderId( } auto* frame = render_frame(); if (frame && !order_id.empty() && !purchase_token.empty()) { + std::string_view order_id_key; + if (product_ == Product::kLeo) { + order_id_key = "braveLeo.orderId"; + } else if (product_ == Product::kOrigin) { + order_id_key = "braveOrigin.orderId"; + } frame->ExecuteJavaScript(base::UTF8ToUTF16(base::StrCat( - {"window.localStorage.setItem(\"braveLeo.orderId\", \"", order_id, + {"window.localStorage.setItem(\"", order_id_key, "\", \"", order_id, "\");", GetPurchaseTokenJSString(purchase_token)}))); } } @@ -274,23 +307,30 @@ bool SubscriptionRenderFrameObserver::IsAllowed() { product_ = Product::kVPN; } else if (product == kProductLeoParamValue) { product_ = Product::kLeo; - // We allow to inject linkResult object if intent value is empty and path is - // /order-link/ as https://account.brave.com?intent=link-order&product=leo - // gets redirected to https://account.brave.com/order-link/?product=leo for - // an actual linking where we should receive the result of linking + } else if (product == kProductOriginParamValue) { + product_ = Product::kOrigin; + } else { + product_ = std::nullopt; + } + + // For link-order products (Leo, Origin), we allow injecting the linkResult + // object if intent value is empty and path is /order-link/ as + // https://account.brave.com?intent=link-order&product= + // gets redirected to https://account.brave.com/order-link/?product= + // for an actual linking where we should receive the result of linking. + if (product_ == Product::kLeo || product_ == Product::kOrigin) { if (intent.empty()) { std::string_view path = current_url.has_path() ? current_url.path() : ""; - if (path == kResultLandingPagePathLeo) { + if (path == kResultLandingPagePath) { page_ = Page::kResultLandingPage; } } else { page_ = Page::kInitialLandingPage; } - } else { - product_ = std::nullopt; } + return (intent == kIntentParamValue || intent == kIntentParamTestValue || - intent == kIntentParamValueLeo || + intent == kIntentParamValueLinkOrder || (intent.empty() && page_.has_value() && page_ == Page::kResultLandingPage)) && product_.has_value(); diff --git a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h index 3cd64f083a6..a4e7a95f7b9 100644 --- a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h +++ b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h @@ -15,6 +15,7 @@ #include "base/values.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "brave/components/ai_chat/core/common/mojom/common.mojom.h" +#include "brave/components/brave_origin/mojom/brave_origin_settings.mojom.h" #include "brave/components/brave_vpn/common/buildflags/buildflags.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.h" @@ -54,7 +55,7 @@ class SubscriptionRenderFrameObserver : public content::RenderFrameObserver { IsAllowed); FRIEND_TEST_ALL_PREFIXES(SubscriptionRenderFrameObserverTest, ExtractParam); FRIEND_TEST_ALL_PREFIXES(SubscriptionRenderFrameObserverTest, IsValueAllowed); - enum class Product { kVPN = 0, kLeo = 1 }; + enum class Product { kVPN = 0, kLeo = 1, kOrigin = 2 }; enum class Page { kInitialLandingPage = 0, kResultLandingPage = 1 }; bool EnsureConnected(); @@ -88,6 +89,7 @@ class SubscriptionRenderFrameObserver : public content::RenderFrameObserver { mojo::Remote vpn_service_; #endif mojo::Remote ai_chat_subscription_; + mojo::Remote origin_subscription_; base::WeakPtrFactory weak_factory_{this}; }; diff --git a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer_browsertest.cc b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer_browsertest.cc index fe6d9efd587..3462356678f 100644 --- a/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer_browsertest.cc +++ b/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer_browsertest.cc @@ -7,6 +7,7 @@ #include "base/test/scoped_feature_list.h" #include "brave/components/ai_chat/core/common/buildflags/buildflags.h" +#include "brave/components/brave_origin/features.h" #include "brave/components/brave_vpn/common/buildflags/buildflags.h" #include "brave/components/brave_vpn/common/features.h" #include "brave/components/skus/common/features.h" @@ -31,6 +32,7 @@ class SubscriptionRenderFrameObserverBrowserTest #if BUILDFLAG(ENABLE_AI_CHAT) ai_chat::features::kAIChat, #endif + brave_origin::features::kBraveOrigin, }, {}); } @@ -81,6 +83,13 @@ TEST_F(SubscriptionRenderFrameObserverBrowserTest, IsAllowed) { EXPECT_TRUE(observer.IsAllowed()); + // Origin + LoadHTMLWithUrlOverride( + R"()", + "https://account.brave.com/?intent=link-order&product=origin"); + + EXPECT_TRUE(observer.IsAllowed()); + // http LoadHTMLWithUrlOverride( R"()", diff --git a/components/brave_origin/BUILD.gn b/components/brave_origin/BUILD.gn index fb7690bc090..71f1496b806 100644 --- a/components/brave_origin/BUILD.gn +++ b/components/brave_origin/BUILD.gn @@ -45,6 +45,13 @@ static_library("brave_origin") { "profile_id.h", ] + if (is_android) { + sources += [ + "origin_iap_subscription.cc", + "origin_iap_subscription.h", + ] + } + friend = [ ":unit_tests" ] public_deps = [ diff --git a/components/brave_origin/mojom/brave_origin_settings.mojom b/components/brave_origin/mojom/brave_origin_settings.mojom index 468223e8a81..59113114325 100644 --- a/components/brave_origin/mojom/brave_origin_settings.mojom +++ b/components/brave_origin/mojom/brave_origin_settings.mojom @@ -25,3 +25,11 @@ interface BraveOriginSettingsHandler { // Set the value of a Brave Origin controlled policy SetPolicyValue(string policy_key, bool value) => (bool success); }; + +// Interface for interaction between SubscriptionRenderFrameObserver +// (renderer process) and browser process for Origin subscription linking. +[EnableIf=is_android] +interface OriginIAPSubscription { + GetPurchaseTokenOrderId() => (string token, string order_id); + SetLinkStatus(int32 status); +}; diff --git a/components/brave_origin/origin_iap_subscription.cc b/components/brave_origin/origin_iap_subscription.cc new file mode 100644 index 00000000000..f50bf17b482 --- /dev/null +++ b/components/brave_origin/origin_iap_subscription.cc @@ -0,0 +1,75 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_origin/origin_iap_subscription.h" + +#include +#include + +#include "base/base64.h" +#include "base/json/json_writer.h" +#include "brave/components/brave_origin/pref_names.h" +#include "components/prefs/pref_service.h" + +namespace { + +inline constexpr char kDefaultPackage[] = "com.brave.browser"; +inline constexpr char kProductId[] = "brave.origin.perpetual"; + +} // namespace + +namespace brave_origin { + +OriginIAPSubscription::OriginIAPSubscription(PrefService* prefs) + : prefs_(prefs) {} + +OriginIAPSubscription::~OriginIAPSubscription() = default; + +void OriginIAPSubscription::GetPurchaseTokenOrderId( + GetPurchaseTokenOrderIdCallback callback) { + std::string order_id_string; + std::string purchase_token_string; + std::string package_string = kDefaultPackage; + std::string product_id_string = kProductId; + + auto* purchase_token = + prefs_->FindPreference(prefs::kBraveOriginPurchaseTokenAndroid); + if (purchase_token && !purchase_token->IsDefaultValue()) { + purchase_token_string = + prefs_->GetString(prefs::kBraveOriginPurchaseTokenAndroid); + } + + auto* package = prefs_->FindPreference(prefs::kBraveOriginPackageNameAndroid); + if (package && !package->IsDefaultValue()) { + package_string = prefs_->GetString(prefs::kBraveOriginPackageNameAndroid); + } + + auto* product_id = + prefs_->FindPreference(prefs::kBraveOriginProductIdAndroid); + if (product_id && !product_id->IsDefaultValue()) { + product_id_string = prefs_->GetString(prefs::kBraveOriginProductIdAndroid); + } + + auto* order_id = prefs_->FindPreference(prefs::kBraveOriginOrderIdAndroid); + if (order_id && !order_id->IsDefaultValue()) { + order_id_string = prefs_->GetString(prefs::kBraveOriginOrderIdAndroid); + } + + base::DictValue response; + response.Set("type", "android"); + response.Set("raw_receipt", purchase_token_string); + response.Set("package", package_string); + response.Set("subscription_id", product_id_string); + + std::string response_json; + base::JSONWriter::Write(response, &response_json); + std::move(callback).Run(base::Base64Encode(response_json), order_id_string); +} + +void OriginIAPSubscription::SetLinkStatus(int32_t status) { + prefs_->SetInteger(prefs::kBraveOriginSubscriptionLinkStatusAndroid, status); +} + +} // namespace brave_origin diff --git a/components/brave_origin/origin_iap_subscription.h b/components/brave_origin/origin_iap_subscription.h new file mode 100644 index 00000000000..e2de36ad2fb --- /dev/null +++ b/components/brave_origin/origin_iap_subscription.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_ORIGIN_ORIGIN_IAP_SUBSCRIPTION_H_ +#define BRAVE_COMPONENTS_BRAVE_ORIGIN_ORIGIN_IAP_SUBSCRIPTION_H_ + +#include "brave/components/brave_origin/mojom/brave_origin_settings.mojom.h" + +class PrefService; + +namespace brave_origin { + +// OriginIAPSubscription is responsible for interaction between +// SubscriptionRenderFrameObserver (renderer process) and browser process +// for Origin subscription linking on Android. +class OriginIAPSubscription final + : public brave_origin::mojom::OriginIAPSubscription { + public: + OriginIAPSubscription(const OriginIAPSubscription&) = delete; + OriginIAPSubscription& operator=(const OriginIAPSubscription&) = delete; + explicit OriginIAPSubscription(PrefService* prefs); + ~OriginIAPSubscription() override; + + // brave_origin::mojom::OriginIAPSubscription + void GetPurchaseTokenOrderId( + GetPurchaseTokenOrderIdCallback callback) override; + void SetLinkStatus(int32_t status) override; + + private: + raw_ptr prefs_ = nullptr; +}; + +} // namespace brave_origin + +#endif // BRAVE_COMPONENTS_BRAVE_ORIGIN_ORIGIN_IAP_SUBSCRIPTION_H_ diff --git a/renderer/DEPS b/renderer/DEPS index 78c68e3bdb9..7df9c3ed6b9 100644 --- a/renderer/DEPS +++ b/renderer/DEPS @@ -26,6 +26,7 @@ include_rules += [ "+brave/components/speedreader/renderer", "+brave/components/brave_vpn/common", "!brave/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h", + "+brave/components/brave_origin/features.h", "+brave/components/playlist/core/common", "+brave/components/playlist/content/renderer", "+brave/components/web_discovery/buildflags", diff --git a/renderer/brave_content_renderer_client.cc b/renderer/brave_content_renderer_client.cc index f3d36c15cfb..31d1fcf26a9 100644 --- a/renderer/brave_content_renderer_client.cc +++ b/renderer/brave_content_renderer_client.cc @@ -52,10 +52,12 @@ #if BUILDFLAG(ENABLE_BRAVE_VPN) #include "brave/components/brave_vpn/common/brave_vpn_utils.h" +#endif // BUILDFLAG(ENABLE_BRAVE_VPN) + #if BUILDFLAG(IS_ANDROID) #include "brave/components/brave_mobile_subscription/renderer/android/subscription_render_frame_observer.h" +#include "brave/components/brave_origin/features.h" #endif // BUILDFLAG(IS_ANDROID) -#endif // BUILDFLAG(ENABLE_BRAVE_VPN) #if BUILDFLAG(ENABLE_WIDEVINE) #include "media/base/key_system_info.h" @@ -191,6 +193,8 @@ void BraveContentRendererClient::RenderFrameCreated( should_create_subscription_observer |= ai_chat::features::IsAIChatHistoryEnabled(); #endif // BUILDFLAG(ENABLE_AI_CHAT) + should_create_subscription_observer |= + base::FeatureList::IsEnabled(brave_origin::features::kBraveOrigin); if (should_create_subscription_observer) { new brave_subscription::SubscriptionRenderFrameObserver( render_frame, content::ISOLATED_WORLD_ID_GLOBAL); diff --git a/renderer/sources.gni b/renderer/sources.gni index 687a7ab3549..09239a70df4 100644 --- a/renderer/sources.gni +++ b/renderer/sources.gni @@ -23,6 +23,7 @@ brave_chrome_renderer_sources = [ brave_chrome_renderer_deps = [ "//brave/common:mojo_bindings", "//brave/components/ai_chat/core/common/buildflags", + "//brave/components/brave_origin:features", "//brave/components/brave_search/common", "//brave/components/brave_search/renderer", "//brave/components/brave_shields/core/common",