Skip to content

Commit

Permalink
Merge branch 'feature-api-android-inapp-purchases'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jontified committed Oct 16, 2023
2 parents d32b6f8 + bf6d46a commit 9ea790f
Show file tree
Hide file tree
Showing 16 changed files with 601 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions mullvad-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down
32 changes: 29 additions & 3 deletions mullvad-api/src/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
}
};
}
},
};
Expand Down
59 changes: 59 additions & 0 deletions mullvad-daemon/src/device/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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>>),
}

Expand Down Expand Up @@ -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),
}
}
Expand All @@ -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>),
}
Loading

0 comments on commit 9ea790f

Please sign in to comment.