summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonathan <jonathan@mullvad.net>2023-10-16 19:44:21 +0200
committerJonathan <jonathan@mullvad.net>2023-10-16 19:44:21 +0200
commit9ea790f2e6ac363f59fceec5ae5a25aad6af06f4 (patch)
tree677680bd9b3fbb1156c7396ace8d57f97141001a
parentd32b6f81ceb8c383588678e8fbabe613fbac4142 (diff)
parentbf6d46a8274106150986665373bbb2127d72b83c (diff)
downloadmullvadvpn-9ea790f2e6ac363f59fceec5ae5a25aad6af06f4.tar.xz
mullvadvpn-9ea790f2e6ac363f59fceec5ae5a25aad6af06f4.zip
Merge branch 'feature-api-android-inapp-purchases'
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt10
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt18
-rw-r--r--mullvad-api/src/lib.rs62
-rw-r--r--mullvad-api/src/rest.rs32
-rw-r--r--mullvad-daemon/src/device/api.rs59
-rw-r--r--mullvad-daemon/src/device/mod.rs136
-rw-r--r--mullvad-daemon/src/device/service.rs44
-rw-r--r--mullvad-daemon/src/lib.rs52
-rw-r--r--mullvad-jni/src/classes.rs5
-rw-r--r--mullvad-jni/src/daemon_interface.rs22
-rw-r--r--mullvad-jni/src/lib.rs114
-rw-r--r--mullvad-types/src/account.rs17
16 files changed, 601 insertions, 6 deletions
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt
new file mode 100644
index 0000000000..8ae46a07a9
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize data class PlayPurchase(val productId: String, val purchaseToken: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt
new file mode 100644
index 0000000000..39aebabbe2
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class PlayPurchaseInitError : Parcelable {
+ // TODO: Add more errors here.
+ OtherError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt
new file mode 100644
index 0000000000..41407474af
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+sealed class PlayPurchaseInitResult : Parcelable {
+ @Parcelize data class Ok(val obfuscatedId: String) : PlayPurchaseInitResult()
+
+ @Parcelize data class Error(val error: PlayPurchaseInitError) : PlayPurchaseInitResult()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt
new file mode 100644
index 0000000000..b0434c22f9
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class PlayPurchaseVerifyError : Parcelable {
+ // TODO: Add more errors here.
+ OtherError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt
new file mode 100644
index 0000000000..7c5ee4d953
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+sealed class PlayPurchaseVerifyResult : Parcelable {
+ @Parcelize data object Ok : PlayPurchaseVerifyResult()
+
+ @Parcelize data class Error(val error: PlayPurchaseVerifyError) : PlayPurchaseVerifyResult()
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
index 6fa03978f7..ceb95a48b7 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
@@ -14,6 +14,9 @@ import net.mullvad.mullvadvpn.model.GeoIpLocation
import net.mullvad.mullvadvpn.model.GetAccountDataResult
import net.mullvad.mullvadvpn.model.LoginResult
import net.mullvad.mullvadvpn.model.ObfuscationSettings
+import net.mullvad.mullvadvpn.model.PlayPurchase
+import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
+import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
import net.mullvad.mullvadvpn.model.QuantumResistantState
import net.mullvad.mullvadvpn.model.RelayList
import net.mullvad.mullvadvpn.model.RelaySettingsUpdate
@@ -171,6 +174,14 @@ class MullvadDaemon(
return submitVoucher(daemonInterfaceAddress, voucher)
}
+ fun initPlayPurchase(): PlayPurchaseInitResult {
+ return initPlayPurchase(daemonInterfaceAddress)
+ }
+
+ fun verifyPlayPurchase(playPurchase: PlayPurchase): PlayPurchaseVerifyResult {
+ return verifyPlayPurchase(daemonInterfaceAddress, playPurchase)
+ }
+
fun updateRelaySettings(update: RelaySettingsUpdate) {
updateRelaySettings(daemonInterfaceAddress, update)
}
@@ -271,6 +282,13 @@ class MullvadDaemon(
voucher: String
): VoucherSubmissionResult
+ private external fun initPlayPurchase(daemonInterfaceAddress: Long): PlayPurchaseInitResult
+
+ private external fun verifyPlayPurchase(
+ daemonInterfaceAddress: Long,
+ playPurchase: PlayPurchase,
+ ): PlayPurchaseVerifyResult
+
private external fun updateRelaySettings(
daemonInterfaceAddress: Long,
update: RelaySettingsUpdate
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs
index 37faf5c40c..63f5c2ad5b 100644
--- a/mullvad-api/src/lib.rs
+++ b/mullvad-api/src/lib.rs
@@ -5,6 +5,8 @@ use chrono::{offset::Utc, DateTime};
use futures::channel::mpsc;
use futures::Stream;
use hyper::Method;
+#[cfg(target_os = "android")]
+use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken};
use mullvad_types::{
account::{AccountToken, VoucherSubmission},
version::AppVersion,
@@ -63,6 +65,8 @@ pub const API_IP_CACHE_FILENAME: &str = "api-ip-address.txt";
const ACCOUNTS_URL_PREFIX: &str = "accounts/v1";
const APP_URL_PREFIX: &str = "app/v1";
+#[cfg(target_os = "android")]
+const GOOGLE_PAYMENTS_URL_PREFIX: &str = "payments/google-play/v1";
pub static API: LazyManual<ApiEndpoint> = LazyManual::new(ApiEndpoint::from_env_vars);
@@ -457,6 +461,64 @@ impl AccountsProxy {
}
}
+ #[cfg(target_os = "android")]
+ pub fn init_play_purchase(
+ &mut self,
+ account_token: AccountToken,
+ ) -> impl Future<Output = Result<PlayPurchasePaymentToken, rest::Error>> {
+ #[derive(serde::Deserialize)]
+ struct PlayPurchaseInitResponse {
+ obfuscated_id: String,
+ }
+
+ let service = self.handle.service.clone();
+ let factory = self.handle.factory.clone();
+ let access_proxy = self.handle.token_store.clone();
+
+ async move {
+ let response = rest::send_json_request(
+ &factory,
+ service,
+ &format!("{GOOGLE_PAYMENTS_URL_PREFIX}/init"),
+ Method::POST,
+ &(),
+ Some((access_proxy, account_token)),
+ &[StatusCode::OK],
+ )
+ .await;
+
+ let PlayPurchaseInitResponse { obfuscated_id } =
+ rest::deserialize_body(response?).await?;
+
+ Ok(obfuscated_id)
+ }
+ }
+
+ #[cfg(target_os = "android")]
+ pub fn verify_play_purchase(
+ &mut self,
+ account_token: AccountToken,
+ play_purchase: PlayPurchase,
+ ) -> impl Future<Output = Result<(), rest::Error>> {
+ let service = self.handle.service.clone();
+ let factory = self.handle.factory.clone();
+ let access_proxy = self.handle.token_store.clone();
+
+ async move {
+ rest::send_json_request(
+ &factory,
+ service,
+ &format!("{GOOGLE_PAYMENTS_URL_PREFIX}/acknowledge"),
+ Method::POST,
+ &play_purchase,
+ Some((access_proxy, account_token)),
+ &[StatusCode::ACCEPTED],
+ )
+ .await?;
+ Ok(())
+ }
+ }
+
pub fn get_www_auth_token(
&self,
account: AccountToken,
diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs
index 674bcf8c4e..c3687a1eee 100644
--- a/mullvad-api/src/rest.rs
+++ b/mullvad-api/src/rest.rs
@@ -394,10 +394,17 @@ impl From<Request> for RestRequest {
}
#[derive(serde::Deserialize)]
-pub struct ErrorResponse {
+struct OldErrorResponse {
pub code: String,
}
+/// If `NewErrorResponse::type` is not defined it should default to "about:blank"
+const DEFAULT_ERROR_TYPE: &str = "about:blank";
+#[derive(serde::Deserialize)]
+struct NewErrorResponse {
+ pub r#type: Option<String>,
+}
+
#[derive(Clone)]
pub struct RequestFactory {
hostname: String,
@@ -600,8 +607,27 @@ pub async fn handle_error_response<T>(response: Response) -> Result<T> {
status => match get_body_length(&response) {
0 => status.canonical_reason().unwrap_or("Unexpected error"),
body_length => {
- let err: ErrorResponse = deserialize_body_inner(response, body_length).await?;
- return Err(Error::ApiError(status, err.code));
+ return match response.headers().get("content-type") {
+ Some(content_type) if content_type == "application/problem+json" => {
+ // TODO: We should make sure we unify the new error format and the old
+ // error format so that they both produce the same Errors for the same
+ // problems after being processed.
+ let err: NewErrorResponse =
+ deserialize_body_inner(response, body_length).await?;
+ // The new error type replaces the `code` field with the `type` field.
+ // This is what is used to programmatically check the error.
+ Err(Error::ApiError(
+ status,
+ err.r#type
+ .unwrap_or_else(|| String::from(DEFAULT_ERROR_TYPE)),
+ ))
+ }
+ _ => {
+ let err: OldErrorResponse =
+ deserialize_body_inner(response, body_length).await?;
+ Err(Error::ApiError(status, err.code))
+ }
+ };
}
},
};
diff --git a/mullvad-daemon/src/device/api.rs b/mullvad-daemon/src/device/api.rs
index 93dae78261..d7b73d6e7b 100644
--- a/mullvad-daemon/src/device/api.rs
+++ b/mullvad-daemon/src/device/api.rs
@@ -2,6 +2,8 @@ use std::pin::Pin;
use chrono::{DateTime, Utc};
use futures::{future::FusedFuture, Future};
+#[cfg(target_os = "android")]
+use mullvad_types::account::PlayPurchasePaymentToken;
use mullvad_types::{account::VoucherSubmission, device::Device, wireguard::WireguardData};
use super::{Error, PrivateAccountAndDevice, ResponseTx};
@@ -47,6 +49,27 @@ impl CurrentApiCall {
self.current_call = Some(Call::VoucherSubmission(voucher_call, Some(tx)));
}
+ #[cfg(target_os = "android")]
+ pub fn set_init_play_purchase(
+ &mut self,
+ init_play_purchase_call: ApiCall<PlayPurchasePaymentToken>,
+ tx: ResponseTx<PlayPurchasePaymentToken>,
+ ) {
+ self.current_call = Some(Call::InitPlayPurchase(init_play_purchase_call, Some(tx)));
+ }
+
+ #[cfg(target_os = "android")]
+ pub fn set_verify_play_purchase(
+ &mut self,
+ verify_play_purchase_call: ApiCall<()>,
+ tx: ResponseTx<()>,
+ ) {
+ self.current_call = Some(Call::VerifyPlayPurchase(
+ verify_play_purchase_call,
+ Some(tx),
+ ));
+ }
+
pub fn is_validating(&self) -> bool {
matches!(
&self.current_call,
@@ -109,6 +132,13 @@ enum Call {
ApiCall<VoucherSubmission>,
Option<ResponseTx<VoucherSubmission>>,
),
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(
+ ApiCall<PlayPurchasePaymentToken>,
+ Option<ResponseTx<PlayPurchasePaymentToken>>,
+ ),
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(ApiCall<()>, Option<ResponseTx<()>>),
ExpiryCheck(ApiCall<DateTime<Utc>>),
}
@@ -142,6 +172,28 @@ impl futures::Future for Call {
std::task::Poll::Pending
}
}
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(call, tx) => {
+ if let std::task::Poll::Ready(response) = Pin::new(call).poll(cx) {
+ std::task::Poll::Ready(ApiResult::InitPlayPurchase(
+ response,
+ tx.take().unwrap(),
+ ))
+ } else {
+ std::task::Poll::Pending
+ }
+ }
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(call, tx) => {
+ if let std::task::Poll::Ready(response) = Pin::new(call).poll(cx) {
+ std::task::Poll::Ready(ApiResult::VerifyPlayPurchase(
+ response,
+ tx.take().unwrap(),
+ ))
+ } else {
+ std::task::Poll::Pending
+ }
+ }
ExpiryCheck(call) => Pin::new(call).poll(cx).map(ApiResult::ExpiryCheck),
}
}
@@ -155,5 +207,12 @@ pub(crate) enum ApiResult {
Result<VoucherSubmission, Error>,
ResponseTx<VoucherSubmission>,
),
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(
+ Result<PlayPurchasePaymentToken, Error>,
+ ResponseTx<PlayPurchasePaymentToken>,
+ ),
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(Result<(), Error>, ResponseTx<()>),
ExpiryCheck(Result<DateTime<Utc>, Error>),
}
diff --git a/mullvad-daemon/src/device/mod.rs b/mullvad-daemon/src/device/mod.rs
index b54638f140..df5606a143 100644
--- a/mullvad-daemon/src/device/mod.rs
+++ b/mullvad-daemon/src/device/mod.rs
@@ -5,6 +5,8 @@ use futures::{
};
use mullvad_api::rest;
+#[cfg(target_os = "android")]
+use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken};
use mullvad_types::{
account::{AccountToken, VoucherSubmission},
device::{
@@ -12,6 +14,7 @@ use mullvad_types::{
},
wireguard::{self, RotationInterval, WireguardData},
};
+
use std::{
future::Future,
path::Path,
@@ -305,6 +308,10 @@ enum AccountManagerCommand {
SetRotationInterval(RotationInterval, ResponseTx<()>),
ValidateDevice(ResponseTx<()>),
SubmitVoucher(String, ResponseTx<VoucherSubmission>),
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(ResponseTx<PlayPurchasePaymentToken>),
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(ResponseTx<()>, PlayPurchase),
CheckExpiry(ResponseTx<DateTime<Utc>>),
Shutdown(oneshot::Sender<()>),
}
@@ -363,6 +370,18 @@ impl AccountManagerHandle {
self.send_command(AccountManagerCommand::CheckExpiry).await
}
+ #[cfg(target_os = "android")]
+ pub async fn init_play_purchase(&self) -> Result<PlayPurchasePaymentToken, Error> {
+ self.send_command(AccountManagerCommand::InitPlayPurchase)
+ .await
+ }
+
+ #[cfg(target_os = "android")]
+ pub async fn verify_play_purchase(&self, play_purchase: PlayPurchase) -> Result<(), Error> {
+ self.send_command(move |tx| AccountManagerCommand::VerifyPlayPurchase(tx, play_purchase))
+ .await
+ }
+
pub async fn shutdown(self) {
let (tx, rx) = oneshot::channel();
let _ = self
@@ -517,6 +536,14 @@ impl AccountManager {
Some(AccountManagerCommand::CheckExpiry(tx)) => {
self.handle_expiry_request(tx, &mut current_api_call);
},
+ #[cfg(target_os = "android")]
+ Some(AccountManagerCommand::InitPlayPurchase(tx)) => {
+ self.handle_init_play_purchase(tx, &mut current_api_call);
+ },
+ #[cfg(target_os = "android")]
+ Some(AccountManagerCommand::VerifyPlayPurchase(tx, play_purchase)) => {
+ self.handle_verify_play_purchase(tx, play_purchase, &mut current_api_call);
+ },
None => {
break;
@@ -589,6 +616,34 @@ impl AccountManager {
}
}
+ #[cfg(target_os = "android")]
+ fn handle_init_play_purchase(
+ &mut self,
+ tx: ResponseTx<PlayPurchasePaymentToken>,
+ current_api_call: &mut api::CurrentApiCall,
+ ) {
+ if current_api_call.is_logging_in() {
+ let _ = tx.send(Err(Error::AccountChange));
+ return;
+ }
+
+ let init_play_purchase_api_call = move || {
+ let old_config = self.data.device().ok_or(Error::NoDevice)?;
+ let account_token = old_config.account_token.clone();
+ let account_service = self.account_service.clone();
+ Ok(async move { account_service.init_play_purchase(account_token).await })
+ };
+
+ match init_play_purchase_api_call() {
+ Ok(call) => {
+ current_api_call.set_init_play_purchase(Box::pin(call), tx);
+ }
+ Err(err) => {
+ let _ = tx.send(Err(err));
+ }
+ }
+ }
+
fn handle_expiry_request(
&mut self,
tx: ResponseTx<DateTime<Utc>>,
@@ -614,6 +669,39 @@ impl AccountManager {
}
}
+ #[cfg(target_os = "android")]
+ fn handle_verify_play_purchase(
+ &mut self,
+ tx: ResponseTx<()>,
+ play_purchase: PlayPurchase,
+ current_api_call: &mut api::CurrentApiCall,
+ ) {
+ if current_api_call.is_logging_in() {
+ let _ = tx.send(Err(Error::AccountChange));
+ return;
+ }
+
+ let play_purchase_verify_api_call = move || {
+ let old_config = self.data.device().ok_or(Error::NoDevice)?;
+ let account_token = old_config.account_token.clone();
+ let account_service = self.account_service.clone();
+ Ok(async move {
+ account_service
+ .verify_play_purchase(account_token, play_purchase)
+ .await
+ })
+ };
+
+ match play_purchase_verify_api_call() {
+ Ok(call) => {
+ current_api_call.set_verify_play_purchase(Box::pin(call), tx);
+ }
+ Err(err) => {
+ let _ = tx.send(Err(err));
+ }
+ }
+ }
+
async fn consume_api_result(
&mut self,
result: api::ApiResult,
@@ -628,6 +716,16 @@ impl AccountManager {
self.consume_voucher_result(data_response, tx).await
}
ExpiryCheck(data_response) => self.consume_expiry_result(data_response).await,
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(data_response, tx) => {
+ self.consume_init_play_purchase_result(data_response, tx)
+ .await
+ }
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(data_response, tx) => {
+ self.consume_verify_play_purchase_result(data_response, tx)
+ .await
+ }
}
}
@@ -799,6 +897,44 @@ impl AccountManager {
}
}
+ #[cfg(target_os = "android")]
+ async fn consume_init_play_purchase_result(
+ &mut self,
+ response: Result<PlayPurchasePaymentToken, Error>,
+ tx: ResponseTx<PlayPurchasePaymentToken>,
+ ) {
+ match &response {
+ Ok(_) => (),
+ Err(Error::InvalidAccount) => {
+ self.revoke_device(|| Error::InvalidAccount).await;
+ }
+ Err(Error::InvalidDevice) => {
+ self.revoke_device(|| Error::InvalidDevice).await;
+ }
+ Err(err) => log::error!("Failed to initialize play purchase: {}", err),
+ }
+ let _ = tx.send(response);
+ }
+
+ #[cfg(target_os = "android")]
+ async fn consume_verify_play_purchase_result(
+ &mut self,
+ response: Result<(), Error>,
+ tx: ResponseTx<()>,
+ ) {
+ match &response {
+ Ok(_) => (),
+ Err(Error::InvalidAccount) => {
+ self.revoke_device(|| Error::InvalidAccount).await;
+ }
+ Err(Error::InvalidDevice) => {
+ self.revoke_device(|| Error::InvalidDevice).await;
+ }
+ Err(err) => log::error!("Failed to verify play purchase: {}", err),
+ }
+ let _ = tx.send(response);
+ }
+
fn drain_device_requests_with_err(&mut self, err: Error) {
let cloneable_err = Arc::new(err);
Self::drain_requests(&mut self.rotation_requests, || {
diff --git a/mullvad-daemon/src/device/service.rs b/mullvad-daemon/src/device/service.rs
index 9c967e4413..fdda61297f 100644
--- a/mullvad-daemon/src/device/service.rs
+++ b/mullvad-daemon/src/device/service.rs
@@ -2,6 +2,8 @@ use std::{future::Future, time::Duration};
use chrono::{DateTime, Utc};
use futures::future::{abortable, AbortHandle};
+#[cfg(target_os = "android")]
+use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken};
use mullvad_types::{
account::{AccountToken, VoucherSubmission},
device::{Device, DeviceId},
@@ -320,6 +322,47 @@ impl AccountService {
}
result.map_err(map_rest_error)
}
+
+ #[cfg(target_os = "android")]
+ pub async fn init_play_purchase(
+ &self,
+ account_token: AccountToken,
+ ) -> Result<PlayPurchasePaymentToken, Error> {
+ let mut proxy = self.proxy.clone();
+ let api_handle = self.api_availability.clone();
+ let result = retry_future(
+ move || proxy.init_play_purchase(account_token.clone()),
+ move |result| should_retry(result, &api_handle),
+ RETRY_ACTION_STRATEGY,
+ )
+ .await;
+ if result.is_ok() {
+ self.initial_check_abort_handle.abort();
+ self.api_availability.resume_background();
+ }
+ result.map_err(map_rest_error)
+ }
+
+ #[cfg(target_os = "android")]
+ pub async fn verify_play_purchase(
+ &self,
+ account_token: AccountToken,
+ play_purchase: PlayPurchase,
+ ) -> Result<(), Error> {
+ let mut proxy = self.proxy.clone();
+ let api_handle = self.api_availability.clone();
+ let result = retry_future(
+ move || proxy.verify_play_purchase(account_token.clone(), play_purchase.clone()),
+ move |result| should_retry(result, &api_handle),
+ RETRY_ACTION_STRATEGY,
+ )
+ .await;
+ if result.is_ok() {
+ self.initial_check_abort_handle.abort();
+ self.api_availability.resume_background();
+ }
+ result.map_err(map_rest_error)
+ }
}
pub fn spawn_account_service(
@@ -409,6 +452,7 @@ fn should_retry_backoff<T>(result: &Result<T, RestError>) -> bool {
fn map_rest_error(error: rest::Error) -> Error {
match error {
RestError::ApiError(_status, ref code) => match code.as_str() {
+ // TODO: Implement invalid payment
mullvad_api::DEVICE_NOT_FOUND => Error::InvalidDevice,
mullvad_api::INVALID_ACCOUNT => Error::InvalidAccount,
mullvad_api::MAX_DEVICES_REACHED => Error::MaxDevicesReached,
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index f7343acc87..1077185ca3 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -38,6 +38,8 @@ use mullvad_relay_selector::{
updater::{RelayListUpdater, RelayListUpdaterHandle},
RelaySelector, SelectorConfig,
};
+#[cfg(target_os = "android")]
+use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken};
use mullvad_types::{
access_method::{AccessMethod, AccessMethodSetting},
account::{AccountData, AccountToken, VoucherSubmission},
@@ -178,6 +180,14 @@ pub enum Error {
#[cfg(target_os = "macos")]
#[error(display = "Failed to set exclusion group")]
GroupIdError(#[error(source)] io::Error),
+
+ #[cfg(target_os = "android")]
+ #[error(display = "Failed to initialize play purchase")]
+ InitPlayPurchase(#[error(source)] device::Error),
+
+ #[cfg(target_os = "android")]
+ #[error(display = "Failed to verify play purchase")]
+ VerifyPlayPurchase(#[error(source)] device::Error),
}
/// Enum representing commands that can be sent to the daemon.
@@ -327,6 +337,12 @@ pub enum DaemonCommand {
/// to bypass the tunnel in blocking states.
#[cfg(target_os = "android")]
BypassSocket(RawFd, oneshot::Sender<()>),
+ /// Initialize a google play purchase through the API.
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(ResponseTx<PlayPurchasePaymentToken, Error>),
+ /// Verify that a google play payment was successful through the API.
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(ResponseTx<(), Error>, PlayPurchase),
}
/// All events that can happen in the daemon. Sent from various threads and exposed interfaces.
@@ -1109,6 +1125,12 @@ where
PrepareRestart => self.on_prepare_restart(),
#[cfg(target_os = "android")]
BypassSocket(fd, tx) => self.on_bypass_socket(fd, tx),
+ #[cfg(target_os = "android")]
+ InitPlayPurchase(tx) => self.on_init_play_purchase(tx),
+ #[cfg(target_os = "android")]
+ VerifyPlayPurchase(tx, play_purchase) => {
+ self.on_verify_play_purchase(tx, play_purchase)
+ }
}
}
@@ -2370,6 +2392,36 @@ where
}
}
+ #[cfg(target_os = "android")]
+ fn on_init_play_purchase(&mut self, tx: ResponseTx<PlayPurchasePaymentToken, Error>) {
+ let manager = self.account_manager.clone();
+ tokio::spawn(async move {
+ Self::oneshot_send(
+ tx,
+ manager
+ .init_play_purchase()
+ .await
+ .map_err(Error::InitPlayPurchase),
+ "init_play_purchase response",
+ );
+ });
+ }
+
+ #[cfg(target_os = "android")]
+ fn on_verify_play_purchase(&mut self, tx: ResponseTx<(), Error>, play_purchase: PlayPurchase) {
+ let manager = self.account_manager.clone();
+ tokio::spawn(async move {
+ Self::oneshot_send(
+ tx,
+ manager
+ .verify_play_purchase(play_purchase)
+ .await
+ .map_err(Error::VerifyPlayPurchase),
+ "verify_play_purchase response",
+ );
+ });
+ }
+
/// Set the target state of the client. If it changed trigger the operations needed to
/// progress towards that state.
/// Returns a bool representing whether or not a state change was initiated.
diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs
index 88b5f6d938..56de8919db 100644
--- a/mullvad-jni/src/classes.rs
+++ b/mullvad-jni/src/classes.rs
@@ -33,6 +33,11 @@ pub const CLASSES: &[&str] = &[
"net/mullvad/mullvadvpn/model/LocationConstraint$Location",
"net/mullvad/mullvadvpn/model/LocationConstraint$CustomList",
"net/mullvad/mullvadvpn/model/ObfuscationSettings",
+ "net/mullvad/mullvadvpn/model/PlayPurchase",
+ "net/mullvad/mullvadvpn/model/PlayPurchaseInitError",
+ "net/mullvad/mullvadvpn/model/PlayPurchaseInitResult",
+ "net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError",
+ "net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult",
"net/mullvad/mullvadvpn/model/PublicKey",
"net/mullvad/mullvadvpn/model/QuantumResistantState",
"net/mullvad/mullvadvpn/model/Port",
diff --git a/mullvad-jni/src/daemon_interface.rs b/mullvad-jni/src/daemon_interface.rs
index 771c432a7a..c64c94041e 100644
--- a/mullvad-jni/src/daemon_interface.rs
+++ b/mullvad-jni/src/daemon_interface.rs
@@ -1,7 +1,7 @@
use futures::{channel::oneshot, executor::block_on};
use mullvad_daemon::{device, DaemonCommand, DaemonCommandSender};
use mullvad_types::{
- account::{AccountData, AccountToken, VoucherSubmission},
+ account::{AccountData, AccountToken, PlayPurchase, VoucherSubmission},
device::{Device, DeviceState},
location::GeoIpLocation,
relay_constraints::{ObfuscationSettings, RelaySettingsUpdate},
@@ -307,6 +307,26 @@ impl DaemonInterface {
.map_err(Error::from)
}
+ pub fn init_play_purchase(&self) -> Result<String> {
+ let (tx, rx) = oneshot::channel();
+
+ self.send_command(DaemonCommand::InitPlayPurchase(tx))?;
+
+ block_on(rx)
+ .map_err(|_| Error::NoResponse)?
+ .map_err(Error::from)
+ }
+
+ pub fn verify_play_purchase(&self, play_purchase: PlayPurchase) -> Result<()> {
+ let (tx, rx) = oneshot::channel();
+
+ self.send_command(DaemonCommand::VerifyPlayPurchase(tx, play_purchase))?;
+
+ block_on(rx)
+ .map_err(|_| Error::NoResponse)?
+ .map_err(Error::from)
+ }
+
pub fn update_relay_settings(&self, update: RelaySettingsUpdate) -> Result<()> {
let (tx, rx) = oneshot::channel();
diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs
index 6f33938e99..a40bf73fc1 100644
--- a/mullvad-jni/src/lib.rs
+++ b/mullvad-jni/src/lib.rs
@@ -24,7 +24,7 @@ use mullvad_daemon::{
DaemonCommandChannel,
};
use mullvad_types::{
- account::{AccountData, VoucherSubmission},
+ account::{AccountData, PlayPurchase, VoucherSubmission},
settings::DnsOptions,
};
use std::{
@@ -191,6 +191,62 @@ impl From<daemon_interface::Error> for VoucherSubmissionError {
}
}
+#[derive(IntoJava)]
+#[jnix(package = "net.mullvad.mullvadvpn.model")]
+pub enum PlayPurchaseInitResult {
+ Ok(String),
+ Error(PlayPurchaseInitError),
+}
+
+#[derive(IntoJava)]
+#[jnix(package = "net.mullvad.mullvadvpn.model")]
+pub enum PlayPurchaseInitError {
+ OtherError,
+}
+
+impl From<Result<String, daemon_interface::Error>> for PlayPurchaseInitResult {
+ fn from(result: Result<String, daemon_interface::Error>) -> Self {
+ match result {
+ Ok(obfuscated_id) => PlayPurchaseInitResult::Ok(obfuscated_id),
+ Err(error) => PlayPurchaseInitResult::Error(error.into()),
+ }
+ }
+}
+
+impl From<daemon_interface::Error> for PlayPurchaseInitError {
+ fn from(_error: daemon_interface::Error) -> Self {
+ PlayPurchaseInitError::OtherError
+ }
+}
+
+#[derive(IntoJava)]
+#[jnix(package = "net.mullvad.mullvadvpn.model")]
+pub enum PlayPurchaseVerifyResult {
+ Ok,
+ Error(PlayPurchaseVerifyError),
+}
+
+#[derive(IntoJava)]
+#[jnix(package = "net.mullvad.mullvadvpn.model")]
+pub enum PlayPurchaseVerifyError {
+ OtherError,
+}
+
+impl From<Result<(), daemon_interface::Error>> for PlayPurchaseVerifyResult {
+ fn from(result: Result<(), daemon_interface::Error>) -> Self {
+ match result {
+ Ok(()) => PlayPurchaseVerifyResult::Ok,
+ Err(error) => PlayPurchaseVerifyResult::Error(error.into()),
+ }
+ }
+}
+
+impl From<daemon_interface::Error> for PlayPurchaseVerifyError {
+ fn from(_error: daemon_interface::Error) -> Self {
+ PlayPurchaseVerifyError::OtherError
+ }
+}
+
#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_initialize(
@@ -1194,6 +1250,62 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_submitV
#[no_mangle]
#[allow(non_snake_case)]
+pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_initPlayPurchase<'env>(
+ env: JNIEnv<'env>,
+ _: JObject<'_>,
+ daemon_interface_address: jlong,
+) -> JObject<'env> {
+ let env = JnixEnv::from(env);
+
+ let result =
+ // SAFETY: The address points to an instance valid for the duration of this function call
+ if let Some(daemon_interface) = unsafe { get_daemon_interface(daemon_interface_address) } {
+ let raw_result = daemon_interface.init_play_purchase();
+
+ if let Err(ref error) = &raw_result {
+ log_request_error("init google play purchase", error);
+ }
+
+ PlayPurchaseInitResult::from(raw_result)
+ } else {
+ PlayPurchaseInitResult::Error(PlayPurchaseInitError::OtherError)
+ };
+
+ result.into_java(&env).forget()
+}
+
+#[no_mangle]
+#[allow(non_snake_case)]
+pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_verifyPlayPurchase<
+ 'env,
+>(
+ env: JNIEnv<'env>,
+ _: JObject<'_>,
+ daemon_interface_address: jlong,
+ play_purchase: JObject<'_>,
+) -> JObject<'env> {
+ let env = JnixEnv::from(env);
+
+ let result =
+ // SAFETY: The address points to an instance valid for the duration of this function call
+ if let Some(daemon_interface) = unsafe { get_daemon_interface(daemon_interface_address) } {
+ let play_purchase = PlayPurchase::from_java(&env, play_purchase);
+ let raw_result = daemon_interface.verify_play_purchase(play_purchase);
+
+ if let Err(ref error) = &raw_result {
+ log_request_error("verify google play purchase", error);
+ }
+
+ PlayPurchaseVerifyResult::from(raw_result)
+ } else {
+ PlayPurchaseVerifyResult::Error(PlayPurchaseVerifyError::OtherError)
+ };
+
+ result.into_java(&env).forget()
+}
+
+#[no_mangle]
+#[allow(non_snake_case)]
pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_updateRelaySettings(
env: JNIEnv<'_>,
_: JObject<'_>,
diff --git a/mullvad-types/src/account.rs b/mullvad-types/src/account.rs
index 16f6a963f2..7adb7fbffa 100644
--- a/mullvad-types/src/account.rs
+++ b/mullvad-types/src/account.rs
@@ -1,6 +1,6 @@
use chrono::{offset::Utc, DateTime};
#[cfg(target_os = "android")]
-use jnix::IntoJava;
+use jnix::{FromJava, IntoJava};
use serde::{Deserialize, Serialize};
/// Identifier used to identify a Mullvad account.
@@ -9,6 +9,11 @@ pub type AccountToken = String;
/// Identifier used to authenticate a Mullvad account.
pub type AccessToken = String;
+/// The payment token returned by initiating a google play purchase.
+/// In the API this is called the `obfuscated_id`.
+#[cfg(target_os = "android")]
+pub type PlayPurchasePaymentToken = String;
+
/// Account expiration info returned by the API via `/v1/me`.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[cfg_attr(target_os = "android", derive(IntoJava))]
@@ -39,6 +44,16 @@ pub struct VoucherSubmission {
pub new_expiry: DateTime<Utc>,
}
+/// `PlayPurchase` is provided to google in order to verify that a google play purchase was acknowledged.
+#[derive(Deserialize, Serialize, Debug, Clone)]
+#[cfg_attr(target_os = "android", derive(FromJava))]
+#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))]
+#[cfg(target_os = "android")]
+pub struct PlayPurchase {
+ pub product_id: String,
+ pub purchase_token: PlayPurchasePaymentToken,
+}
+
/// Token used for authentication in the API.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct AccessTokenData {