176 lines
9.5 KiB
TypeScript
176 lines
9.5 KiB
TypeScript
// storebound config for one app (profile "subscription-app": accounts plus auto-renewable subscriptions, an
|
|
// analytics SDK, no ads). Every answer below is a starting point: edit it to match your app. Credentials NEVER go
|
|
// here (env vars or .storebound/credentials.json, gitignored). Paths are relative to this file.
|
|
// The type import is optional: it only works when storebound is installed in the project (bun add -d storebound).
|
|
import type { ShipConfig } from 'storebound';
|
|
|
|
const config: ShipConfig = {
|
|
// Every compliance answer below is the TEMPLATE's, not yours. While an area is listed here, check / validate block
|
|
// and nothing sends it to a store. Derive the answers from the code (storebound scan --write; for an app that is
|
|
// already live: storebound onboard --declarations), answer the rest, then remove the area from this list.
|
|
unanswered: ['dataSafety', 'appPrivacy', 'reviewNotes', 'ageRating', 'accounts', 'reviewAccess'],
|
|
|
|
app: {
|
|
name: 'Example App',
|
|
profile: 'subscription-app',
|
|
copyright: '2026 Example Studio',
|
|
privacyPolicyUrl: 'https://example.com/privacy',
|
|
termsUrl: 'https://example.com/terms', // Terms of Use (EULA): in the app and the metadata (Apple 3.1.2)
|
|
supportUrl: 'https://example.com/support',
|
|
marketingUrl: 'https://example.com',
|
|
website: 'https://example.com',
|
|
contactEmail: 'support@example.com',
|
|
},
|
|
|
|
// Accounts (account-deletion skill). Remove this block if the app has no sign-up / sign-in at all.
|
|
// Apple 5.1.1(v): deletion must start inside the app. Google Play: in the app AND on a public web page.
|
|
accounts: {
|
|
enabled: true,
|
|
deletionUrl: 'https://example.com/delete-account', // public page, no login needed to request deletion
|
|
inApp: 'Settings > Account > Delete account', // goes into the App Review notes
|
|
// How people sign up: 'username' (a name picked in the app, no password, no email), 'username-password',
|
|
// 'email-password', 'email-link', 'phone', 'oauth', 'other'. `storebound scan` proposes the account data and
|
|
// dataSafety.accountCreation from it; only the 'email-*' methods mean an email address.
|
|
signUp: ['email-password'],
|
|
},
|
|
|
|
// Sign-in for store reviewers (review-access skill). The demo account itself lives ONLY in
|
|
// .storebound/credentials.json ("reviewDemoAccount") or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD.
|
|
reviewAccess: {
|
|
signInRequired: true, // false if every feature works without signing in
|
|
instructions: 'Sign in with the demo account. Sample data is already there; nothing needs a second device.',
|
|
},
|
|
|
|
// id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale
|
|
languages: [
|
|
{ id: 'en', play: 'en-US', apple: 'en-US' },
|
|
{ id: 'tr', play: 'tr-TR', apple: 'tr' },
|
|
],
|
|
|
|
play: {
|
|
packageName: 'com.example.app',
|
|
defaultLanguage: 'en-US',
|
|
listing: 'store/listing.md',
|
|
icon: 'store/icons/play-512.png',
|
|
},
|
|
|
|
apple: {
|
|
bundleId: 'com.example.app',
|
|
// name: 'Example: Short Tagline', // App Store name (max 30) when app.name is taken there ("already being used"); default app.name
|
|
listing: 'store/appstore.md',
|
|
// Export compliance (export-compliance skill): decide it, then uncomment. false ONLY if the app uses no encryption
|
|
// beyond the OS (HTTPS through the system, standard sign-in).
|
|
// usesNonExemptEncryption: false,
|
|
// ipad: true,
|
|
info: {
|
|
categories: { primary: 'PRODUCTIVITY', secondary: 'UTILITIES' },
|
|
ageRating: {}, // every content question NONE / false; change what applies (user-generated content, web access ...)
|
|
contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',
|
|
free: true,
|
|
reviewNotes: [
|
|
'Example App keeps lists and notes in sync across devices. Pro (yearly with a 1-week free trial, or monthly) unlocks unlimited lists, sharing and themes.',
|
|
'Subscriptions: Settings > Pro (or the paywall after the third list). Restore Purchases is on the paywall and in Settings > Pro.',
|
|
'Privacy policy and Terms of Use: on the paywall and in Settings > About.',
|
|
// With a remote paywall or feature flags (storebound scan warns, 2.3.1) you may add a line like:
|
|
// 'Paywall designs and the prices shown are tested remotely with <SDK>; what Pro unlocks does not change after review.',
|
|
// Google Play has no review-notes field: put the same line in reviewAccess.instructions (Play App content >
|
|
// App access, 'Any other information') when the app has a sign-in, and say in the full description what needs
|
|
// a paid subscription (Play: Deceptive behavior, Payments).
|
|
].join('\n\n'),
|
|
},
|
|
},
|
|
|
|
// Auto-renewable subscriptions (store-subscriptions skill): `storebound apple subscriptions`, `storebound play
|
|
// subscriptions`. Apple: one group, levels rank the plans (1 = highest). Play: one subscription per entry with one
|
|
// auto-renewing base plan. Prices: USD base, Apple equalizes from the USA, Play converts per region; overrides set a
|
|
// region by hand. The disclosure (price per period, renewal, how to cancel, Terms + Privacy links) belongs on the
|
|
// purchase screen and in the descriptions.
|
|
subscriptions: [
|
|
{
|
|
name: 'Example Pro',
|
|
listings: { en: { name: 'Example Pro' }, tr: { name: 'Example Pro' } },
|
|
subscriptions: [
|
|
{
|
|
id: 'com.example.app.pro_yearly', period: 'P1Y', level: 1, usd: 29.99,
|
|
listings: {
|
|
en: { title: 'Pro yearly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },
|
|
tr: { title: 'Pro yıllık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },
|
|
},
|
|
intro: { type: 'free', duration: 'P1W' }, // 1 week free trial for new subscribers
|
|
reviewScreenshot: 'store/iap-review/paywall.png',
|
|
},
|
|
{
|
|
id: 'com.example.app.pro_monthly', period: 'P1M', level: 2, usd: 3.99,
|
|
overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }],
|
|
listings: {
|
|
en: { title: 'Pro monthly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },
|
|
tr: { title: 'Pro aylık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },
|
|
},
|
|
reviewScreenshot: 'store/iap-review/paywall.png',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
// Google Play Data safety. Types not listed = not collected. Typical app with accounts + an analytics SDK acting as
|
|
// your processor (collected, not shared). Edit to match what your app and its SDKs really do.
|
|
dataSafety: {
|
|
collectsData: true,
|
|
encryptedInTransit: true,
|
|
// Sign-up methods. Google does not publish these ids: compare with Play Console > App content > Data safety >
|
|
// Export to CSV before the first import. The delete-account URL (PSL_ACCOUNT_DELETION_URL) comes from
|
|
// accounts.deletionUrl; `storebound check` names any other row Play requires.
|
|
accountCreation: ['PSL_ACM_USER_ID_PASSWORD'],
|
|
// Can people delete their data without deleting the account (a data-only page)? true needs partialDeletionUrl.
|
|
// Play's "can users request deletion?" is answered yes by accounts.deletionUrl. Ask the owner.
|
|
partialDeletion: false,
|
|
data: {
|
|
PSL_EMAIL: { shared: false, collect: ['functionality', 'account'] },
|
|
PSL_USER_ACCOUNT: { shared: false, collect: ['functionality', 'account'] },
|
|
PSL_USER_GENERATED_CONTENT: { shared: false, collect: ['functionality'] },
|
|
PSL_USER_INTERACTION: { shared: false, collect: ['analytics'] },
|
|
PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] },
|
|
PSL_PERFORMANCE_DIAGNOSTICS: { shared: false, collect: ['analytics'] },
|
|
PSL_DEVICE_ID: { shared: false, collect: ['analytics'] },
|
|
PSL_PURCHASE_HISTORY: { shared: false, collect: ['functionality'] }, // subscription status tied to the account
|
|
},
|
|
},
|
|
|
|
renders: 'store/renders',
|
|
|
|
shots: {
|
|
style: 'app', // calm gradient, device frame, headline + subline (game: sparkles and badges)
|
|
copy: 'store/shots/copy.json',
|
|
frames: 'store/shots/frames', // <lang>/phone/<name>.png, <lang>/tablet/<name>.png
|
|
out: 'store/renders',
|
|
// fonts: { title: 'Inter', body: 'Inter' }, // the app style default
|
|
scenes: [
|
|
{ id: 'home', palette: 'mist', frames: [{ src: 'home.png' }] },
|
|
{ id: 'detail', palette: 'mint', frames: [{ src: 'detail.png' }] },
|
|
{ id: 'share', palette: 'sky', frames: [{ src: 'share.png' }] },
|
|
{ id: 'privacy', palette: 'slate', frames: [{ src: 'settings.png' }] },
|
|
],
|
|
feature: { frame: 'home.png', palette: 'mist' },
|
|
// Raw frames from a web build, the iOS Simulator (ios-sim) or an Android emulator (android-emu): sign in to a demo
|
|
// account with realistic sample data, never real user data (store-screenshots skill).
|
|
capture: {
|
|
baseUrl: 'http://127.0.0.1:5173',
|
|
query: 'lang={lang}&demo=1',
|
|
shots: {
|
|
'home': { path: '/', delayMs: 1000, device: 'both' },
|
|
'detail': { path: '/lists/1', delayMs: 1000, device: 'both' },
|
|
'share': { path: '/lists/1/share', delayMs: 1000, device: 'both' },
|
|
'settings': { path: '/settings', delayMs: 1000, device: 'both' },
|
|
},
|
|
},
|
|
},
|
|
|
|
icons: {
|
|
composite: 'store/icon/icon.png',
|
|
foreground: 'store/icon/foreground.png',
|
|
background: 'store/icon/background.png',
|
|
out: 'store/icons',
|
|
},
|
|
};
|
|
|
|
export default config;
|