Support Nanga Kathai: one-time in-app gifts in local currency

Three consumable products (nk_support_small/medium/large) via Google Play
and App Store billing; a 'give more' option emails support. FAQ, Terms and
Privacy updated: v1 is free, creator accounts come in a later update.
docs/support-payments.md lists the per-country prices and store setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-30 19:07:26 -04:00
parent d994fc733f
commit d257fc4c65
12 changed files with 327 additions and 8 deletions

View File

@ -33,7 +33,8 @@ Your liked songs, recently played list, sound settings and downloaded songs are
- To run the app: streaming, your profile and your preferences.
- To review creator applications and song reports.
- To keep the service safe and to fix problems.
- In future, to count song plays so that verified artists can share in subscription revenue. We will update this policy before that starts.
- To count song plays, so we can show what is popular and, in future, give artists listening statistics.
- If you make a gift to support the app, Google Play or the App Store processes the payment. We receive a confirmation but never your card or bank details.
We do not use your information for advertising.

View File

@ -29,7 +29,11 @@ Use "Is this your song?" on the song in the app, or write to support@nkbmusic.co
## Creators
Singers, lyricists and music directors can apply to become verified creators. Verification is manual and may require identity documents. Creators who upload songs confirm that they own them or have permission from everyone with rights in them, and accept the separate terms shown when they apply. Plans to share subscription revenue with verified creators will be described in those terms before they begin.
In a future update, singers, lyricists and music directors will be able to apply to become verified creators. Verification will be manual and may require identity documents. Creators who upload songs will confirm that they own them or have permission from everyone with rights in them, and will accept separate creator terms. Any way for listeners to support creators with money, and any fee we keep, will be set out in those terms before it begins.
## Supporting Nanga Kathai
You can make an optional one-time gift to support the app's development. Gifts are paid through Google Play or the App Store, in your local currency, and their terms and refund rules apply. A gift does not unlock any content or features: every song stays free for everyone.
## Acceptable use

55
docs/support-payments.md Normal file
View File

@ -0,0 +1,55 @@
# Support payments ("Support Nanga Kathai")
One-time gifts that support the app's development. They unlock nothing; every song stays free.
Payments go through Google Play and the App Store, which charge each listener in their own
currency and pay us (after their fee, usually 15% for small developers).
## Products to create
Create three **consumable** in-app products, with exactly these IDs, in both stores:
| Product ID | Name in the app |
|---------------------|----------------------------|
| `nk_support_small` | Supporter |
| `nk_support_medium` | Friend of Nanga Kathai |
| `nk_support_large` | Champion of Badaga music |
## Prices
Set the main countries by hand. Every other country can use the store's automatic conversion from
the US price. The App Store only offers fixed price points, so pick the nearest one.
| Country | Currency | Supporter | Friend | Champion |
|----------------|----------|-----------|---------|----------|
| India | INR | 300 | 500 | 1,000 |
| United States | USD | 10 | 25 | 50 |
| Canada | CAD | 10 | 25 | 50 |
| UAE | AED | 39 | 99 | 199 |
| Saudi Arabia | SAR | 39 | 99 | 199 |
| Qatar | QAR | 39 | 99 | 199 |
| Kuwait | KWD | 3 | 7.5 | 15 |
| Oman | OMR | 3.9 | 9.9 | 19.9 |
| Bahrain | BHD | 3.9 | 9.9 | 19.9 |
Gulf prices are about US$10 / $25 / $50. Bigger gifts and sponsorships go through
"Want to give more?" on the Support page, which emails support@nkbmusic.com.
## Google Play (Play Console)
1. Set up a payments profile (merchant account): Play Console > Setup > Payments profile.
2. Upload a build that includes billing (any build from now on) to the Internal testing track.
3. Monetize > Products > In-app products > Create product, once for each ID above. Set the US
price, then open "Edit prices" to set India, Canada and the Gulf countries by hand. Activate each one.
4. Add your Google account under Setup > License testing, so test purchases are free.
## App Store (App Store Connect)
1. Accept the Paid Apps agreement, and add bank and tax details under Agreements, Tax, and Banking.
2. In the app, open Monetization > In-App Purchases and press +. Choose Consumable, and use the same
IDs. Add a price schedule with a US base price, and set manual prices for India, Canada and the Gulf.
3. Add a review screenshot of the Support page. The products go to review with the first app version.
## In the app
Settings > Support Nanga Kathai shows the products, with prices exactly as the store returns them.
Until the products exist in the store, the page says support is unavailable on this phone.

View File

@ -24,6 +24,7 @@ import '../features/settings/document_screen.dart';
import '../features/settings/help_screen.dart';
import '../features/settings/report_screen.dart';
import '../features/settings/settings_screen.dart';
import '../features/support/support_screen.dart';
import '../features/welcome/splash_screen.dart';
import '../shared/widgets/app_shell.dart';
@ -86,6 +87,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(path: '/about', builder: (_, _) => const AboutScreen()),
GoRoute(path: '/settings', builder: (_, _) => const SettingsScreen()),
GoRoute(path: '/help', builder: (_, _) => const HelpScreen()),
GoRoute(path: '/support', builder: (_, _) => const SupportScreen()),
GoRoute(
path: '/privacy',
builder: (_, _) => const DocumentScreen(title: 'Privacy Policy', asset: 'assets/legal/privacy.md'),

View File

@ -7,9 +7,9 @@ import 'settings_screen.dart';
const _faq = [
(
'Is Nanga Kathai free?',
'Yes. The songs in the app today are free to stream with a free account. Guests can hear a 30-second '
'preview of each song. In future, new songs from verified creators may be part of a paid plan that '
'shares revenue with the artists.',
'Yes. Every song is free to stream with a free account, with no ads. Guests can hear a 30-second '
'preview of each song. If you would like to help, you can make a one-time gift from Settings > '
'Support Nanga Kathai.',
),
(
'How do I listen without internet?',
@ -45,7 +45,7 @@ const _faq = [
),
(
'I am a singer or music director. Can I publish my songs here?',
'Yes, that is why we built Nanga Kathai. A creator application in the app is coming soon. Until then, '
'Yes, that is why we built Nanga Kathai. Creator accounts are coming in a future update. Until then, '
'write to support@nkbmusic.com and we will get in touch.',
),
(

View File

@ -75,6 +75,9 @@ class SettingsScreen extends ConsumerWidget {
tile(Icons.sd_storage_outlined, 'Storage used',
subtitle: '${downloads.entries.length} songs · $mb MB',
onTap: downloads.entries.isEmpty ? null : () => _removeAll(context, ref)),
section('Support us'),
tile(Icons.favorite_outline, 'Support Nanga Kathai',
subtitle: 'Help keep Badaga music free', onTap: () => context.push('/support')),
section('Help'),
tile(Icons.help_outline, 'Help & FAQ', onTap: () => context.push('/help')),
tile(Icons.flag_outlined, 'Is this your song? Report a song', onTap: () => context.push('/report')),

View File

@ -0,0 +1,217 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../../app/theme.dart';
import '../settings/settings_screen.dart';
/// One-time "Support Nanga Kathai" amounts. Create these as consumable in-app products with the
/// same IDs in Play Console and App Store Connect; each store charges in the buyer's own
/// currency (₹, CA$, US$, AED, SAR…) and the app shows the price exactly as the store gives it.
const supportProductIds = {'nk_support_small', 'nk_support_medium', 'nk_support_large'};
/// Prices are set per country in the stores, e.g. India ₹300 / ₹500 / ₹1,000,
/// US and Canada $10 / $25 / $50 (see docs/support-payments.md).
const _labels = {
'nk_support_small': ('Supporter', Icons.favorite_outline),
'nk_support_medium': ('Friend of Nanga Kathai', Icons.music_note_outlined),
'nk_support_large': ('Champion of Badaga music', Icons.favorite_rounded),
};
enum SupportStatus { loading, ready, unavailable, buying, thanks, failed }
class SupportState {
const SupportState(this.status, [this.products = const []]);
final SupportStatus status;
final List<ProductDetails> products;
SupportState copyWith(SupportStatus status) => SupportState(status, products);
}
/// Started at app launch, so a purchase that finishes while the app was closed is still
/// completed (Google refunds purchases that are never acknowledged).
class SupportPurchases extends Notifier<SupportState> {
StreamSubscription<List<PurchaseDetails>>? _sub;
@override
SupportState build() {
_sub = InAppPurchase.instance.purchaseStream.listen(_onPurchases, onError: (_) {
state = state.copyWith(SupportStatus.failed);
});
ref.onDispose(() => _sub?.cancel());
return const SupportState(SupportStatus.loading);
}
Future<void> loadProducts() async {
if (state.products.isNotEmpty) return;
state = const SupportState(SupportStatus.loading);
try {
if (!await InAppPurchase.instance.isAvailable()) {
state = const SupportState(SupportStatus.unavailable);
return;
}
final res = await InAppPurchase.instance.queryProductDetails(supportProductIds);
final products = [...res.productDetails]..sort((a, b) => a.rawPrice.compareTo(b.rawPrice));
state = SupportState(products.isEmpty ? SupportStatus.unavailable : SupportStatus.ready, products);
} catch (_) {
state = const SupportState(SupportStatus.unavailable);
}
}
Future<void> buy(ProductDetails product) async {
state = state.copyWith(SupportStatus.buying);
try {
await InAppPurchase.instance.buyConsumable(purchaseParam: PurchaseParam(productDetails: product));
} catch (_) {
state = state.copyWith(SupportStatus.failed);
}
}
void reset() => state = state.copyWith(state.products.isEmpty ? SupportStatus.loading : SupportStatus.ready);
Future<void> _onPurchases(List<PurchaseDetails> purchases) async {
for (final p in purchases) {
switch (p.status) {
case PurchaseStatus.purchased || PurchaseStatus.restored:
state = state.copyWith(SupportStatus.thanks);
case PurchaseStatus.error:
state = state.copyWith(SupportStatus.failed);
case PurchaseStatus.canceled:
state = state.copyWith(SupportStatus.ready);
case PurchaseStatus.pending:
break;
}
if (p.pendingCompletePurchase) await InAppPurchase.instance.completePurchase(p);
}
}
}
final supportProvider = NotifierProvider<SupportPurchases, SupportState>(SupportPurchases.new);
class SupportScreen extends ConsumerStatefulWidget {
const SupportScreen({super.key});
@override
ConsumerState<SupportScreen> createState() => _SupportScreenState();
}
class _SupportScreenState extends ConsumerState<SupportScreen> {
@override
void initState() {
super.initState();
Future.microtask(() => ref.read(supportProvider.notifier).loadProducts());
}
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final support = ref.watch(supportProvider);
final notifier = ref.read(supportProvider.notifier);
if (support.status == SupportStatus.thanks) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.favorite_rounded, color: NkColors.sun, size: 64),
const SizedBox(height: 16),
Text('Thank you!', textAlign: TextAlign.center, style: text.headlineMedium),
const SizedBox(height: 8),
const Text(
'Your support keeps Badaga music free for everyone and helps us bring more artists to Nanga Kathai.',
textAlign: TextAlign.center,
style: TextStyle(color: NkColors.muted, height: 1.5),
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {
notifier.reset();
Navigator.of(context).maybePop();
},
child: const Text('Done'),
),
],
),
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Support Nanga Kathai')),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 32),
children: [
Text('Help keep Badaga music free', style: text.headlineSmall),
const SizedBox(height: 8),
const Text(
'Nanga Kathai is free, with no ads. If it brings you joy, a one-time gift helps us pay for the servers, '
'restore old recordings and build the tools that will let singers earn from their songs.',
style: TextStyle(color: NkColors.muted, height: 1.5),
),
const SizedBox(height: 20),
switch (support.status) {
SupportStatus.loading => const Padding(
padding: EdgeInsets.all(32),
child: Center(child: CircularProgressIndicator()),
),
SupportStatus.unavailable => const Card(
color: NkColors.surface,
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Support is not available on this phone right now. Please check that you are signed in to '
'Google Play or the App Store, and try again later.',
style: TextStyle(color: NkColors.muted, height: 1.5),
),
),
),
_ => Column(
children: [
for (final p in support.products)
Card(
color: NkColors.surface,
child: ListTile(
leading: Icon(_labels[p.id]?.$2 ?? Icons.favorite_outline, color: NkColors.sun),
title: Text(_labels[p.id]?.$1 ?? p.title,
style: const TextStyle(fontWeight: FontWeight.w700)),
trailing: FilledButton(
onPressed: support.status == SupportStatus.buying ? null : () => notifier.buy(p),
child: Text(p.price),
),
),
),
if (support.status == SupportStatus.failed)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text('The payment did not go through. You have not been charged.',
style: TextStyle(color: NkColors.muted)),
),
],
),
},
const SizedBox(height: 8),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.volunteer_activism_outlined, color: NkColors.sun),
title: const Text('Want to give more, or sponsor an album?'),
subtitle: const Text('Write to us at $supportEmail', style: TextStyle(color: NkColors.muted)),
onTap: () => contactSupport(subject: 'Supporting Nanga Kathai'),
),
const SizedBox(height: 8),
const Text(
'One-time payment through Google Play or the App Store, in your local currency. '
'Nothing renews and nothing is unlocked: every song stays free for everyone.',
style: TextStyle(color: NkColors.muted, fontSize: 12, height: 1.5),
),
],
),
);
}
}

View File

@ -12,6 +12,7 @@ import 'audio/player_providers.dart';
import 'audio/sound_settings.dart';
import 'core/env.dart';
import 'data/user_library.dart';
import 'features/support/support_screen.dart';
import 'offline/download_store.dart';
Future<void> main() async {
@ -53,6 +54,7 @@ class NangaKathaiApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(soundSettingsProvider); // apply the saved equalizer and boost at startup
ref.read(supportProvider); // finish any support payment that completed while the app was closed
final router = ref.watch(routerProvider);
// Guests hear a short preview of each song, then are invited to sign up.
ref.listen(positionProvider, (_, next) {

View File

@ -509,6 +509,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
in_app_purchase:
dependency: "direct main"
description:
name: in_app_purchase
sha256: d8317ec2ed6fe33199957d67ee67e08a4fd4871ef93b91d7611d8ee560892afc
url: "https://pub.dev"
source: hosted
version: "3.3.1"
in_app_purchase_android:
dependency: transitive
description:
name: in_app_purchase_android
sha256: b76ab601573f8a848e72d015496257235066ba3479bef6300482239f417e0d72
url: "https://pub.dev"
source: hosted
version: "0.5.5"
in_app_purchase_platform_interface:
dependency: transitive
description:
name: in_app_purchase_platform_interface
sha256: "0b0076cac8ce4fa7048f01e76af8b123aeb6a7c4e0dea2a5206d6664454f3e36"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
in_app_purchase_storekit:
dependency: transitive
description:
name: in_app_purchase_storekit
sha256: fb2744b928bb0fcbfc95d1833469b3f638a44670aac3e278695930d3727011b6
url: "https://pub.dev"
source: hosted
version: "0.4.16"
intl:
dependency: transitive
description:

View File

@ -51,6 +51,7 @@ dependencies:
http: ^1.6.0
url_launcher: ^6.3.3
connectivity_plus: ^7.3.2
in_app_purchase: ^3.3.1
dev_dependencies:
flutter_test:

View File

@ -22,7 +22,7 @@ strong{color:#FBF7EE} a{color:#F5B335} li{margin:4px 0} .brand{color:#F5B335;fon
<h2>What stays on your phone</h2>
<p>Your liked songs, recently played list, sound settings and downloaded songs are stored on your device. Downloaded songs are encrypted and can only be played inside Nanga Kathai. Removing the app removes them.</p>
<h2>How we use your information</h2>
<ul><li>To create and secure your account, including sending sign-in and verification codes by email.</li><li>To run the app: streaming, your profile and your preferences.</li><li>To review creator applications and song reports.</li><li>To keep the service safe and to fix problems.</li><li>In future, to count song plays so that verified artists can share in subscription revenue. We will update this policy before that starts.</li></ul>
<ul><li>To create and secure your account, including sending sign-in and verification codes by email.</li><li>To run the app: streaming, your profile and your preferences.</li><li>To review creator applications and song reports.</li><li>To keep the service safe and to fix problems.</li><li>To count song plays, so we can show what is popular and, in future, give artists listening statistics.</li><li>If you make a gift to support the app, Google Play or the App Store processes the payment. We receive a confirmation but never your card or bank details.</li></ul>
<p>We do not use your information for advertising.</p>
<h2>Who can see your information</h2>
<ul><li><strong>Our team:</strong> only the people who run Nanga Kathai, and only when needed.</li><li><strong>Service providers:</strong> our app runs on servers we operate, and email is sent through our own mail server. If we ever use an outside provider for hosting, email or payments, they may only use your data to provide that service to us.</li><li><strong>The law:</strong> we may disclose information if required by law or to protect the safety of our users.</li></ul>

View File

@ -20,7 +20,9 @@ strong{color:#FBF7EE} a{color:#F5B335} li{margin:4px 0} .brand{color:#F5B335;fon
<ul><li><strong>claim a song</strong> so we credit you correctly,</li><li><strong>ask us to correct</strong> its details, or</li><li><strong>ask us to remove it.</strong></li></ul>
<p>Use &quot;Is this your song?&quot; on the song in the app, or write to <a href="mailto:support@nkbmusic.com.">support@nkbmusic.com.</a> We review every request and act on genuine removal requests promptly.</p>
<h2>Creators</h2>
<p>Singers, lyricists and music directors can apply to become verified creators. Verification is manual and may require identity documents. Creators who upload songs confirm that they own them or have permission from everyone with rights in them, and accept the separate terms shown when they apply. Plans to share subscription revenue with verified creators will be described in those terms before they begin.</p>
<p>In a future update, singers, lyricists and music directors will be able to apply to become verified creators. Verification will be manual and may require identity documents. Creators who upload songs will confirm that they own them or have permission from everyone with rights in them, and will accept separate creator terms. Any way for listeners to support creators with money, and any fee we keep, will be set out in those terms before it begins.</p>
<h2>Supporting Nanga Kathai</h2>
<p>You can make an optional one-time gift to support the app&#x27;s development. Gifts are paid through Google Play or the App Store, in your local currency, and their terms and refund rules apply. A gift does not unlock any content or features: every song stays free for everyone.</p>
<h2>Acceptable use</h2>
<p>Please do not:</p>
<ul><li>break, overload or try to get around the app&#x27;s security, including its download protection;</li><li>use bots or scripts to play songs, create accounts or inflate play counts;</li><li>impersonate a singer or anyone else, or submit false claims or reports;</li><li>upload or send anything unlawful, hateful or harmful.</li></ul>