Added donate page
This commit is contained in:
parent
6af51d8fc8
commit
4ce065425b
8 changed files with 363 additions and 2 deletions
|
@ -69,5 +69,7 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation "androidx.browser:browser:1.7.0"
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||||
implementation 'nl.dionsegijn:konfetti-xml:2.0.2' // This library holds the fabric of reality together please dont remove it at any costs >:3
|
implementation 'nl.dionsegijn:konfetti-xml:2.0.2' // This library holds the fabric of reality together please dont remove it at any costs >:3
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|
204
app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt
Normal file
204
app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package partisan.weforge.xyz.pulse
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import okhttp3.*
|
||||||
|
import partisan.weforge.xyz.pulse.databinding.FragmentDonateBinding
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
class DonateFragment : Fragment() {
|
||||||
|
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
private val apiBase = "https://api.weforge.xyz/api"
|
||||||
|
private var _binding: FragmentDonateBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var prefs: Preferences
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentDonateBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
(requireActivity() as? MainActivity)?.setAppBarTitle(
|
||||||
|
getString(R.string.about_name, R.string.donate_name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
prefs = Preferences(requireContext())
|
||||||
|
|
||||||
|
// Pre-fill token field
|
||||||
|
binding.tokenInput.setText(prefs.donationToken)
|
||||||
|
|
||||||
|
// Show toast and open Ko-fi
|
||||||
|
binding.kofiButton.setOnClickListener {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
"Make sure to include your token in the donation message to get rewarded 😊",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
val customTab = CustomTabsIntent.Builder().build()
|
||||||
|
customTab.launchUrl(requireContext(), Uri.parse("https://ko-fi.com/internetaddict"))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tokenDisplay.text = "token:${prefs.donationToken}"
|
||||||
|
binding.tokenDisplay.setOnClickListener {
|
||||||
|
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||||
|
val clip = android.content.ClipData.newPlainText("Ko-fi token", "token:${prefs.donationToken}")
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast.makeText(context, "Token copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show token entry section
|
||||||
|
binding.openTokenSection.setOnClickListener {
|
||||||
|
binding.tokenSection.visibility = View.VISIBLE
|
||||||
|
binding.openTokenSection.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.verifyButton.setOnClickListener {
|
||||||
|
var token = binding.tokenInput.text.toString().trim()
|
||||||
|
|
||||||
|
// Strip optional "token:" prefix
|
||||||
|
if (token.startsWith("token:")) {
|
||||||
|
token = token.removePrefix("token:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token format
|
||||||
|
if (token.length != 16) {
|
||||||
|
Toast.makeText(context, "Invalid token format", Toast.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.donationToken = token
|
||||||
|
|
||||||
|
if (prefs.isDonationActivated) {
|
||||||
|
binding.resultText.text = "✅ Already activated"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 0: Check INTERNET permission
|
||||||
|
if (!hasInternetPermission(requireContext())) {
|
||||||
|
binding.resultText.text = "❌ Missing INTERNET permission"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Try activation server first
|
||||||
|
val aliveRequest = Request.Builder().url("$apiBase/alive").build()
|
||||||
|
client.newCall(aliveRequest).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
// If server unreachable, fallback to internet check
|
||||||
|
val internetCheck = Request.Builder()
|
||||||
|
.url("https://deb.debian.org/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(internetCheck).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ No internet access"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
if (!response.isSuccessful || response.body?.string().isNullOrBlank()) {
|
||||||
|
binding.resultText.text = "❌ No internet access"
|
||||||
|
} else {
|
||||||
|
binding.resultText.text = "❌ Activation server is unreachable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.body?.string()?.trim() != "true") {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ Server not responding"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Check token
|
||||||
|
val checkRequest = Request.Builder()
|
||||||
|
.url("$apiBase/check?token=$token")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(checkRequest).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ Could not check token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val result = response.body?.string()?.trim()
|
||||||
|
if (result == "0") {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ Invalid or expired token"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Activate
|
||||||
|
val activateRequest = Request.Builder()
|
||||||
|
.url("$apiBase/activate?token=$token")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(activateRequest).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ Activation failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val activateResult = response.body?.string()?.trim()
|
||||||
|
if (activateResult == "success") {
|
||||||
|
prefs.isDonationActivated = true
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text =
|
||||||
|
"✅ Token activated! You had $result activations left."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
binding.resultText.text = "❌ Activation failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasInternetPermission(context: Context): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.INTERNET
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
.commit()
|
.commit()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_donate -> {
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragmentContainer, DonateFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}.also {
|
}.also {
|
||||||
binding.drawerLayout.closeDrawers()
|
binding.drawerLayout.closeDrawers()
|
||||||
|
|
|
@ -14,6 +14,8 @@ class Preferences(private val context: Context) {
|
||||||
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
|
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
|
||||||
private const val BLACKLIST_ENABLED = "blacklist_enabled"
|
private const val BLACKLIST_ENABLED = "blacklist_enabled"
|
||||||
private val SERVICE_ORDER_KEY = "service_order"
|
private val SERVICE_ORDER_KEY = "service_order"
|
||||||
|
private const val DONATION_ACTIVATED = "donation_activated"
|
||||||
|
private const val DONATION_TOKEN = "donation_token"
|
||||||
|
|
||||||
private const val DEFAULT_REDIRECTION_DELAY = 2000L
|
private const val DEFAULT_REDIRECTION_DELAY = 2000L
|
||||||
private const val DEFAULT_POPUP_POSITION = 333
|
private const val DEFAULT_POPUP_POSITION = 333
|
||||||
|
@ -57,6 +59,21 @@ class Preferences(private val context: Context) {
|
||||||
get() = prefs.getBoolean(BLACKLIST_ENABLED, false)
|
get() = prefs.getBoolean(BLACKLIST_ENABLED, false)
|
||||||
set(value) = prefs.edit { putBoolean(BLACKLIST_ENABLED, value) }
|
set(value) = prefs.edit { putBoolean(BLACKLIST_ENABLED, value) }
|
||||||
|
|
||||||
|
var isDonationActivated: Boolean
|
||||||
|
get() = prefs.getBoolean(DONATION_ACTIVATED, false)
|
||||||
|
set(value) = prefs.edit { putBoolean(DONATION_ACTIVATED, value) }
|
||||||
|
|
||||||
|
var donationToken: String
|
||||||
|
get() {
|
||||||
|
val stored = prefs.getString(DONATION_TOKEN, null)
|
||||||
|
return if (stored != null) {
|
||||||
|
stored
|
||||||
|
} else {
|
||||||
|
generateAndStoreToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set(value) = prefs.edit { putString(DONATION_TOKEN, value) }
|
||||||
|
|
||||||
var popupEnabled: Boolean
|
var popupEnabled: Boolean
|
||||||
get() = prefs.getBoolean(POPUP_ENABLED, true)
|
get() = prefs.getBoolean(POPUP_ENABLED, true)
|
||||||
set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) }
|
set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) }
|
||||||
|
@ -116,4 +133,12 @@ class Preferences(private val context: Context) {
|
||||||
}
|
}
|
||||||
blacklistedContacts = current
|
blacklistedContacts = current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateAndStoreToken(): String {
|
||||||
|
val newToken = (1..16)
|
||||||
|
.map { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".random() }
|
||||||
|
.joinToString("")
|
||||||
|
prefs.edit().putString(DONATION_TOKEN, newToken).apply()
|
||||||
|
return newToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
11
app/src/main/res/drawable/heart_filled_24.xml
Normal file
11
app/src/main/res/drawable/heart_filled_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,840L422,788Q321,697 255,631Q189,565 150,512.5Q111,460 95.5,416Q80,372 80,326Q80,232 143,169Q206,106 300,106Q352,106 399,128Q446,150 480,190Q514,150 561,128Q608,106 660,106Q754,106 817,169Q880,232 880,326Q880,372 864.5,416Q849,460 810,512.5Q771,565 705,631Q639,697 538,788L480,840Z"/>
|
||||||
|
</vector>
|
||||||
|
|
106
app/src/main/res/layout/fragment_donate.xml
Normal file
106
app/src/main/res/layout/fragment_donate.xml
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/donate_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<!-- Intro section -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Support Pulse Development 💖"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/descText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Pulse is free and open-source. You can support future development through Ko-fi. As a thank-you, donors get special animation effects!"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tokenDisplay"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text=""
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/kofiButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Donate via Ko-fi 💙"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/postDonatePrompt"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Already donated? Tap below to activate your token."
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/openTokenSection"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="I have a token"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Token activation section (initially hidden) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/tokenSection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/instruction"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Enter your Ko-fi token:"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/tokenInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="token:abcd1234efgh5678"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLength="32"
|
||||||
|
android:layout_marginBottom="12dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/verifyButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Activate Token" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/resultText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text=""
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
|
@ -27,6 +27,11 @@
|
||||||
android:id="@+id/section_about"
|
android:id="@+id/section_about"
|
||||||
android:title="@string/about_name"
|
android:title="@string/about_name"
|
||||||
android:enabled="false" />
|
android:enabled="false" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_donate"
|
||||||
|
android:title="Donate"
|
||||||
|
android:icon="@drawable/heart_24"
|
||||||
|
app:showAsAction="never" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_about"
|
android:id="@+id/action_about"
|
||||||
android:title="@string/about_name"
|
android:title="@string/about_name"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue