feat: ship ad-supported only — gate Remove Ads IAP behind kIapEnabled=false
The developer is an individual without a Korean business registration, so the App Store / Play paid-apps (merchant) agreements can't be completed. Hide the Remove Ads + Restore tiles and skip IAP init; ads always show. AdMob revenue is independent of those agreements. Reversible: flip kIapEnabled to re-enable once a merchant agreement exists. Bump to build 2; drop the now-unused IAP review-screenshot generator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
google.com, pub-5605900229781491, DIRECT, f08c47fec0942fa0
|
||||
+63
-83
@@ -1,89 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Welcome to Firebase Hosting</title>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — 시즌마다 새로워지는 블록 퍼즐</title>
|
||||
<meta name="description" content="Block Seasons는 8×8 보드에 블록을 놓아 줄을 지우고, 몇 주마다 새 테마 시즌을 즐기는 편안한 블록 퍼즐입니다.">
|
||||
<style>
|
||||
:root{ --navy:#0E1430; --navy2:#1B2350; --accent:#5B7FFF; --ink:#EAF0FF; --muted:#9DA9C7; }
|
||||
*{ box-sizing:border-box; }
|
||||
body{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans KR",sans-serif;
|
||||
background:linear-gradient(160deg,var(--navy),var(--navy2)); color:var(--ink); line-height:1.6; }
|
||||
.wrap{ max-width:680px; margin:0 auto; padding:56px 24px 72px; }
|
||||
.mark{ display:flex; gap:6px; margin-bottom:28px; }
|
||||
.mark span{ width:30px; height:30px; border-radius:8px; box-shadow:inset 0 -3px 0 rgba(0,0,0,.18), 0 2px 6px rgba(0,0,0,.3); }
|
||||
.b1{ background:#6E8BFF; } .b2{ background:#F4B6C2; } .b3{ background:#7FD4C0; } .b4{ background:#F6CF76; }
|
||||
h1{ font-size:2.2rem; margin:0 0 6px; letter-spacing:-.5px; }
|
||||
.tag{ color:var(--accent); font-weight:600; margin:0 0 28px; font-size:1.05rem; }
|
||||
p{ color:var(--ink); }
|
||||
.lead{ font-size:1.05rem; }
|
||||
ul{ padding-left:1.1rem; } li{ margin:.3rem 0; color:var(--ink); }
|
||||
.muted{ color:var(--muted); }
|
||||
h2{ font-size:1.1rem; margin:2.4rem 0 .6rem; color:#fff; }
|
||||
.links{ display:flex; flex-wrap:wrap; gap:12px; margin:30px 0 8px; }
|
||||
.links a{ display:inline-block; text-decoration:none; padding:12px 20px; border-radius:10px;
|
||||
background:var(--accent); color:#fff; font-weight:600; }
|
||||
.links a.alt{ background:transparent; border:1px solid rgba(255,255,255,.25); color:var(--ink); }
|
||||
hr{ border:none; border-top:1px solid rgba(255,255,255,.12); margin:40px 0 24px; }
|
||||
footer{ color:var(--muted); font-size:.86rem; }
|
||||
a.inline{ color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- update the version number as needed -->
|
||||
<script defer src="/__/firebase/12.14.0/firebase-app-compat.js"></script>
|
||||
<!-- include only the Firebase features as you need -->
|
||||
<script defer src="/__/firebase/12.14.0/firebase-auth-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-database-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-firestore-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-functions-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-messaging-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-storage-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-analytics-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-remote-config-compat.js"></script>
|
||||
<script defer src="/__/firebase/12.14.0/firebase-performance-compat.js"></script>
|
||||
<!--
|
||||
initialize the SDK after all desired features are loaded, set useEmulator to false
|
||||
to avoid connecting the SDK to running emulators.
|
||||
-->
|
||||
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
|
||||
<div class="mark"><span class="b1"></span><span class="b2"></span><span class="b3"></span><span class="b4"></span></div>
|
||||
|
||||
<style media="screen">
|
||||
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
||||
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
|
||||
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
||||
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
||||
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
||||
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
||||
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
||||
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
||||
@media (max-width: 600px) {
|
||||
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
||||
body { border-top: 16px solid #ffa100; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message">
|
||||
<h2>Welcome</h2>
|
||||
<h1>Firebase Hosting Setup Complete</h1>
|
||||
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
|
||||
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
|
||||
</div>
|
||||
<p id="load">Firebase SDK Loading…</p>
|
||||
<h1>Block Seasons</h1>
|
||||
<p class="tag">시즌마다 새로워지는 블록 퍼즐 · A seasonal block puzzle</p>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loadEl = document.querySelector('#load');
|
||||
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
// // The Firebase SDK is initialized and available here!
|
||||
//
|
||||
// firebase.auth().onAuthStateChanged(user => { });
|
||||
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
|
||||
// firebase.firestore().doc('/foo/bar').get().then(() => { });
|
||||
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
|
||||
// firebase.messaging().requestPermission().then(() => { });
|
||||
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
|
||||
// firebase.analytics(); // call to activate
|
||||
// firebase.analytics().logEvent('tutorial_completed');
|
||||
// firebase.performance(); // call to activate
|
||||
//
|
||||
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
<p class="lead">8×8 보드에 세 조각을 드래그해 가로·세로 줄을 지우는, 편안하고 예쁜 블록 퍼즐입니다.
|
||||
몇 주마다 새 테마의 시즌과 스테이지가 앱 업데이트 없이 도착하고, 시즌 1은 오프라인으로도 즐길 수 있어요.</p>
|
||||
|
||||
try {
|
||||
let app = firebase.app();
|
||||
let features = [
|
||||
'auth',
|
||||
'database',
|
||||
'firestore',
|
||||
'functions',
|
||||
'messaging',
|
||||
'storage',
|
||||
'analytics',
|
||||
'remoteConfig',
|
||||
'performance',
|
||||
].filter(feature => typeof app[feature] === 'function');
|
||||
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
<ul>
|
||||
<li>시즌제 — 몇 주마다 새 테마와 스테이지</li>
|
||||
<li>일러스트 여정 맵 + 엔드리스 모드</li>
|
||||
<li>광고 강요 없는 공정한 설계, 일회성 ‘광고 제거’ 지원</li>
|
||||
<li>오프라인 플레이 (시즌 1 내장)</li>
|
||||
</ul>
|
||||
|
||||
<p class="muted">A cozy 8×8 block puzzle. Drop three pieces, clear lines, and enjoy a fresh themed
|
||||
season every few weeks — no app update needed. Season 1 plays fully offline.</p>
|
||||
|
||||
<div class="links">
|
||||
<a href="mailto:airkjw@gmail.com">문의 / Contact</a>
|
||||
<a class="alt" href="/privacy-policy.html">개인정보처리방침 / Privacy</a>
|
||||
</div>
|
||||
|
||||
<h2>지원 / Support</h2>
|
||||
<p class="muted">문의 사항은 <a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a> 으로 보내주세요.
|
||||
보통 2~3일 내에 답변드립니다. · For support, email
|
||||
<a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a>.</p>
|
||||
|
||||
<hr>
|
||||
<footer>© 2026 Joungwook Kwon · Block Seasons</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — Privacy Policy / 개인정보처리방침</title>
|
||||
<style>
|
||||
body{max-width:760px;margin:0 auto;padding:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#1a1a2e}
|
||||
h1{font-size:1.5rem} h2{font-size:1.15rem;margin-top:2rem;border-bottom:1px solid #ddd;padding-bottom:4px}
|
||||
h3{font-size:1rem;margin-top:1.4rem} code{background:#f0f0f5;padding:1px 5px;border-radius:4px}
|
||||
.meta{color:#666;font-size:.9rem} hr{margin:3rem 0;border:none;border-top:2px solid #eee}
|
||||
a{color:#3a5fcd}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ========================= KOREAN ========================= -->
|
||||
<h1>Block Seasons 개인정보처리방침</h1>
|
||||
<p class="meta">최종 업데이트: 2026년 6월 14일 · 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>본 방침은 모바일 게임 <strong>Block Seasons</strong>(이하 "본 앱")의 개인정보 처리에 관한 내용을 설명합니다. 본 앱은 계정 가입이 필요 없으며, 이름·이메일 등 직접적인 개인 식별 정보를 수집하지 않습니다.</p>
|
||||
|
||||
<h2>1. 수집하는 정보</h2>
|
||||
<ul>
|
||||
<li><strong>광고 식별자</strong> (Android 광고 ID / Apple IDFA): 광고 게재 및 측정을 위해 광고 파트너(Google AdMob)가 사용합니다.</li>
|
||||
<li><strong>사용 데이터</strong> (앱 이용 통계, 화면·이벤트 상호작용): 앱 품질 개선과 분석을 위해 Firebase Analytics가 수집합니다.</li>
|
||||
<li><strong>기기 정보</strong> (기기 모델, 운영체제 버전, 대략적 지역 등): 광고·분석의 기본 진단 정보로 사용됩니다.</li>
|
||||
</ul>
|
||||
<p>본 앱 개발자는 위 정보를 통해 개인을 식별하지 않으며, 별도의 서버에 개인정보를 저장하지 않습니다. 게임 진행·설정은 기기 내부(로컬)에만 저장됩니다.</p>
|
||||
|
||||
<h2>2. 정보 이용 목적</h2>
|
||||
<ul>
|
||||
<li>광고 게재 및 수익 창출 (무료 제공을 위한 광고 기반 모델)</li>
|
||||
<li>앱 사용성 분석 및 기능·난이도 개선</li>
|
||||
<li>오류 진단 및 안정성 향상</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 제3자 제공 및 처리</h2>
|
||||
<p>본 앱은 다음 제3자 서비스를 사용하며, 해당 서비스의 정책에 따라 정보가 처리됩니다.</p>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (광고) — <a href="https://policies.google.com/privacy">Google 개인정보처리방침</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (분석) — <a href="https://firebase.google.com/support/privacy">Firebase 개인정보 보호</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. 추적 및 맞춤 광고 (iOS)</h2>
|
||||
<p>iOS에서는 앱 실행 시 <strong>추적 허용(App Tracking Transparency)</strong> 동의를 요청합니다. 동의하면 맞춤형 광고가 제공될 수 있고, 거부해도 본 앱의 모든 기능을 정상적으로 이용할 수 있으며 비맞춤형 광고가 표시됩니다.</p>
|
||||
|
||||
<h2>5. 인앱 구매</h2>
|
||||
<p>일회성 "광고 제거(Remove Ads)" 인앱 구매를 제공합니다. 결제는 Apple App Store 또는 Google Play를 통해 처리되며, 개발자는 결제 카드 등 결제 수단 정보를 수집하거나 보관하지 않습니다.</p>
|
||||
|
||||
<h2>6. 아동의 개인정보</h2>
|
||||
<p>본 앱은 만 13세 미만 아동을 주 대상으로 하지 않으며, 아동의 개인정보를 고의로 수집하지 않습니다.</p>
|
||||
|
||||
<h2>7. 데이터 보관 및 삭제</h2>
|
||||
<p>로컬 저장 데이터는 앱 삭제 시 함께 제거됩니다. 광고/분석 데이터의 처리·삭제는 위 제3자 정책을 따릅니다. 관련 문의는 아래 이메일로 연락 주십시오.</p>
|
||||
|
||||
<h2>8. 문의</h2>
|
||||
<p>개인정보 관련 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ========================= ENGLISH ========================= -->
|
||||
<h1>Block Seasons Privacy Policy</h1>
|
||||
<p class="meta">Last updated: June 14, 2026 · Contact: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>This policy describes how the mobile game <strong>Block Seasons</strong> ("the App") handles information. The App requires no account sign-up and does not collect directly identifying personal information such as your name or email.</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<ul>
|
||||
<li><strong>Advertising identifier</strong> (Android Advertising ID / Apple IDFA): used by our advertising partner (Google AdMob) to serve and measure ads.</li>
|
||||
<li><strong>Usage data</strong> (app interaction, screen and event analytics): collected by Firebase Analytics to improve app quality.</li>
|
||||
<li><strong>Device information</strong> (device model, OS version, coarse region): used for advertising and analytics diagnostics.</li>
|
||||
</ul>
|
||||
<p>The developer does not use this information to identify you personally and stores no personal data on its own servers. Game progress and settings are stored only locally on your device.</p>
|
||||
|
||||
<h2>2. How We Use Information</h2>
|
||||
<ul>
|
||||
<li>To serve ads and generate revenue (an ad-supported free model)</li>
|
||||
<li>To analyze usage and improve features and difficulty</li>
|
||||
<li>To diagnose errors and improve stability</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Third Parties</h2>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (advertising) — <a href="https://policies.google.com/privacy">Google Privacy Policy</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (analytics) — <a href="https://firebase.google.com/support/privacy">Firebase Privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Tracking & Personalized Ads (iOS)</h2>
|
||||
<p>On iOS the App requests <strong>App Tracking Transparency</strong> permission. If you allow it, personalized ads may be shown. If you decline, the App works fully and shows non-personalized ads.</p>
|
||||
|
||||
<h2>5. In-App Purchases</h2>
|
||||
<p>A one-time "Remove Ads" purchase is offered. Payment is handled by the Apple App Store or Google Play; the developer does not collect or store your payment details.</p>
|
||||
|
||||
<h2>6. Children's Privacy</h2>
|
||||
<p>The App is not primarily directed at children under 13 and does not knowingly collect personal information from children.</p>
|
||||
|
||||
<h2>7. Data Retention & Deletion</h2>
|
||||
<p>Locally stored data is removed when the App is uninstalled. Advertising and analytics data follow the third-party policies above. For requests, contact us below.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>Privacy inquiries: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
google.com, pub-5605900229781491, DIRECT, f08c47fec0942fa0
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — Privacy Policy / 개인정보처리방침</title>
|
||||
<style>
|
||||
body{max-width:760px;margin:0 auto;padding:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#1a1a2e}
|
||||
h1{font-size:1.5rem} h2{font-size:1.15rem;margin-top:2rem;border-bottom:1px solid #ddd;padding-bottom:4px}
|
||||
h3{font-size:1rem;margin-top:1.4rem} code{background:#f0f0f5;padding:1px 5px;border-radius:4px}
|
||||
.meta{color:#666;font-size:.9rem} hr{margin:3rem 0;border:none;border-top:2px solid #eee}
|
||||
a{color:#3a5fcd}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ========================= KOREAN ========================= -->
|
||||
<h1>Block Seasons 개인정보처리방침</h1>
|
||||
<p class="meta">최종 업데이트: 2026년 6월 14일 · 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>본 방침은 모바일 게임 <strong>Block Seasons</strong>(이하 "본 앱")의 개인정보 처리에 관한 내용을 설명합니다. 본 앱은 계정 가입이 필요 없으며, 이름·이메일 등 직접적인 개인 식별 정보를 수집하지 않습니다.</p>
|
||||
|
||||
<h2>1. 수집하는 정보</h2>
|
||||
<ul>
|
||||
<li><strong>광고 식별자</strong> (Android 광고 ID / Apple IDFA): 광고 게재 및 측정을 위해 광고 파트너(Google AdMob)가 사용합니다.</li>
|
||||
<li><strong>사용 데이터</strong> (앱 이용 통계, 화면·이벤트 상호작용): 앱 품질 개선과 분석을 위해 Firebase Analytics가 수집합니다.</li>
|
||||
<li><strong>기기 정보</strong> (기기 모델, 운영체제 버전, 대략적 지역 등): 광고·분석의 기본 진단 정보로 사용됩니다.</li>
|
||||
</ul>
|
||||
<p>본 앱 개발자는 위 정보를 통해 개인을 식별하지 않으며, 별도의 서버에 개인정보를 저장하지 않습니다. 게임 진행·설정은 기기 내부(로컬)에만 저장됩니다.</p>
|
||||
|
||||
<h2>2. 정보 이용 목적</h2>
|
||||
<ul>
|
||||
<li>광고 게재 및 수익 창출 (무료 제공을 위한 광고 기반 모델)</li>
|
||||
<li>앱 사용성 분석 및 기능·난이도 개선</li>
|
||||
<li>오류 진단 및 안정성 향상</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 제3자 제공 및 처리</h2>
|
||||
<p>본 앱은 다음 제3자 서비스를 사용하며, 해당 서비스의 정책에 따라 정보가 처리됩니다.</p>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (광고) — <a href="https://policies.google.com/privacy">Google 개인정보처리방침</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (분석) — <a href="https://firebase.google.com/support/privacy">Firebase 개인정보 보호</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. 추적 및 맞춤 광고 (iOS)</h2>
|
||||
<p>iOS에서는 앱 실행 시 <strong>추적 허용(App Tracking Transparency)</strong> 동의를 요청합니다. 동의하면 맞춤형 광고가 제공될 수 있고, 거부해도 본 앱의 모든 기능을 정상적으로 이용할 수 있으며 비맞춤형 광고가 표시됩니다.</p>
|
||||
|
||||
<h2>5. 인앱 구매</h2>
|
||||
<p>일회성 "광고 제거(Remove Ads)" 인앱 구매를 제공합니다. 결제는 Apple App Store 또는 Google Play를 통해 처리되며, 개발자는 결제 카드 등 결제 수단 정보를 수집하거나 보관하지 않습니다.</p>
|
||||
|
||||
<h2>6. 아동의 개인정보</h2>
|
||||
<p>본 앱은 만 13세 미만 아동을 주 대상으로 하지 않으며, 아동의 개인정보를 고의로 수집하지 않습니다.</p>
|
||||
|
||||
<h2>7. 데이터 보관 및 삭제</h2>
|
||||
<p>로컬 저장 데이터는 앱 삭제 시 함께 제거됩니다. 광고/분석 데이터의 처리·삭제는 위 제3자 정책을 따릅니다. 관련 문의는 아래 이메일로 연락 주십시오.</p>
|
||||
|
||||
<h2>8. 문의</h2>
|
||||
<p>개인정보 관련 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ========================= ENGLISH ========================= -->
|
||||
<h1>Block Seasons Privacy Policy</h1>
|
||||
<p class="meta">Last updated: June 14, 2026 · Contact: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>This policy describes how the mobile game <strong>Block Seasons</strong> ("the App") handles information. The App requires no account sign-up and does not collect directly identifying personal information such as your name or email.</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<ul>
|
||||
<li><strong>Advertising identifier</strong> (Android Advertising ID / Apple IDFA): used by our advertising partner (Google AdMob) to serve and measure ads.</li>
|
||||
<li><strong>Usage data</strong> (app interaction, screen and event analytics): collected by Firebase Analytics to improve app quality.</li>
|
||||
<li><strong>Device information</strong> (device model, OS version, coarse region): used for advertising and analytics diagnostics.</li>
|
||||
</ul>
|
||||
<p>The developer does not use this information to identify you personally and stores no personal data on its own servers. Game progress and settings are stored only locally on your device.</p>
|
||||
|
||||
<h2>2. How We Use Information</h2>
|
||||
<ul>
|
||||
<li>To serve ads and generate revenue (an ad-supported free model)</li>
|
||||
<li>To analyze usage and improve features and difficulty</li>
|
||||
<li>To diagnose errors and improve stability</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Third Parties</h2>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (advertising) — <a href="https://policies.google.com/privacy">Google Privacy Policy</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (analytics) — <a href="https://firebase.google.com/support/privacy">Firebase Privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Tracking & Personalized Ads (iOS)</h2>
|
||||
<p>On iOS the App requests <strong>App Tracking Transparency</strong> permission. If you allow it, personalized ads may be shown. If you decline, the App works fully and shows non-personalized ads.</p>
|
||||
|
||||
<h2>5. In-App Purchases</h2>
|
||||
<p>A one-time "Remove Ads" purchase is offered. Payment is handled by the Apple App Store or Google Play; the developer does not collect or store your payment details.</p>
|
||||
|
||||
<h2>6. Children's Privacy</h2>
|
||||
<p>The App is not primarily directed at children under 13 and does not knowingly collect personal information from children.</p>
|
||||
|
||||
<h2>7. Data Retention & Deletion</h2>
|
||||
<p>Locally stored data is removed when the App is uninstalled. Advertising and analytics data follow the third-party policies above. For requests, contact us below.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>Privacy inquiries: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB |
@@ -0,0 +1,28 @@
|
||||
zardu.plum@gmail.com
|
||||
zardu.sqa.101@gmail.com
|
||||
zardu.sqa.102@gmail.com
|
||||
zardu.sqa.103@gmail.com
|
||||
zardu.sqa.104@gmail.com
|
||||
zardu.sqa.105@gmail.com
|
||||
zardu.sqa.106@gmail.com
|
||||
zardu.sqa.107@gmail.com
|
||||
zardu.sqa.108@gmail.com
|
||||
zardu.sqa.109@gmail.com
|
||||
zardu.sqa.110@gmail.com
|
||||
zardu.sqa.111@gmail.com
|
||||
zardu.sqa.112@gmail.com
|
||||
zardu.sqa.113@gmail.com
|
||||
zardu.sqa.114@gmail.com
|
||||
zardu.sqa.115@gmail.com
|
||||
zardu.sqa.116@gmail.com
|
||||
zardu.sqa.117@gmail.com
|
||||
zardu.sqa.118@gmail.com
|
||||
zardu.sqa.119@gmail.com
|
||||
zardu.sqa.120@gmail.com
|
||||
zardu.sqa.121@gmail.com
|
||||
axiom.kor@gmail.com
|
||||
boson.seoul@gmail.com
|
||||
cepheid.space@gmail.com
|
||||
diffraction.rayman@gmail.com
|
||||
duality.frame@gmail.com
|
||||
quantum.tteokshop@gmail.com
|
||||
|
+2
-1
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'core/feature_flags.dart';
|
||||
import 'l10n/gen/app_localizations.dart';
|
||||
import 'state/providers.dart';
|
||||
import 'ui/screens/splash_screen.dart';
|
||||
@@ -24,7 +25,7 @@ class _BlockSeasonsAppState extends ConsumerState<BlockSeasonsApp>
|
||||
// Eagerly start the IAP service so its purchase stream is live for the
|
||||
// whole session — restores and interrupted/deferred transactions are
|
||||
// delivered (and completed) even if the player never opens Settings.
|
||||
ref.read(iapServiceProvider);
|
||||
if (kIapEnabled) ref.read(iapServiceProvider);
|
||||
// Start background music for the current context (menu by default).
|
||||
ref.read(musicServiceProvider).playKey(ref.read(activeThemeProvider).bgm);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/// Compile-time feature toggles.
|
||||
library;
|
||||
|
||||
/// Whether the in-app purchase ("Remove Ads") is offered.
|
||||
///
|
||||
/// Disabled for launch: the developer is an individual without a Korean
|
||||
/// business registration, so the App Store / Play paid-apps (merchant)
|
||||
/// agreements can't be completed. The app ships ad-supported only. AdMob
|
||||
/// revenue is independent of those agreements, so ads still pay out.
|
||||
///
|
||||
/// To re-enable later (once a merchant agreement exists): flip this to `true`,
|
||||
/// re-activate the `remove_ads` product in both stores, and ship a new build.
|
||||
const bool kIapEnabled = false;
|
||||
@@ -2,6 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/feature_flags.dart';
|
||||
import '../../game/models/season.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../state/providers.dart';
|
||||
@@ -13,18 +14,8 @@ class SettingsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final adsRemoved = ref.watch(adsRemovedProvider);
|
||||
final soundOn = ref.watch(soundEnabledProvider);
|
||||
final musicOn = ref.watch(musicEnabledProvider);
|
||||
final iap = ref.read(iapServiceProvider);
|
||||
|
||||
ref.listen<bool>(adsRemovedProvider, (prev, next) {
|
||||
if (next && !(prev ?? false)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.adsRemovedThanks)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
@@ -51,31 +42,9 @@ class SettingsScreen extends ConsumerWidget {
|
||||
onChanged: (v) =>
|
||||
ref.read(musicEnabledProvider.notifier).set(v),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(l10n.removeAds),
|
||||
subtitle: Text(l10n.removeAdsDescription),
|
||||
trailing: adsRemoved
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: Text(iap.product?.price ?? ''),
|
||||
onTap: adsRemoved
|
||||
? null
|
||||
: () async {
|
||||
if (!iap.available || iap.product == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.purchaseUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await iap.buyRemoveAds();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restore),
|
||||
title: Text(l10n.restorePurchases),
|
||||
onTap: () => iap.restorePurchases(),
|
||||
),
|
||||
// The "Remove Ads" purchase is gated off at launch (no merchant
|
||||
// agreement); the app ships ad-supported only. See kIapEnabled.
|
||||
if (kIapEnabled) ..._iapTiles(context, ref, l10n),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text(
|
||||
@@ -92,4 +61,46 @@ class SettingsScreen extends ConsumerWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _iapTiles(
|
||||
BuildContext context, WidgetRef ref, AppLocalizations l10n) {
|
||||
final adsRemoved = ref.watch(adsRemovedProvider);
|
||||
final iap = ref.read(iapServiceProvider);
|
||||
|
||||
ref.listen<bool>(adsRemovedProvider, (prev, next) {
|
||||
if (next && !(prev ?? false)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.adsRemovedThanks)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(l10n.removeAds),
|
||||
subtitle: Text(l10n.removeAdsDescription),
|
||||
trailing: adsRemoved
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: Text(iap.product?.price ?? ''),
|
||||
onTap: adsRemoved
|
||||
? null
|
||||
: () async {
|
||||
if (!iap.available || iap.product == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.purchaseUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await iap.buyRemoveAds();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restore),
|
||||
title: Text(l10n.restorePurchases),
|
||||
onTap: () => iap.restorePurchases(),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
// Headless generator for the App Store IAP review screenshot: renders the
|
||||
// Settings screen (where "광고 제거 / Remove Ads" is purchased) to a PNG.
|
||||
//
|
||||
// flutter test test/tool/generate_iap_review_screenshot_test.dart
|
||||
//
|
||||
// Output: docs/store/iap_review_remove_ads.png
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/l10n/gen/app_localizations.dart';
|
||||
import 'package:block_seasons/services/iap_service.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:block_seasons/ui/screens/settings_screen.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// A stand-in IAP service that reports the product as available with a price,
|
||||
// so the Settings row renders exactly as a shopper sees it. initialize() is
|
||||
// never called, so no real store query happens.
|
||||
class _FakeIap extends IapService {
|
||||
_FakeIap() : super(onEntitlementGranted: _noop);
|
||||
static Future<void> _noop() async {}
|
||||
|
||||
@override
|
||||
bool get available => true;
|
||||
|
||||
@override
|
||||
ProductDetails get product => ProductDetails(
|
||||
id: 'remove_ads',
|
||||
title: 'Remove Ads',
|
||||
description: 'Remove banner and interstitial ads',
|
||||
price: r'$1.99',
|
||||
rawPrice: 1.99,
|
||||
currencyCode: 'USD',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadFont() async {
|
||||
final bytes =
|
||||
File('/System/Library/Fonts/Supplemental/Arial.ttf').readAsBytesSync();
|
||||
final loader = FontLoader('Arial')
|
||||
..addFont(Future.value(ByteData.view(bytes.buffer)));
|
||||
await loader.load();
|
||||
}
|
||||
|
||||
Widget _wrap(ProviderContainer c, Widget screen, GlobalKey key) =>
|
||||
UncontrolledProviderScope(
|
||||
container: c,
|
||||
child: RepaintBoundary(
|
||||
key: key,
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Arial',
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF5B7FFF),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: screen,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('generate IAP review screenshot', (tester) async {
|
||||
// in_app_purchase only registers a native platform for android/iOS/macOS;
|
||||
// on a desktop target it registers none, so constructing the real
|
||||
// IapService never makes a store channel call (which would throw async in
|
||||
// the headless binding). The fake overrides available/product anyway.
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
|
||||
await _loadFont();
|
||||
|
||||
const dpr = 3.0;
|
||||
tester.view.devicePixelRatio = dpr;
|
||||
tester.view.physicalSize = const Size(1242, 2688); // 6.5" — accepted size
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = SaveRepository(await SharedPreferences.getInstance());
|
||||
|
||||
final key = GlobalKey();
|
||||
final c = ProviderContainer(overrides: [
|
||||
saveRepositoryProvider.overrideWithValue(repo),
|
||||
iapServiceProvider.overrideWithValue(_FakeIap()),
|
||||
]);
|
||||
await tester.pumpWidget(_wrap(c, const SettingsScreen(), key));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 450));
|
||||
|
||||
var ro = key.currentContext!.findRenderObject()!;
|
||||
while (!ro.isRepaintBoundary) {
|
||||
ro = ro.parent!;
|
||||
}
|
||||
final layer = ro.debugLayer! as OffsetLayer;
|
||||
final bounds = ro.paintBounds;
|
||||
final bytes = await tester.runAsync(() async {
|
||||
final image = await layer.toImage(bounds, pixelRatio: dpr);
|
||||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
image.dispose();
|
||||
return data;
|
||||
});
|
||||
File('docs/store/iap_review_remove_ads.png')
|
||||
..parent.createSync(recursive: true)
|
||||
..writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
c.dispose();
|
||||
|
||||
// Reset before the body ends so flutter_test's foundation-var invariant
|
||||
// check (which runs before addTearDown) passes.
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
|
||||
expect(
|
||||
File('docs/store/iap_review_remove_ads.png').existsSync(), isTrue);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user