summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock18
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt9
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt9
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt172
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt15
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/SegmentedInputFormatter.kt137
-rw-r--r--android/src/main/res/drawable/dialog_background.xml15
-rw-r--r--android/src/main/res/drawable/edit_text_background.xml25
-rw-r--r--android/src/main/res/layout/confirm_no_email.xml2
-rw-r--r--android/src/main/res/layout/redeem_voucher.xml55
-rw-r--r--android/src/main/res/layout/welcome.xml8
-rw-r--r--android/src/main/res/values/dimensions.xml2
-rw-r--r--android/src/main/res/values/strings.xml11
-rw-r--r--mullvad-jni/Cargo.toml2
-rw-r--r--mullvad-jni/src/classes.rs2
-rw-r--r--mullvad-jni/src/daemon_interface.rs13
-rw-r--r--mullvad-jni/src/lib.rs58
-rw-r--r--mullvad-types/Cargo.toml2
-rw-r--r--mullvad-types/src/account.rs6
-rw-r--r--talpid-core/Cargo.toml2
-rw-r--r--talpid-types/Cargo.toml2
24 files changed, 548 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24be9f238c..c92c9464b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@ Line wrap the file at 100 chars. Th
#### Android
- Add possibility to create account from the login screen.
- Add welcome screen for newly created accounts.
+- Allow submitting voucher codes to add time to the account.
### Changed
- Move location of the account data (including the WireGuard keys), so that it isn't lost when the
diff --git a/Cargo.lock b/Cargo.lock
index 40d529824b..5bace2d4e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -982,18 +982,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jnix"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"jni 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jnix-macros 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jnix-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "jnix-macros"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1436,7 +1436,7 @@ dependencies = [
"err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"ipnetwork 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jnix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-client-core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1551,7 +1551,7 @@ dependencies = [
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ipnetwork 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jnix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mullvad-paths 0.1.0",
@@ -2720,7 +2720,7 @@ dependencies = [
"futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ipnetwork 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jnix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)",
"jsonrpc-macros 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2809,7 +2809,7 @@ dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ipnetwork 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jnix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3672,8 +3672,8 @@ dependencies = [
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
"checksum jni 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402"
"checksum jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
-"checksum jnix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cd26c8a9ccd3b8c1d47e17f290237ae1c4bd3a89ef31fafc61bc09804cc2b1ec"
-"checksum jnix-macros 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a819d1d2905045ab6da444b4aa612e82a028508e6148acd9b61ce4985b4d6f8f"
+"checksum jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6354ae923ca4df982181ae2cd77eb4214f8c11d11d0c0cd8606c9347ac2abc57"
+"checksum jnix-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c78132fe420156f13b30518fcda9449b0ab8ae3b5584e8a1c53ce390fe770b44"
"checksum js-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)" = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
"checksum jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)" = "<none>"
"checksum jsonrpc-client-core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f29cb249837420fb0cee7fb0fbf1d22679e121b160e71bb5e0d90b9df241c23e"
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt
new file mode 100644
index 0000000000..9a14c4cf7e
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.model
+
+data class VoucherSubmission(val timeAdded: Long, val newExpiry: String)
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt
new file mode 100644
index 0000000000..33c57a595a
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.model
+
+sealed class VoucherSubmissionResult {
+ class Ok(val submission: VoucherSubmission) : VoucherSubmissionResult()
+ class InvalidVoucher : VoucherSubmissionResult()
+ class VoucherAlreadyUsed : VoucherSubmissionResult()
+ class RpcError : VoucherSubmissionResult()
+ class OtherError : VoucherSubmissionResult()
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
index 593139a8a9..3c55f76b7f 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
@@ -9,6 +9,7 @@ import net.mullvad.mullvadvpn.model.RelayList
import net.mullvad.mullvadvpn.model.RelaySettingsUpdate
import net.mullvad.mullvadvpn.model.Settings
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
import net.mullvad.talpid.util.EventNotifier
class MullvadDaemon(val vpnService: MullvadVpnService) {
@@ -109,6 +110,10 @@ class MullvadDaemon(val vpnService: MullvadVpnService) {
shutdown(daemonInterfaceAddress)
}
+ fun submitVoucher(voucher: String): VoucherSubmissionResult {
+ return submitVoucher(daemonInterfaceAddress, voucher)
+ }
+
fun updateRelaySettings(update: RelaySettingsUpdate) {
updateRelaySettings(daemonInterfaceAddress, update)
}
@@ -147,6 +152,10 @@ class MullvadDaemon(val vpnService: MullvadVpnService) {
private external fun setAutoConnect(daemonInterfaceAddress: Long, alwaysOn: Boolean)
private external fun setWireguardMtu(daemonInterfaceAddress: Long, wireguardMtu: Int?)
private external fun shutdown(daemonInterfaceAddress: Long)
+ private external fun submitVoucher(
+ daemonInterfaceAddress: Long,
+ voucher: String
+ ): VoucherSubmissionResult
private external fun updateRelaySettings(
daemonInterfaceAddress: Long,
update: RelaySettingsUpdate
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
new file mode 100644
index 0000000000..7dde157903
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
@@ -0,0 +1,172 @@
+package net.mullvad.mullvadvpn.ui
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.support.v4.app.DialogFragment
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import android.widget.EditText
+import android.widget.TextView
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
+import net.mullvad.mullvadvpn.service.MullvadDaemon
+import net.mullvad.mullvadvpn.ui.widget.Button
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.SegmentedInputFormatter
+
+const val FULL_VOUCHER_CODE_LENGTH = "XXXX-XXXX-XXXX-XXXX".length
+
+class RedeemVoucherDialogFragment : DialogFragment() {
+ private val jobTracker = JobTracker()
+
+ private lateinit var parentActivity: MainActivity
+ private lateinit var errorMessage: TextView
+ private lateinit var voucherInput: EditText
+
+ private var redeemButton: Button? = null
+ private var subscriptionId: Int? = null
+
+ private var daemon: MullvadDaemon? = null
+ set(value) {
+ field = value
+ updateRedeemButton()
+ }
+
+ private var voucherInputIsValid = false
+ set(value) {
+ field = value
+ updateRedeemButton()
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ parentActivity = context as MainActivity
+
+ subscriptionId = parentActivity.serviceNotifier.subscribe { connection ->
+ daemon = connection?.daemon
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.redeem_voucher, container, false)
+
+ voucherInput = view.findViewById<EditText>(R.id.voucher_code).apply {
+ addTextChangedListener(ValidVoucherCodeChecker())
+ }
+
+ SegmentedInputFormatter(voucherInput, '-').apply {
+ allCaps = true
+
+ isValidInputCharacter = { character ->
+ ('A' <= character && character <= 'Z') || ('0' <= character && character <= '9')
+ }
+ }
+
+ redeemButton = view.findViewById<Button>(R.id.redeem).apply {
+ setEnabled(false)
+
+ setOnClickAction("action", jobTracker) {
+ submitVoucher()
+ }
+ }
+
+ errorMessage = view.findViewById(R.id.error)
+
+ view.findViewById<Button>(R.id.cancel).setOnClickAction("action", jobTracker) {
+ activity?.onBackPressed()
+ }
+
+ return view
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = super.onCreateDialog(savedInstanceState)
+
+ dialog.window?.setBackgroundDrawable(ColorDrawable(android.R.color.transparent))
+
+ return dialog
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+ }
+
+ override fun onDestroyView() {
+ jobTracker.cancelAllJobs()
+
+ super.onDestroyView()
+ }
+
+ override fun onDetach() {
+ subscriptionId?.let { id ->
+ parentActivity.serviceNotifier.unsubscribe(id)
+ }
+
+ super.onDetach()
+ }
+
+ private fun updateRedeemButton() {
+ redeemButton?.apply {
+ setEnabled(voucherInputIsValid && daemon != null)
+ }
+ }
+
+ private suspend fun submitVoucher() {
+ errorMessage.visibility = View.INVISIBLE
+
+ val result = jobTracker.runOnBackground {
+ daemon?.submitVoucher(voucherInput.text.toString())
+ }
+
+ when (result) {
+ is VoucherSubmissionResult.Ok -> {
+ if (result.submission.timeAdded > 0) {
+ dismiss()
+ }
+ }
+ is VoucherSubmissionResult.InvalidVoucher -> showError(R.string.invalid_voucher)
+ is VoucherSubmissionResult.VoucherAlreadyUsed -> {
+ showError(R.string.voucher_already_used)
+ }
+ else -> showError(R.string.error_occurred)
+ }
+ }
+
+ private fun showError(message: Int) {
+ errorMessage.apply {
+ setText(message)
+ visibility = View.VISIBLE
+ }
+ }
+
+ inner class ValidVoucherCodeChecker : TextWatcher {
+ private var editRecursionCount = 0
+
+ override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {
+ editRecursionCount += 1
+ }
+
+ override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
+
+ override fun afterTextChanged(text: Editable) {
+ editRecursionCount -= 1
+
+ if (editRecursionCount == 0) {
+ voucherInputIsValid = text.length == FULL_VOUCHER_CODE_LENGTH
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
index a30b8b1dea..43e47dfd9b 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
@@ -11,6 +11,7 @@ import android.widget.TextView
import android.widget.Toast
import kotlinx.coroutines.delay
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.ui.widget.Button
import net.mullvad.mullvadvpn.ui.widget.UrlButton
import net.mullvad.mullvadvpn.util.JobTracker
import org.joda.time.DateTime
@@ -41,6 +42,12 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
prepare(daemon, jobTracker)
}
+ view.findViewById<Button>(R.id.redeem_voucher).apply {
+ setOnClickAction("openRedeemVoucherDialog", jobTracker) {
+ showRedeemVoucherDialog()
+ }
+ }
+
return view
}
@@ -127,4 +134,12 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show()
}
+
+ private fun showRedeemVoucherDialog() {
+ val transaction = fragmentManager?.beginTransaction()
+
+ transaction?.addToBackStack(null)
+
+ RedeemVoucherDialogFragment().show(transaction, null)
+ }
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt
index 106be5b28a..ac510d0735 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt
@@ -104,6 +104,8 @@ open class Button : FrameLayout {
init {
button.setOnClickListener {
jobTracker?.newUiJob(clickJobName!!) {
+ setEnabled(false)
+
if (showSpinner) {
image.visibility = GONE
spinner.visibility = VISIBLE
@@ -116,6 +118,8 @@ open class Button : FrameLayout {
if (detailImage != null) {
image.visibility = VISIBLE
}
+
+ setEnabled(true)
}
}
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SegmentedInputFormatter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SegmentedInputFormatter.kt
new file mode 100644
index 0000000000..ed471778eb
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SegmentedInputFormatter.kt
@@ -0,0 +1,137 @@
+package net.mullvad.mullvadvpn.util
+
+import android.text.Editable
+import android.text.TextWatcher
+import android.widget.EditText
+
+class SegmentedInputFormatter(val input: EditText, var separator: Char) : TextWatcher {
+ private var editing = false
+ private var removing = false
+ private var separatorSkipCount = 5
+
+ var allCaps = false
+ var isValidInputCharacter: (Char) -> Boolean = { _ -> true }
+
+ var segmentSize = 4
+ set(value) {
+ field = value
+ separatorSkipCount = value + 1
+ }
+
+ init {
+ input.addTextChangedListener(this)
+ }
+
+ override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {
+ if (!editing) {
+ editing = true
+ removing = after < count
+ }
+ }
+
+ override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
+
+ override fun afterTextChanged(text: Editable) {
+ val string = text.toString()
+
+ if (isValidInput(string)) {
+ editing = false
+ maybeUpdateSelection()
+ } else {
+ formatInput(text)
+ }
+ }
+
+ private fun maybeUpdateSelection() {
+ if (removing) {
+ var start = input.selectionStart
+ var end = input.selectionEnd
+ var changed = false
+
+ if (start % separatorSkipCount == 0) {
+ start -= 1
+ changed = true
+ }
+
+ if (end % separatorSkipCount == 0) {
+ end -= 1
+ changed = true
+ }
+
+ if (changed) {
+ input.setSelection(start, end)
+ }
+ }
+ }
+
+ private fun isValidInput(string: String): Boolean {
+ return string
+ .asSequence()
+ .withIndex()
+ .all { item ->
+ val index = item.index
+ val character = item.value
+
+ if ((index + 1) % separatorSkipCount == 0) {
+ character == separator
+ } else {
+ isValidInputCharacter(character)
+ }
+ }
+ }
+
+ private fun formatInput(input: Editable) {
+ var index = 0
+ val length = input.length
+ var changed = false
+
+ while (index < length && !changed) {
+ val segmentStart = index
+ val segmentEnd = index + segmentSize - 1
+ val separatorPosition = segmentEnd + 1
+
+ changed = formatSegment(input, segmentStart..segmentEnd) ||
+ formatSeparator(input, separatorPosition)
+
+ index = separatorPosition + 1
+ }
+ }
+
+ private fun formatSegment(input: Editable, range: IntRange): Boolean {
+ val length = input.length
+ val start = range.start
+ var end = range.endInclusive
+
+ if (start < length) {
+ end = minOf(end, length - 1)
+
+ for (index in start..end) {
+ val character = input[index]
+
+ if (allCaps && character >= 'a' && character <= 'z') {
+ input.replace(index, index + 1, character.toString().toUpperCase())
+ } else if (!isValidInputCharacter(character)) {
+ input.delete(index, index + 1)
+ } else {
+ // Only continue looping if no changes were made to the string
+ continue
+ }
+
+ // Abort loop because the input was edited and `afterTextChanged` will be called
+ // again
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private fun formatSeparator(input: Editable, index: Int): Boolean {
+ if (index < input.length && input[index] != '-') {
+ input.insert(index, "-")
+ return true
+ } else {
+ return false
+ }
+ }
+}
diff --git a/android/src/main/res/drawable/dialog_background.xml b/android/src/main/res/drawable/dialog_background.xml
index d0dee092f5..a552adc351 100644
--- a/android/src/main/res/drawable/dialog_background.xml
+++ b/android/src/main/res/drawable/dialog_background.xml
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <corners android:radius="11dp" />
- <solid android:color="@color/darkBlue" />
-</shape>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetTop="@dimen/dialog_margin"
+ android:insetLeft="@dimen/dialog_margin"
+ android:insetRight="@dimen/dialog_margin"
+ android:insetBottom="@dimen/dialog_margin">
+ <shape android:shape="rectangle">
+ <corners android:radius="11dp" />
+ <solid android:color="@color/darkBlue" />
+ </shape>
+</inset>
diff --git a/android/src/main/res/drawable/edit_text_background.xml b/android/src/main/res/drawable/edit_text_background.xml
new file mode 100644
index 0000000000..06252ac37c
--- /dev/null
+++ b/android/src/main/res/drawable/edit_text_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <item android:state_enabled="false">
+ <inset android:insetTop="1dp"
+ android:insetBottom="1dp"
+ android:insetLeft="1dp"
+ android:insetRight="1dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/edit_text_corner_radius" />
+ <solid android:color="@color/white20" />
+ </shape>
+ </inset>
+ </item>
+ <item android:state_enabled="true">
+ <inset android:insetTop="1dp"
+ android:insetBottom="1dp"
+ android:insetLeft="1dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/edit_text_corner_radius" />
+ <solid android:color="@color/white" />
+ </shape>
+ </inset>
+ </item>
+</selector>
diff --git a/android/src/main/res/layout/confirm_no_email.xml b/android/src/main/res/layout/confirm_no_email.xml
index 10955cf0f6..7ae862302f 100644
--- a/android/src/main/res/layout/confirm_no_email.xml
+++ b/android/src/main/res/layout/confirm_no_email.xml
@@ -1,7 +1,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:padding="16dp"
+ android:padding="30dp"
android:background="@drawable/dialog_background"
android:orientation="vertical"
android:gravity="left"
diff --git a/android/src/main/res/layout/redeem_voucher.xml b/android/src/main/res/layout/redeem_voucher.xml
new file mode 100644
index 0000000000..2728d543d3
--- /dev/null
+++ b/android/src/main/res/layout/redeem_voucher.xml
@@ -0,0 +1,55 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:mullvad="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="30dp"
+ android:background="@drawable/dialog_background"
+ android:orientation="vertical"
+ android:gravity="left"
+ android:elevation="2dp">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:layout_marginBottom="12dp"
+ android:textColor="@color/white80"
+ android:textSize="16sp"
+ android:text="@string/enter_voucher_code" />
+ <EditText android:id="@+id/voucher_code"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="14dp"
+ android:background="@drawable/edit_text_background"
+ android:singleLine="true"
+ android:imeActionLabel="@string/redeem"
+ android:imeOptions="flagNoPersonalizedLearning"
+ android:inputType="textCapCharacters"
+ android:textCursorDrawable="@drawable/text_input_cursor"
+ android:hint="@string/voucher_hint"
+ android:maxLength="19"
+ android:digits="0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ android:textAllCaps="true"
+ android:textColorHint="@color/blue40"
+ android:textColor="@color/blue"
+ android:textSize="13sp"
+ android:textStyle="bold" />
+ <TextView android:id="@+id/error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textColor="@color/red"
+ android:textSize="13sp"
+ android:textStyle="bold"
+ android:visibility="invisible" />
+ <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/redeem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="16dp"
+ mullvad:showSpinner="true"
+ mullvad:buttonColor="green"
+ mullvad:text="@string/redeem" />
+ <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/cancel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ mullvad:buttonColor="blue"
+ mullvad:text="@string/cancel" />
+</LinearLayout>
diff --git a/android/src/main/res/layout/welcome.xml b/android/src/main/res/layout/welcome.xml
index 521bfc4dff..34fe031622 100644
--- a/android/src/main/res/layout/welcome.xml
+++ b/android/src/main/res/layout/welcome.xml
@@ -53,7 +53,7 @@
android:textSize="13sp"
android:text="@string/here_is_your_account_number" />
<TextView android:id="@+id/account_number"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="11dp"
@@ -82,9 +82,15 @@
<net.mullvad.mullvadvpn.ui.widget.UrlButton android:id="@+id/buy_credit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
mullvad:buttonColor="green"
mullvad:text="@string/buy_credit"
mullvad:url="@string/account_url"
mullvad:withToken="true" />
+ <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/redeem_voucher"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ mullvad:buttonColor="green"
+ mullvad:text="@string/redeem_voucher" />
</LinearLayout>
</LinearLayout>
diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml
index 5d106210ea..4342bec6c5 100644
--- a/android/src/main/res/values/dimensions.xml
+++ b/android/src/main/res/values/dimensions.xml
@@ -3,7 +3,9 @@
<dimen name="city_row_padding">40dp</dimen>
<dimen name="relay_row_padding">60dp</dimen>
<dimen name="relay_list_divider">1dp</dimen>
+ <dimen name="dialog_margin">14dp</dimen>
<dimen name="account_input_corner_radius">4dp</dimen>
+ <dimen name="edit_text_corner_radius">4dp</dimen>
<dimen name="normal_button_height">44dp</dimen>
<dimen name="tall_button_height">64dp</dimen>
<dimen name="cell_switch_border_radius">16dp</dimen>
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 4e03f847ee..27d7350a0c 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -26,9 +26,16 @@
<string name="account_created">Account created</string>
<string name="congrats">Congrats!</string>
<string name="here_is_your_account_number">Here\'s your account number. Save it!</string>
- <string name="pay_to_start_using">To start using the app, you first need to add time to you
- account. Buy credit on our website.</string>
+ <string name="pay_to_start_using">To start using the app, you first need to add time to your
+ account. Either buy credit on our website or redeem a voucher.</string>
<string name="buy_credit">Buy credit</string>
+ <string name="redeem_voucher">Redeem voucher</string>
+ <string name="enter_voucher_code">Enter voucher code</string>
+ <string name="voucher_hint">XXXX-XXXX-XXXX-XXXX</string>
+ <string name="redeem">Redeem</string>
+ <string name="invalid_voucher">Voucher code is invalid.</string>
+ <string name="voucher_already_used">Voucher code has already been used.</string>
+ <string name="error_occurred">An error occurred.</string>
<string name="settings">Settings</string>
<string name="settings_account">Account</string>
<string name="less_than_a_day_left">less than a day left</string>
diff --git a/mullvad-jni/Cargo.toml b/mullvad-jni/Cargo.toml
index de207328b9..a778680d17 100644
--- a/mullvad-jni/Cargo.toml
+++ b/mullvad-jni/Cargo.toml
@@ -14,7 +14,7 @@ crate_type = ["cdylib"]
err-derive = "0.2.1"
futures = "0.1"
ipnetwork = "0.15"
-jnix = { version = "0.2.2", features = ["derive"] }
+jnix = { version = "0.2.3", features = ["derive"] }
jsonrpc-client-core = "0.5"
jsonrpc-core = "8"
lazy_static = "1"
diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs
index 15e27d3a11..cc2a836626 100644
--- a/mullvad-jni/src/classes.rs
+++ b/mullvad-jni/src/classes.rs
@@ -36,6 +36,8 @@ pub const CLASSES: &[&str] = &[
"net/mullvad/mullvadvpn/model/TunnelState$Connecting",
"net/mullvad/mullvadvpn/model/TunnelState$Disconnected",
"net/mullvad/mullvadvpn/model/TunnelState$Disconnecting",
+ "net/mullvad/mullvadvpn/model/VoucherSubmission",
+ "net/mullvad/mullvadvpn/model/VoucherSubmissionResult",
"net/mullvad/mullvadvpn/model/WireguardEndpointData",
"net/mullvad/mullvadvpn/service/MullvadDaemon",
"net/mullvad/mullvadvpn/service/MullvadVpnService",
diff --git a/mullvad-jni/src/daemon_interface.rs b/mullvad-jni/src/daemon_interface.rs
index 45454c2f72..c8294fa744 100644
--- a/mullvad-jni/src/daemon_interface.rs
+++ b/mullvad-jni/src/daemon_interface.rs
@@ -1,7 +1,7 @@
use futures::{sync::oneshot, Future};
use mullvad_daemon::{DaemonCommand, DaemonCommandSender};
use mullvad_types::{
- account::AccountData,
+ account::{AccountData, VoucherSubmission},
location::GeoIpLocation,
relay_constraints::RelaySettingsUpdate,
relay_list::RelayList,
@@ -210,6 +210,17 @@ impl DaemonInterface {
self.send_command(DaemonCommand::Shutdown)
}
+ pub fn submit_voucher(&self, voucher: String) -> Result<VoucherSubmission> {
+ let (tx, rx) = oneshot::channel();
+
+ self.send_command(DaemonCommand::SubmitVoucher(tx, voucher))?;
+
+ rx.wait()
+ .map_err(|_| Error::NoResponse)?
+ .wait()
+ .map_err(Error::RpcError)
+ }
+
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 c46c2ba99f..15b72f8f6b 100644
--- a/mullvad-jni/src/lib.rs
+++ b/mullvad-jni/src/lib.rs
@@ -18,7 +18,7 @@ use jnix::{
};
use mullvad_daemon::{exception_logging, logging, version, Daemon, DaemonCommandChannel};
use mullvad_rpc::{rest::Error as RestError, StatusCode};
-use mullvad_types::account::AccountData;
+use mullvad_types::account::{AccountData, VoucherSubmission};
use std::{
path::{Path, PathBuf},
ptr,
@@ -75,6 +75,33 @@ impl From<Result<AccountData, daemon_interface::Error>> for GetAccountDataResult
}
}
+#[derive(IntoJava)]
+#[jnix(package = "net.mullvad.mullvadvpn.model")]
+pub enum VoucherSubmissionResult {
+ Ok(VoucherSubmission),
+ InvalidVoucher,
+ VoucherAlreadyUsed,
+ RpcError,
+ OtherError,
+}
+
+impl From<Result<VoucherSubmission, daemon_interface::Error>> for VoucherSubmissionResult {
+ fn from(result: Result<VoucherSubmission, daemon_interface::Error>) -> Self {
+ match result {
+ Ok(submission) => VoucherSubmissionResult::Ok(submission),
+ Err(daemon_interface::Error::RpcError(RestError::ApiError(_, code))) => {
+ match code.as_str() {
+ "INVALID_VOUCHER" => VoucherSubmissionResult::InvalidVoucher,
+ "VOUCHER_USED" => VoucherSubmissionResult::VoucherAlreadyUsed,
+ _ => VoucherSubmissionResult::RpcError,
+ }
+ }
+ Err(daemon_interface::Error::RpcError(_)) => VoucherSubmissionResult::RpcError,
+ _ => VoucherSubmissionResult::OtherError,
+ }
+ }
+}
+
#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_initialize(
@@ -797,6 +824,35 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_shutdow
#[no_mangle]
#[allow(non_snake_case)]
+pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_submitVoucher<'env>(
+ env: JNIEnv<'env>,
+ _: JObject<'_>,
+ daemon_interface_address: jlong,
+ voucher: JString<'_>,
+) -> JObject<'env> {
+ let env = JnixEnv::from(env);
+
+ let result = if let Some(daemon_interface) = get_daemon_interface(daemon_interface_address) {
+ let voucher = String::from_java(&env, voucher);
+ let raw_result = daemon_interface.submit_voucher(voucher);
+
+ if let Err(ref error) = &raw_result {
+ log::error!(
+ "{}",
+ error.display_chain_with_msg("Failed to submit voucher code")
+ );
+ }
+
+ VoucherSubmissionResult::from(raw_result)
+ } else {
+ VoucherSubmissionResult::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/Cargo.toml b/mullvad-types/Cargo.toml
index 51226e37be..cd6114675e 100644
--- a/mullvad-types/Cargo.toml
+++ b/mullvad-types/Cargo.toml
@@ -21,4 +21,4 @@ talpid-types = { path = "../talpid-types" }
mullvad-paths = { path = "../mullvad-paths" }
[target.'cfg(target_os = "android")'.dependencies]
-jnix = { version = "0.2.2", features = ["derive"] }
+jnix = { version = "0.2.3", features = ["derive"] }
diff --git a/mullvad-types/src/account.rs b/mullvad-types/src/account.rs
index 67fb6ce2d0..f5dc450e76 100644
--- a/mullvad-types/src/account.rs
+++ b/mullvad-types/src/account.rs
@@ -17,11 +17,15 @@ pub struct AccountData {
/// Data structure that's returned from successful invocation of the mullvad API's
/// `/v1/submit-voucher` RPC.
-#[derive(serde::Deserialize, serde::Serialize, Debug)]
+#[derive(Deserialize, Serialize, Debug)]
+#[cfg_attr(target_os = "android", derive(IntoJava))]
+#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))]
pub struct VoucherSubmission {
/// Amount of time added to the account
+ #[cfg_attr(target_os = "android", jnix(map = "|time_added| time_added as i64"))]
pub time_added: u64,
/// Updated expiry time
+ #[cfg_attr(target_os = "android", jnix(map = "|expiry| expiry.to_string()"))]
pub new_expiry: DateTime<Utc>,
}
diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml
index 3e85b7dfe5..90a8ed9cf5 100644
--- a/talpid-core/Cargo.toml
+++ b/talpid-core/Cargo.toml
@@ -42,7 +42,7 @@ tokio-io = "0.1"
[target.'cfg(target_os = "android")'.dependencies]
-jnix = { version = "0.2.2", features = ["derive"] }
+jnix = { version = "0.2.3", features = ["derive"] }
rand = "0.7"
diff --git a/talpid-types/Cargo.toml b/talpid-types/Cargo.toml
index 61b9bb8db4..bdd0c9003c 100644
--- a/talpid-types/Cargo.toml
+++ b/talpid-types/Cargo.toml
@@ -16,4 +16,4 @@ rand = "0.7"
err-derive = "0.2.1"
[target.'cfg(target_os = "android")'.dependencies]
-jnix = { version = "0.2.2", features = ["derive"] }
+jnix = { version = "0.2.3", features = ["derive"] }