שינוי תוכנית מנוי: שדרוג והורדת דרגה ב-iOS וב-Android
שינוי תוכנית מנוי הוא אחד החלקים המסובכים ביותר ברכישות בתוך האפליקציה וב-StoreKit. אפל וגוגל מטפלות במעברים בצורה שונה. "פשוט לקנות מוצר אחר" אינו שדרוג. במשך 5 שנים, יישמנו יותר מ-30 פרויקטים מבוססי מנויים בשתי הפלטפורמות. הניסיון שלנו מראה שאינטגרציה נכונה של StoreKit 2 ו-Google Play Billing מקצרת את זמן היציאה לשוק בחצי. בלעדיה, משתמש מסיים עם שני מנויים פעילים או מפסיד עד 30% מהכסף שלו בהורדת דרגה. בשדרוג, עד 40% מהיתרה הנותרת מתאדה. בואו נראה כיצד לטפל נכון בכל התרחישים: למעלה, למטה, ומעבר צדדי.
כיצד פועלים מעברים ב-iOS וב-Android
iOS: לוגיקת מעברים ו-StoreKit 2
ב-App Store, כל המנויים בתוך קבוצת מנויים אחת מנוהלים אוטומטית על ידי אפל. לא ניתן לרכוש שני מוצרים מאותה קבוצה בו-זמנית. כאשר מוצר חדש נרכש, אפל מיישמת אחת משלוש מדיניויות:
- שדרוג מיידי (לדרגה גבוהה יותר) – המנוי החדש מופעל מיד, המשתמש מקבל זיכוי יחסי עבור התקופה הנותרת (חיסכון של עד 40%)
- מעבר צדדי בחידוש (לדרגה דומה) – המנוי החדש מתחיל במועד החידוש הבא
- הורדת דרגה בחידוש (לדרגה נמוכה יותר) – המנוי הנוכחי נמשך עד סוף התקופה, ואז המנוי החדש מופעל
דרגת המוצר מוגדרת ב-App Store Connect → קבוצת מנויים → סדר גרירה ושחרור של המוצרים. למעלה = הגבוה ביותר.
כדי לבצע מעבר, קרא ל-product.purchase() בדיוק כמו רכישה רגילה – StoreKit קובע את סוג המעבר אוטומטית:
func changePlan(to newProduct: Product) async throws {
let result = try await newProduct.purchase(options: [
.appAccountToken(userAccountToken)
])
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
if let upgradeInfo = transaction.subscriptionGroupID {
await handlePlanChange(transaction: transaction)
}
await transaction.finish()
}
case .pending:
showPendingChangeNotification()
case .userCancelled:
break
}
}לאחר הרכישה, בדוק את הזכאויות הפעילות הנוכחיות באמצעות func changePlan(to newProduct: Product) async throws { let result = try await newProduct.purchase(options: [ .appAccountToken(userAccountToken) ]) switch result { case .success(let verification): if case .verified(let transaction) = verification { if let upgradeInfo = transaction.subscriptionGroupID { await handlePlanChange(transaction: transaction) } await transaction.finish() } case .pending: showPendingChangeNotification() case .userCancelled: break } } .
Android: מצבי חלוקה פרופורציונלית וטיפול בממתינים
ב-Android, שינוי מנוי דורש ציון מפורש של Transaction.currentEntitlements ב-ProrationMode:
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(newProductDetails)
.setOfferToken(newOfferToken)
.build()
)
)
.setSubscriptionUpdateParams(
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(currentPurchaseToken)
.setSubscriptionReplacementMode(
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
)
.build()
)
.build()BillingFlowParams שגוי הוא טעות נפוצה (נראית בחצי מהפרויקטים). val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList( listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(newProductDetails) .setOfferToken(newOfferToken) .build() ) ) .setSubscriptionUpdateParams( BillingFlowParams.SubscriptionUpdateParams.newBuilder() .setOldPurchaseToken(currentPurchaseToken) .setSubscriptionReplacementMode( BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE ) .build() ) .build() עבור הורדת דרגה גורם לחיוב מיידי במחיר החדש ללא פיצוי. עבור הורדות דרגה, השתמש תמיד ב-ReplacementMode. זה מבטיח שהדרגה החדשה מתחילה רק לאחר סיום התקופה בתשלום, ומונע אובדן למשתמש. בדוק את CHARGE_PRORATED_PRICE ואל תציג את הדרגה החדשה כפעילה עד לאישור מהשרת. הגדרה נכונה מונעת הפסדים ב-95% ממקרי הורדת הדרגה.
UI/UX של מעברים
הבעיה העיקרית היא שמשתמשים לא מבינים מתי השינוי נכנס לתוקף. עליך להסביר במפורש:
func planChangeDescription(from current: Product, to new: Product) -> String {
let currentLevel = subscriptionLevel(for: current.id)
let newLevel = subscriptionLevel(for: new.id)
if newLevel > currentLevel {
return "Переход на \(new.displayName) будет активирован немедленно. Остаток текущего периода будет зачтён."
} else if newLevel == currentLevel {
return "Переход на \(new.displayName) произойдёт при следующем продлении."
} else {
return "Текущий тариф \(current.displayName) останется активным до \(currentExpirationDate). Затем начнётся \(new.displayName)."
}
}מסך אישור מודאלי עם תנאים ברורים הוא חובה. הסבר גם שחידוש אוטומטי נשאר פעיל, והורדת דרגה נכנסת לתוקף לאחר סיום התקופה הנוכחית בתשלום.
השוואת מדיניויות: iOS מול Android
| תרחיש | מדיניות iOS | מצב החלפה ב-Android |
|---|---|---|
| שדרוג | מיידי, זיכוי יחסי | CHARGE_PRORATED_PRICE |
| הורדת דרגה | סוף התקופה | WITH_TIME_PRORATION |
| מעבר צדדי (אותה דרגה) | חידוש הבא | DEFERRED |
הניהול האוטומטי של מעברים באפל אמין פי שניים מיישום ידני ב-Android ללא מצבי חלוקה פרופורציונלית נכונים. אבל עם הגדרה נכונה, Android מספק גמישות.
טעויות נפוצות וכיצד להימנע מהן
| טעות | השלכה | פתרון |
|---|---|---|
| שימוש ב-CHARGE_PRORATED_PRICE עבור הורדת דרגה | המשתמש מפסיד עד 30% | השתמש ב-WITH_TIME_PRORATION |
| התעלמות מסטטוס ממתין ב-Android | הדרגה החדשה מופעלת לפני אישור | עקוב באמצעות PurchaseUpdatedListener |
| חסר appAccountToken ב-iOS | לא ניתן לשחזר מנוי במכשיר אחר | העבר token ברכישה |
| סדר מוצרים שגוי בקבוצת מנויים | סוג מעבר שגוי | ודא גרירה ושחרור ב-App Store Connect |
מדוע שדרוג מנוי דורש טיפול מיוחד?
שדרוג הוא המעבר היחיד שמתרחש מיד עם זיכוי. אבל חשוב לקשר את הרכישה למשתמש באמצעות WITH_TIME_PRORATION. זה מאפשר לשרת לקבוע באופן חד-משמעי את הדרגה הנוכחית בשחזור. בלעדיו, מתרחשים כפילויות ואובדן נתונים. עקוב אחר תיעוד StoreKit 2 לטיפול נכון.
כיצד להימנע משגיאות הורדת דרגה ב-Android?
עבור הורדת דרגה, השתמש תמיד ב-purchaseState == PENDING. עקוב אחר סטטוס PENDING באמצעות func planChangeDescription(from current: Product, to new: Product) -> String { let currentLevel = subscriptionLevel(for: current.id) let newLevel = subscriptionLevel(for: new.id) if newLevel > currentLevel { return "Переход на \(new.displayName) будет активирован немедленно. Остаток текущего периода будет зачтён." } else if newLevel == currentLevel { return "Переход на \(new.displayName) произойдёт при следующем продлении." } else { return "Текущий тариф \(current.displayName) останется активным до \(currentExpirationDate). Затем начнётся \(new.displayName)." } } . אחסן את מצב המעבר בשרת והודע למשתמש על השינוי המתוכנן. אנו ממליצים גם להירשם ל-התראות מפתח בזמן אמת לסנכרון סטטוס. שלבים:
- בבחירת הורדת דרגה, העבר
appAccountToken. - ספק
WITH_TIME_PRORATIONשל המנוי הנוכחי. - טפל בסטטוס PENDING ב-
PurchaseUpdatedListener. - לאחר אישור השרת, הפעל את הדרגה החדשה.
דוגמה מעשית: אפליקציית כושר עם 5 דרגות
אחד הפרויקטים שלנו היה אפליקציית כושר עם 5 רמות מנוי. לאחר מעבר לטיפול נכון בשדרוג/הורדת דרגה, נטישת המשתמשים ירדה ב-15%. השינוי המרכזי היה יישום מצבי חלוקה פרופורציונלית נכונים ב-Android ושדרוג מיידי עם זיכוי ב-iOS. זה שמר על עד 40% מכספי המשתמשים בשדרוגים ומנע אובדן של עד 30% בהורדות דרגה.
מה כלול וזמני הערכה
- הגדרת דרגות מנוי ב-App Store Connect / Google Play Console
- לוגיקת רכישה בצד הלקוח המטפלת במיידי / ממתין
- ממשק משתמש המסביר את התנאים לכל תרחיש (למעלה/למטה/צדדי)
- טיפול ב-
WITH_TIME_PRORATIONלמעקב אחר הפעלת ממתינים - Google Play:
oldPurchaseTokenנכון עבור כל סוג מעבר - סנכרון בצד השרת באמצעות App Store Server Notifications / RTDN
- התאמת המנוי לכללים העסקיים שלך
זמן פיתוח משוער: 3 עד 5 ימים תלוי במספר הדרגות והפלטפורמות. העלות נקבעת באופן אישי לאחר ניתוח דרישות. קבל ייעוץ – צור קשר עם המומחים שלנו כדי לדון בפרויקט שלך מקצה לקצה. הזמן ביקורת על יישום המנוי הנוכחי שלך – זה לוקח לא יותר משעה.







