SubSovereign 안드로이드 통합 가이드
이 가이드는 안드로이드 앱(스마트폰, 태블릿, 안드로이드 TV 또는 Amazon Fire TV)을 *"누가 결제했는지 전혀 모르겠음"*에서 "내 앱이 올바른 사용자에게 올바른 기능을 잠금 해제하며, 내 서버에서 검증됨" 상태로 안내합니다. 구독 도구에 대한 사전 지식이 없다고 가정하고 처음부터 끝까지 따라갈 수 있도록 작성되었습니다. 여기 포함된 모든 Kotlin 코드 샘플은 SDK에 대해 매 테스트 실행 시 컴파일되므로, 여기에서 읽는 내용이 코드가 실제로 수행하는 작업입니다.
SubSovereign이 제공하는 기능
사용자는 Google Play 스토어(또는 Fire TV의 Amazon)를 통해 구독합니다. SubSovereign은 앱을 위해 한 가지 질문에 확실하게 답합니다: 이 사용자가 실제로 무엇을 결제했는가?
- 앱은 SubSovereign에 사용자의 엔타이틀먼트 — 그들이 잠금 해제한 접근 권한 — 을 요청합니다.
- 영수증 검증은 서버에서 직접 스토어와 이루어지므로, 수정된 앱이 구독을 위조할 수 없습니다.
- 페이월 — 요금제를 제공하는 화면 — 은 원격으로 구성되며 SDK에서 그려지므로, 가격, 평가판 기간, 문구를 새 앱 버전을 출시하지 않고도 변경할 수 있습니다.
- EU 철회 기능 — 법적 취소 컨트롤 — 도 SDK에서 고객의 언어로 그려지므로, 법적 고지가 마케팅으로 변질되지 않습니다.
- 자체 호스팅입니다: SubSovereign은 여러분의 인프라에서 실행되며, 사용자 데이터는 여러분과 함께 유지되며, 수익 배분 없음 — 사용자가 지불한 금액의 100%를 유지합니다.
휴대폰을 신뢰하지 마세요. 휴대폰이 요청하면 서버가 결정합니다.
시작하기 전에
다음 항목이 필요합니다:
- 실행 중인 SubSovereign 서버(자체 호스팅 배포). SDK가 이를 가리키도록 설정합니다. 아직 없다면 먼저 배포하세요 — 이 가이드의 나머지 부분은 서버가 라이브 상태라고 가정합니다.
- 대시보드에 등록된 앱. SubSovereign 대시보드에서 앱을 생성합니다. 두 가지를 얻을 수 있습니다:
appId(앱을 식별함), 그리고- API 키(앱이 서버와 통신하는 데 사용하는 자격 증명). 앱이 출시되는 플랫폼 — 스마트폰, 안드로이드 TV 또는 Fire TV — 에 맞게 등록하세요. 하나의 코드베이스가 여러 플랫폼에 출시되는 것은 문제없습니다. 2단계의 스마트폰과 텔레비전을 참조하세요.
- 접근 수준 생성. 대시보드에서 앱이 부여하는 접근 수준(또는 티어) — 예를 들어
pro또는premium— 을 정의하고, 각 수준을 사용자가 구매하는 스토어 제품 ID와 연결합니다. - 스토어 결제가 이미 작동 중. SubSovereign은 구매를 검증하고 추적하지만, Google Play Billing(또는 Amazon IAP)을 대체하지 않습니다. 평소처럼 앱에서 이를 설정하세요 — SubSovereign은 구매 후 이를 검증하고 기록합니다.
코드 측면에서는 Kotlin과 코루틴(SDK 호출은 suspend 함수), Jetpack Compose(페이월과 취소 컨트롤은 컴포저블), 그리고 minSdk 24 이상이 필요합니다.
1단계 — SDK 추가
SDK는 현재 소스로 제공됩니다: Maven Central에서 다운로드할 항목은 아직 없습니다. SubSovereign 저장소에서 sdk-android/src/main/java 폴더를 프로젝트로 가져와 앱의 일부로 컴파일하세요 — 저장소의 샘플 앱과 정확히 동일한 방식으로 진행합니다. 아래는 SDK 자체에 필요한 항목으로, 앱에 이미 있는 항목에 추가해야 합니다: SDK 코드가 사용하는 두 가지 플러그인과, SDK가 빌드 및 테스트된 버전으로 고정된 종속성입니다(마지막 줄은 샘플에서 화면에 컴포저블을 배치하는 데 사용하는 항목입니다). AGP 8에서는 org.jetbrains.kotlin.android도 적용하세요. AGP 9에서는 Kotlin이 내장되어 있습니다. 플러그인 버전은 샘플처럼 루트 빌드 파일에 선언하세요.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization") // the SDK's models are @Serializable
}
android {
// ... your existing settings; minSdk 24 or higher
buildFeatures { compose = true }
// Compile the SDK's source folder as part of your app — wherever you put it.
sourceSets["main"].kotlin.srcDir("../subsovereign-sdk/src/main/java")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("io.coil-kt:coil-compose:2.7.0") // hero images — the SDK's one UI dependency
implementation(platform("androidx.compose:compose-bom:2024.09.02"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.runtime:runtime")
implementation("androidx.activity:activity-compose:1.9.3") // setContent, as in the sample
}
앱의 AndroidManifest.xml에 인터넷 권한이 있는지 확인하세요:
<uses-permission android:name="android.permission.INTERNET" />
SDK가 자체 소스로 컴파일되므로 업데이트할 버전이 없습니다: 업데이트하려면 폴더를 교체하세요. 게시된 아티팩트가 있으면 이 단계는 하나의 종속성 줄로 바뀌며, 이 가이드에서 이를 언급할 것입니다.
2단계 — 앱 시작 시 한 번 구성
SDK는 한 번만 구성합니다 — 가장 적합한 위치는 Application.onCreate() 또는 사용자가 로그인한 직후입니다. API 키, 앱 ID, 이 사용자를 위한 안정적인 식별자, 사용자의 언어, 그리고 앱이 실행되는 플랫폼을 제공합니다.
import com.subsovereign.sdk.SubSovereign
import com.subsovereign.sdk.SubSovereignConfig
import com.subsovereign.sdk.detectPlatform
SubSovereign.configure(
SubSovereignConfig(
apiKey = "YOUR_APP_API_KEY", // from the dashboard
appId = "your-app-id", // from the dashboard
baseUrl = "https://subs.yourdomain.com/api/v1", // YOUR self-hosted server
userId = currentUser.id, // your own stable user id
locale = customerLanguage, // the CUSTOMER's language, e.g. "fr" — decides the words they get
platform = detectPlatform(this), // "android", "androidtv" or "firetv"
)
)
알아두면 좋은 몇 가지 사항:
userId는 여러분의 것입니다. 로그인한 사용자를 위해 이미 가지고 있는 안정적인 ID를 사용하세요. 사용자가 기기를 변경해도 접근 권한이 따라가도록 매번 동일한 값을 사용하세요. 로그아웃을 지원하고 다른 사용자가 로그인하면, 새로운userId로configure를 다시 호출하세요.baseUrl은 여러분의 서버를 가리킵니다. SDK의 기본값은 플레이스홀더입니다 — 이를 여러분의 SubSovereign 배포로 설정하세요.- **
locale**은 고객의 언어이며, 여기에서 결정됩니다: 서버는 페이월과 법적 고지의 단어를 구성한 언어로 반환하며, SDK는 날짜를 해당 언어로 포맷합니다. 고객의 프로필이나 기기에서 가져오세요, 리터럴이 아닙니다.
스마트폰과 텔레비전
하나의 Play 코드베이스는 스마트폰, 태블릿, 안드로이드 TV에 출시될 수 있으며, Fire TV 빌드는 Amazon을 통해 출시됩니다. **platform**은 SDK에 어떤 화면인지 알려줍니다 — "android", "androidtv" 또는 "firetv" — 그리고 이는 세 가지를 결정합니다: 서버가 보내는 페이월(텔레비전용 페이월은 손에 쥐는 것이 아니라 방에 맞게 레이아웃됨), 취소가 제출되는 플랫폼, 그리고 고객이 어디에서 왔는지 기록됩니다. detectPlatform(context)은 런타임에 이를 감지합니다: Fire TV는 Amazon의 자체 기기 기능으로, 안드로이드 TV는 구성 또는 Leanback으로, 그 외는 모두 스마트폰으로 처리됩니다. 텔레비전에서 실행되는 빌드에 "android"를 하드코딩하지 마세요 — 이는 TV에서 스마트폰 페이월을 가져옵니다.
Fire TV에서는 대시보드에 Fire TV로 앱을 등록하거나, Fire TV 페이월을 게시하세요. Fire TV 빌드는 서버에 Fire TV 페이월을 요청하며, 서버에 해당 페이월이 없으면 Error로 응답합니다(서버는 안드로이드 TV에서 스마트폰 페이월로 폴백하며, Fire TV에서는 아무것도 폴백하지 않습니다). SDK는 플랫폼을 추측하지 않습니다: 인식할 수 없는 platform 값은 해당 값을 명시한 Error를 반환하며, 아무것도 전송되지 않습니다.
3단계 — 사용자가 접근할 수 있는 항목 확인
빠른 예/아니요 확인은 hasAccess()입니다. 이는 **실패 시 닫힘(fail-closed)**으로 작동합니다: 네트워크 문제, 서버 오류 또는 구성되지 않은 SDK가 있으면 false를 반환하므로, 일시적인 장애로 인해 유료 기능이 잠금 해제되는 일이 없습니다. 기능을 게이트할 때 사용하세요:
lifecycleScope.launch {
if (SubSovereign.hasAccess("pro")) unlockProFeatures() else showPaywall()
}
hasAccess("pro")는 활성 엔타이틀먼트가 해당 접근 수준 이름을 가지고 있을 때만 true입니다. 이름 없이 hasAccess()를 호출하면 서버의 전체 판단을 반환합니다.
세부 사항이 필요할 때 — 정확히 무엇이 활성화되어 있는지, 언제 갱신되는지, 또는 *"아니요"*와 *"확인할 수 없음"*을 구분할 때 — checkEntitlements()를 호출하세요. 이는 결과를 반환하며, 오류 시의 올바른 결정은 일반적으로 고객을 기존 상태로 유지하는 것입니다:
import com.subsovereign.sdk.SubSovereignResult
import kotlinx.coroutines.launch
lifecycleScope.launch {
when (val result = SubSovereign.checkEntitlements()) {
is SubSovereignResult.Success -> {
val ent = result.data
if (ent.hasAccess) unlockProFeatures() else showPaywall()
}
is SubSovereignResult.Error -> {
// A network hiccup or a server error. Keep the customer on whatever access they
// last had and try again later — never lock a paying customer out over a blip.
Log.w("MyApp", "Entitlement check failed: ${result.message}")
}
}
}
앱에 여러 티어가 있는 경우 result.data.entitlements 내부를 확인하세요 — 각 항목은 활성화된 접근 수준을 정확히 알려줍니다. 대시보드에서 수준에 부여한 이름과 일치시키세요. accessLevelId는 서버의 내부 ID이며, 그 이름이 아닙니다:
val isPro = result.data.entitlements.any { it.isActive && it.accessLevelName.equals("pro", ignoreCase = true) }
모든 엔타이틀먼트에는 expiresAt, willRenew, 그리고 그것이 온 store도 포함됩니다 — "구독이 갱신됩니다…" 메시지에 유용합니다. fromCache는 서버가 자체 단기 캐시에서 응답했음을 의미합니다. 이는 오프라인 응답이 아닙니다 — 연결할 수 없는 서버는 Error이며, 이 SDK는 복사본을 유지하지 않습니다.
4단계 — 구독 판매
페이월 가져오기
앱에 가격을 하드코딩하는 대신 서버에서 페이월을 가져오세요. 이를 통해 앱을 출시하지 않고도 세일이나 평가판 기간을 변경할 수 있습니다:
lifecycleScope.launch {
when (val result = SubSovereign.getPaywallConfig()) {
is SubSovereignResult.Success -> showPaywall(result.data) // the paywall published for THIS surface
is SubSovereignResult.Error -> showFallbackPaywall() // your own built-in default
}
}
PaywallConfig에는 헤드라인, 기능, 제공할 제품(각각 표시 가격, 기간, 평가판 및 선택적 배지 포함), 행동 유도, 푸터, 색상 및 템플릿 — 화면에 필요한 모든 항목이 포함됩니다.
페이월 그리기
SDK가 페이월을 그립니다. PaywallView는 컴포저블입니다: 구성과 고객이 제품을 선택했을 때 수행할 작업을 제공하세요.
import androidx.compose.runtime.Composable
@Composable
fun PaywallScreen(config: PaywallConfig, onBuy: (String) -> Unit) {
PaywallView(
config = config,
onDismiss = { /* the customer closed it — only reachable when you allow closing in the console */ },
) { productId -> onBuy(productId) } // start Google Play Billing (or Amazon IAP) for this product
}
이는 레이아웃, 콘솔에서 선택한 색상(및 텍스트가 흐려져 읽기 어려워지지 않도록 유지), 허용한 경우 닫기 버튼, 그리고 — 텔레비전에서 — 리모컨으로 선택한 컨트롤을 표시하는 링을 처리합니다. 선택한 제품에 대해 스토어의 구매 흐름을 시작하세요. 화면에 표시하는 것은 일반적인 Compose 형태입니다:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
class PaywallActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { PaywallScreen(config = paywall) { productId -> startPurchase(productId) } } // `paywall`: the PaywallConfig you fetched above
}
}
구매 완료 후 검증
구매를 평소처럼 Google Play Billing을 통해 진행하세요. Google이 purchaseToken을 반환하면, 서버가 스토어와 직접 영수증을 검증하고 접근 수준을 부여할 수 있도록 SubSovereign에 전달하세요. 티어는 대시보드의 제품-수준 맵에서 가져오며, 앱에서 가져오지 않습니다 — 수정된 앱이 스스로 권한을 상승시킬 수 없습니다 — 따라서 accessLevelId는 호환성을 위해 허용되며 무시됩니다:
lifecycleScope.launch {
val ok = SubSovereign.validateGooglePurchase(
purchaseToken = purchase.purchaseToken, // from Google Play Billing
productId = purchase.products.first(),
accessLevelId = "pro", // required by the SDK; the server grants the tier YOUR DASHBOARD maps this product to
)
when (ok) {
is SubSovereignResult.Success ->
// Confirmed by the server: re-check (fail-closed) and unlock on the answer, not on hope.
if (ok.data && SubSovereign.hasAccess("pro")) unlockProFeatures() else showTryAgain("Not confirmed yet")
is SubSovereignResult.Error -> showTryAgain(ok.message)
}
}
이것이 전체 신뢰 모델입니다: 서버가 확인한 후에야 구매가 실제로 유효합니다.
Amazon Fire TV에서
Fire TV 앱은 Google Play 대신 Amazon의 인앱 구매를 사용합니다. 입력만 다를 뿐 동일한 패턴입니다: Amazon은 영수증과 사용자 데이터 객체를 제공하며, 영수증은 Amazon 자체 사용자 ID와 함께 있어야만 유효합니다 — 이는 여러분의 userId가 아닙니다. 서버는 SKU를 접근 수준에 매핑하므로, 전달할 accessLevelId가 없습니다:
SubSovereign.validateAmazonPurchase(
receiptId = receipt.receiptId, // from Amazon's PurchaseResponse
amazonUserId = userData.userId, // from Amazon's UserData — NOT your own userId
productId = receipt.sku,
)
5단계 — EU 철회 기능 (Compliance Passport)
EU 소비자에게 구독을 판매하는 경우, Directive (EU) 2023/2673은 명확하게 표시된 철회 기능 — 취소 컨트롤 — 을 요구합니다. SDK는 이를 바로 사용할 수 있도록 제공하며, 두 부분으로 그려집니다: 앱 설정을 가져온 다음, 이를 사용하여 컨트롤을 그립니다.
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun CancelSubscription(subscriptionId: String, customerLocale: String) {
var settings by remember { mutableStateOf<WithdrawalConfig?>(null) }
LaunchedEffect(Unit) {
when (val r = SubSovereign.getWithdrawalConfig()) {
is SubSovereignResult.Success -> settings = r.data
// Log it: a silent failure here is how you ship English to German customers and never find out.
is SubSovereignResult.Error -> Log.w("MyApp", "Withdrawal settings unavailable: ${r.message}")
}
}
CancelButton(subscriptionId, settings, customerLocale)
}
@Composable
fun CancelButton(subscriptionId: String, settings: WithdrawalConfig?, customerLocale: String) {
if (settings?.enabled == false) return // switched off for this app in the console
// The WORDS are not passed in and cannot be: getWithdrawalConfig() fetched them in your
// customer's language and the SDK kept them, and this control reads them from there.
// Fetch FIRST — without it the notice falls back to English for every customer.
WithdrawalView(
subscriptionId = subscriptionId,
labelKey = settings?.labelKey, // ← the label chosen for this app
locale = customerLocale, // ← without this the DATE is formatted for the device
appearance = Appearance.AUTO, // LIGHT, DARK or AUTO — and nothing else
)
}
문구를 변경할 수 없습니다. 철회 기능에는 두 가지 레이블 — *"여기에서 계약 철회"*와 "여기에서 취소" — 이 있으며, labelKey는 앱에 설정된 레이블을 전달합니다. 단어 자체는 고객의 언어로 서버에서 제공됩니다. 자체 텍스트를 전달할 방법은 없습니다: label 매개변수는 자유 텍스트를 허용했으나 이제는 무시되며 로그에 경고가 기록됩니다. 문구를 변경할 수 있는 법적 고지는 마케팅으로 부드럽게 변질될 수 있으며, 이는 색상을 전달할 수 없는 이유와 동일합니다.
🚩 strings를 전달하지 않으면, 법적 고지는 모든 사용자에게 영어로 표시됩니다. 단어는 여러분의 SubSovereign 서버에서 제공되며, 서버가 지원하지 않는 언어는 영어로 돌아갑니다. 얻는 언어는 2단계에서 구성한 locale에 의해 결정됩니다 — 설정 호출이 이를 전송하므로, 여기의 customerLocale은 동일한 값이어야 합니다. 이는 날짜를 포맷할 뿐, 단어를 선택하지 않습니다. 컨트롤은 전달된 내용을 렌더링하며, 네트워크를 수행하지 않습니다. 동일한 계약이 모든 플랫폼에서 유지되어야 하며, UI 구성 요소가 네트워크를 전혀 수행할 수 없는 플랫폼도 포함됩니다. 가져오기 실패 시 컨트롤은 영어로 나타나지 않는 대신 영어로 표시됩니다: 소비자에게 찾기 쉬운 철회 기능을 제공하지 않는 것은 위반이지만, 잘못된 언어로 표시하는 것은 아닙니다. 오류를 로그하세요. 고객에게 절대 보여주지 마세요.
🚩 locale도 전달하세요. 이는 확인 메시지의 타임스탬프를 포맷합니다. 전달하지 않으면 SDK는 구성한 로캘로 폴백합니다. 둘 다 없으면 날짜는 독자가 오해할 수 없는 형식으로 출력됩니다. 미국 로캘 기기에서 독일 고객에게 9/3/2026을 보여주지 마세요. 이는 3월 9일로 읽힐 것입니다.
외관 — LIGHT, DARK 또는 AUTO(기본값), 그 외는 없습니다. 컨트롤은 자체 카드와 텍스트를 그려 어떤 화면에서도 읽을 수 있도록 합니다. AUTO는 스마트폰이나 태블릿에서 기기의 다크 모드 설정을 따르며, 텔레비전에서는 항상 다크 모드입니다. 색상을 전달할 방법은 의도적으로 없습니다.
이는 레이블이 지정된 버튼을 표시하고, 한 번 확인한 후, 멱등성으로 제출합니다(네트워크 재시도가 두 번의 철회를 생성할 수 없음). 확인 메시지와 날짜를 표시합니다. enabled에 따른 게이팅은 위에서와 같이 여러분이 수행합니다. 컨트롤 자체는 제출 외에 네트워크를 수행하지 않습니다.
6단계 — 동의 및 개인정보
개인정보 요청에 필요한 모든 항목은 각각 하나의 호출로 처리되며, 모든 호출은 여러분의 서버를 통해 이루어집니다:
SubSovereign.recordConsent(purpose = "analytics", granted = true)
SubSovereign.setDoNotSell(enabled = true) // CCPA "do not sell" / Global Privacy Control
SubSovereign.requestErasure() // GDPR Art. 17 — the customer's right to be forgotten
val export = SubSovereign.exportMyData() // GDPR Art. 20 — their data, as JSON
val flags = SubSovereign.getFeatureFlags() // remote feature flags for this app
SubSovereign.recordAttributionTouch(utmSource = "newsletter") // where this customer first came from
recordConsent의 purpose는 analytics, marketing, personalisation 또는 consumption_data_sharing 중 하나입니다. 그 외는 Error입니다. jurisdiction은 기본값으로 "GDPR"이며, policyVersion은 "1.0"입니다. 다른 값이 있다면 전달하세요. 각 호출은 다른 호출과 마찬가지로 SubSovereignResult를 반환합니다.
결과 처리
hasAccess()를 제외한 모든 SDK 호출은 SubSovereignResult를 반환하며, 이는 Success(데이터 포함) 또는 Error(메시지와 관련 HTTP 상태 code 포함) 중 하나입니다. 이는 의도적인 것입니다: 네트워크 호출은 실패할 수 있으며, 앱은 충돌 대신 무엇을 할지 결정해야 합니다. 간단한 습관:
Success시 — 데이터를 사용하세요.Error시 — 로그를 남기고, 사용자를 마지막으로 알려진 접근 상태로 유지하며, 나중에 재시도하세요. 일시적인 네트워크 문제로 인해 유료 사용자가 잠금되지 않도록 하세요.- **
hasAccess()**는 예외입니다: 이는 일반Boolean이며, 오류 시false를 반환하므로, 게이팅에 사용하고checkEntitlements()로 설명을 제공하세요.
모범 사례
- 콜드 스타트 시 확인. 앱이 잠금된 기능에 도달하기 전에 게이팅이 올바르게 설정되도록 앱 시작 시
checkEntitlements()를 호출하세요. - 구매 후 재확인. 성공적인
validate…호출 직후checkEntitlements()를 다시 실행하여 UI가 즉시 새로운 접근을 반영하도록 하세요. - 클라이언트를 신뢰하지 마세요. 앱에 "프로 여부"를 저장하고 이를 진실로 취급하지 마세요. 서버에 문의하세요. 서버가 영수증을 검증합니다.
- 실제 사용자당 하나의
userId. 접근이 사용자를 따라갈 수 있도록 안정적으로 유지하고, 로그인한 사용자가 변경되면 재구성하세요. - SDK가 플랫폼을 감지하도록 하세요. 구성 시
detectPlatform(context)를 사용하세요. 여러 종류의 기기에 출시되는 빌드에 리터럴을 사용하지 마세요. - 코루틴을 깔끔하게 유지하세요. 이들은
suspend함수입니다 —lifecycleScope또는viewModelScope에서 호출하여 화면과 함께 취소되도록 하세요.
빠른 참조
| 원하는 작업… | 호출 |
|---|---|
| SDK 설정 | SubSovereign.configure(config) with platform = detectPlatform(context) |
| 기능 게이팅, 실패 시 닫힘(fail-closed) | hasAccess(accessLevelName?) → Boolean |
| 사용자가 잠금 해제한 항목 확인 | checkEntitlements() → EntitlementResult |
| 이 플랫폼의 페이월 가져오기 | getPaywallConfig() → PaywallConfig |
| 페이월 그리기 | PaywallView(config, onDismiss) { productId -> … } |
| Google Play 구매 검증 | validateGooglePurchase(purchaseToken, productId, accessLevelId) |
| Amazon (Fire TV) 구매 검증 | validateAmazonPurchase(receiptId, amazonUserId, productId) |
| 철회 설정 및 단어 가져오기 | getWithdrawalConfig() → WithdrawalConfig |
| EU 철회 기능 그리기 | WithdrawalView(subscriptionId, strings, labelKey, locale, appearance) |
| GDPR 동의 기록 | recordConsent(purpose, granted) |
| CCPA 판매 금지 · 삭제 · 내보내기 · 플래그 · 어트리뷰션 | setDoNotSell · requestErasure · exportMyData · getFeatureFlags · recordAttributionTouch |
다음 단계
- 다른 플랫폼에서도 동일하게 진행하세요 — iOS, 웹/JavaScript, Roku SDK도 동일한 형태로 제공됩니다.
- 개념이 처음이신가요? SubSovereign의 작동 방식을 읽어보세요.