Added donate page

This commit is contained in:
partisan 2025-05-20 12:50:08 +02:00
parent 6af51d8fc8
commit 4ce065425b
8 changed files with 363 additions and 2 deletions

View file

@ -69,5 +69,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
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
}

View file

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<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" />
<application

View 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
}
}

View file

@ -78,6 +78,13 @@ class MainActivity : AppCompatActivity() {
.commit()
true
}
R.id.action_donate -> {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, DonateFragment())
.addToBackStack(null)
.commit()
true
}
else -> false
}.also {
binding.drawerLayout.closeDrawers()

View file

@ -14,6 +14,8 @@ class Preferences(private val context: Context) {
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
private const val BLACKLIST_ENABLED = "blacklist_enabled"
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_POPUP_POSITION = 333
@ -54,8 +56,23 @@ class Preferences(private val context: Context) {
}
var isBlacklistEnabled: Boolean
get() = prefs.getBoolean(BLACKLIST_ENABLED, false)
set(value) = prefs.edit { putBoolean(BLACKLIST_ENABLED, value) }
get() = prefs.getBoolean(BLACKLIST_ENABLED, false)
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
get() = prefs.getBoolean(POPUP_ENABLED, true)
@ -116,4 +133,12 @@ class Preferences(private val context: Context) {
}
blacklistedContacts = current
}
private fun generateAndStoreToken(): String {
val newToken = (1..16)
.map { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".random() }
.joinToString("")
prefs.edit().putString(DONATION_TOKEN, newToken).apply()
return newToken
}
}

View 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>

View 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>

View file

@ -27,6 +27,11 @@
android:id="@+id/section_about"
android:title="@string/about_name"
android:enabled="false" />
<item
android:id="@+id/action_donate"
android:title="Donate"
android:icon="@drawable/heart_24"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:title="@string/about_name"