diff options
| -rw-r--r-- | mullvad-api/src/lib.rs | 59 | ||||
| -rw-r--r-- | mullvad-daemon/src/device/api.rs | 55 | ||||
| -rw-r--r-- | mullvad-daemon/src/device/mod.rs | 123 | ||||
| -rw-r--r-- | mullvad-daemon/src/device/service.rs | 41 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 52 | ||||
| -rw-r--r-- | mullvad-jni/src/daemon_interface.rs | 22 | ||||
| -rw-r--r-- | mullvad-jni/src/lib.rs | 114 | ||||
| -rw-r--r-- | mullvad-types/src/account.rs | 17 |
8 files changed, 475 insertions, 8 deletions
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index 37faf5c40c..647b5cf071 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -6,7 +6,7 @@ use futures::channel::mpsc; use futures::Stream; use hyper::Method; use mullvad_types::{ - account::{AccountToken, VoucherSubmission}, + account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, version::AppVersion, }; use proxy::ApiConnectionMode; @@ -63,6 +63,7 @@ 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"; +const GOOGLE_PAYMENTS_URL_PREFIX: &str = "payments/google-play/v1"; pub static API: LazyManual<ApiEndpoint> = LazyManual::new(ApiEndpoint::from_env_vars); @@ -457,6 +458,62 @@ impl AccountsProxy { } } + 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) + } + } + + 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-daemon/src/device/api.rs b/mullvad-daemon/src/device/api.rs index 93dae78261..07e7b35311 100644 --- a/mullvad-daemon/src/device/api.rs +++ b/mullvad-daemon/src/device/api.rs @@ -2,7 +2,11 @@ use std::pin::Pin; use chrono::{DateTime, Utc}; use futures::{future::FusedFuture, Future}; -use mullvad_types::{account::VoucherSubmission, device::Device, wireguard::WireguardData}; +use mullvad_types::{ + account::{PlayPurchasePaymentToken, VoucherSubmission}, + device::Device, + wireguard::WireguardData, +}; use super::{Error, PrivateAccountAndDevice, ResponseTx}; @@ -47,6 +51,25 @@ impl CurrentApiCall { self.current_call = Some(Call::VoucherSubmission(voucher_call, Some(tx))); } + 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))); + } + + 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,11 @@ enum Call { ApiCall<VoucherSubmission>, Option<ResponseTx<VoucherSubmission>>, ), + InitPlayPurchase( + ApiCall<PlayPurchasePaymentToken>, + Option<ResponseTx<PlayPurchasePaymentToken>>, + ), + VerifyPlayPurchase(ApiCall<()>, Option<ResponseTx<()>>), ExpiryCheck(ApiCall<DateTime<Utc>>), } @@ -142,6 +170,26 @@ impl futures::Future for Call { std::task::Poll::Pending } } + 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 + } + } + 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 +203,10 @@ pub(crate) enum ApiResult { Result<VoucherSubmission, Error>, ResponseTx<VoucherSubmission>, ), + InitPlayPurchase( + Result<PlayPurchasePaymentToken, Error>, + ResponseTx<PlayPurchasePaymentToken>, + ), + 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..528c25ce9a 100644 --- a/mullvad-daemon/src/device/mod.rs +++ b/mullvad-daemon/src/device/mod.rs @@ -6,7 +6,7 @@ use futures::{ use mullvad_api::rest; use mullvad_types::{ - account::{AccountToken, VoucherSubmission}, + account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, device::{ AccountAndDevice, Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceName, DeviceState, }, @@ -305,6 +305,8 @@ enum AccountManagerCommand { SetRotationInterval(RotationInterval, ResponseTx<()>), ValidateDevice(ResponseTx<()>), SubmitVoucher(String, ResponseTx<VoucherSubmission>), + InitPlayPurchase(ResponseTx<PlayPurchasePaymentToken>), + VerifyPlayPurchase(ResponseTx<()>, PlayPurchase), CheckExpiry(ResponseTx<DateTime<Utc>>), Shutdown(oneshot::Sender<()>), } @@ -359,6 +361,16 @@ impl AccountManagerHandle { .await } + pub async fn init_play_purchase(&self) -> Result<PlayPurchasePaymentToken, Error> { + self.send_command(AccountManagerCommand::InitPlayPurchase) + .await + } + + 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 check_expiry(&self) -> Result<DateTime<Utc>, Error> { self.send_command(AccountManagerCommand::CheckExpiry).await } @@ -514,6 +526,12 @@ impl AccountManager { Some(AccountManagerCommand::SubmitVoucher(voucher, tx)) => { self.handle_voucher_submission(tx, voucher, &mut current_api_call); }, + Some(AccountManagerCommand::InitPlayPurchase(tx)) => { + self.handle_init_play_purchase(tx, &mut current_api_call); + }, + Some(AccountManagerCommand::VerifyPlayPurchase(tx, play_purchase)) => { + self.handle_verify_play_purchase(tx, play_purchase, &mut current_api_call); + }, Some(AccountManagerCommand::CheckExpiry(tx)) => { self.handle_expiry_request(tx, &mut current_api_call); }, @@ -589,6 +607,65 @@ impl AccountManager { } } + 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_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)); + } + } + } + fn handle_expiry_request( &mut self, tx: ResponseTx<DateTime<Utc>>, @@ -627,6 +704,14 @@ impl AccountManager { VoucherSubmission(data_response, tx) => { self.consume_voucher_result(data_response, tx).await } + InitPlayPurchase(data_response, tx) => { + self.consume_init_play_purchase_result(data_response, tx) + .await + } + VerifyPlayPurchase(data_response, tx) => { + self.consume_verify_play_purchase_result(data_response, tx) + .await + } ExpiryCheck(data_response) => self.consume_expiry_result(data_response).await, } } @@ -665,6 +750,42 @@ impl AccountManager { let _ = tx.send(response); } + 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); + } + + 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); + } + async fn consume_expiry_result(&mut self, response: Result<DateTime<Utc>, Error>) { match response { Ok(expiry) => { diff --git a/mullvad-daemon/src/device/service.rs b/mullvad-daemon/src/device/service.rs index 9c967e4413..85d9d56d84 100644 --- a/mullvad-daemon/src/device/service.rs +++ b/mullvad-daemon/src/device/service.rs @@ -3,7 +3,7 @@ use std::{future::Future, time::Duration}; use chrono::{DateTime, Utc}; use futures::future::{abortable, AbortHandle}; use mullvad_types::{ - account::{AccountToken, VoucherSubmission}, + account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, device::{Device, DeviceId}, wireguard::WireguardData, }; @@ -320,6 +320,45 @@ impl AccountService { } result.map_err(map_rest_error) } + + 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) + } + + 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( diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index f7343acc87..ceb50ca2b2 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -40,7 +40,7 @@ use mullvad_relay_selector::{ }; use mullvad_types::{ access_method::{AccessMethod, AccessMethodSetting}, - account::{AccountData, AccountToken, VoucherSubmission}, + account::{AccountData, AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, auth_failed::AuthFailed, custom_list::CustomList, device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, @@ -178,6 +178,16 @@ pub enum Error { #[cfg(target_os = "macos")] #[error(display = "Failed to set exclusion group")] GroupIdError(#[error(source)] io::Error), + + // TODO + //#[cfg(target_os = "android")] + #[error(display = "Failed to initialize play purchase")] + InitPlayPurchase(#[error(source)] device::Error), + + // TODO + //#[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,14 @@ 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. + ///TODO + //#[cfg(target_os = "android")] + InitPlayPurchase(ResponseTx<PlayPurchasePaymentToken, Error>), + /// Verify that a google play payment was successful through the API. + ///TODO + //#[cfg(target_os = "android")] + VerifyPlayPurchase(ResponseTx<(), Error>, PlayPurchase), } /// All events that can happen in the daemon. Sent from various threads and exposed interfaces. @@ -1027,6 +1045,10 @@ where GetAccountData(tx, account_token) => self.on_get_account_data(tx, account_token), GetWwwAuthToken(tx) => self.on_get_www_auth_token(tx).await, SubmitVoucher(tx, voucher) => self.on_submit_voucher(tx, voucher), + InitPlayPurchase(tx) => self.on_init_play_purchase(tx), + VerifyPlayPurchase(tx, play_purchase) => { + self.on_verify_play_purchase(tx, play_purchase) + } GetRelayLocations(tx) => self.on_get_relay_locations(tx), UpdateRelayLocations => self.on_update_relay_locations().await, LoginAccount(tx, account_token) => self.on_login_account(tx, account_token), @@ -1404,6 +1426,34 @@ where }); } + 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", + ); + }); + } + + 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", + ); + }); + } + fn on_get_relay_locations(&mut self, tx: oneshot::Sender<RelayList>) { Self::oneshot_send(tx, self.relay_selector.get_locations(), "relay locations"); } 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..ff7a978799 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..2d12a80a2a 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; +// TODO: Should be only android +/// The payment token returned by initiating a google play purchase. +/// In the API this is called the `obfuscated_id`. +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>, } +/// TODO Should be only android +/// `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"))] +pub struct PlayPurchase { + pub product_id: String, + pub purchase_token: String, +} + /// Token used for authentication in the API. #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct AccessTokenData { |
