Implementing Data Transfer During Device Migration
User bought new phone. If data migration from app doesn't work — they start over: reconfigure profile, lose history, reinstall purchases. App store rating doesn't go up. Device migration — not single mechanism, but set of tools with different tradeoffs.
Platform Tools
iOS: QuickStart (iPhone-to-iPhone direct transfer via Bluetooth/WiFi) and iCloud Backup automatically transfer app data if app saves it correctly. Data in Documents and Application Support included in backup by default. Keychain with kSecAttrAccessible = kSecAttrAccessibleAfterFirstUnlock — transfers on iCloud Backup if kSecAttrSynchronizable = true.
Android: Google One Backup + Auto Backup transfers SharedPreferences, Room databases, files from getDataDir(). Config in AndroidManifest.xml:
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules">
<!-- res/xml/data_extraction_rules.xml (Android 12+) -->
<data-extraction-rules>
<cloud-backup>
<include domain="database" path="app.db"/>
<include domain="sharedpref" path="user_prefs.xml"/>
<exclude domain="database" path="http_cache.db"/>
<exclude domain="file" path="temp/"/>
</cloud-backup>
</data-extraction-rules>
Server-Side Transfer via Account
Most reliable approach — all important data stored on server, tied to account. User logs in on new device — gets all data. For auth apps this is standard.
What should be on server:
- User profile
- Action history
- Purchases (mandatory — for restoration)
- User content
- Settings if they affect backend logic
What doesn't need sync:
- Local UI settings (theme, font size) — cheaper to re-choose
- Cache (will restore automatically)
- Temp files
QR Code or Migration Code
For apps without accounts — direct transfer via QR or numeric code. Principle: old device generates temporary token or encrypted payload, new device scans it.
// Migration code generation
class MigrationCodeGenerator(private val exportManager: DataExportManager) {
suspend fun generateMigrationCode(): MigrationCode {
val exportedData = exportManager.exportUserData()
val encryptedPayload = encryptWithTemporaryKey(exportedData)
// Either send to server and get short code
val code = api.createMigrationSession(
payload = encryptedPayload,
expiresIn = 10 * 60 // 10 minutes
)
return MigrationCode(
code = code.shortCode, // "ABCD-1234"
qrData = code.qrPayload, // for QR code
expiresAt = code.expiresAt
)
}
}
// Import on new device
suspend fun importFromCode(code: String): ImportResult {
return try {
val session = api.getMigrationSession(code)
if (session.isExpired) return ImportResult.Expired
val data = decryptPayload(session.encryptedPayload, session.tempKey)
importManager.applyUserData(data)
api.invalidateMigrationSession(code) // one-time — invalidate immediately
ImportResult.Success
} catch (e: Exception) {
ImportResult.Error(e.message)
}
}
Direct Peer-to-Peer Transfer
For sensitive data that shouldn't go through server — direct channel between devices:
iOS: MultipeerConnectivity framework — WiFi Direct or Bluetooth, no internet. Android: Nearby Connections API (Google Play Services) — WiFi, Bluetooth, NFC.
// iOS: initiate MultipeerConnectivity session
let peerID = MCPeerID(displayName: UIDevice.current.name)
let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
let advertiser = MCNearbyServiceAdvertiser(peer: peerID,
discoveryInfo: ["appVersion": Bundle.main.appVersionString],
serviceType: "myapp-migrate")
For large data volumes (photos, files) — only direct channel, not through server. WiFi Direct transfer speed — 20-50 MB/s vs 1-5 MB/s via internet.
Purchase Restoration
In-app purchases — separate story. Apple and Google store purchase history on their side.
// iOS: restore purchases StoreKit 2
for await result in Transaction.currentEntitlements {
switch result {
case .verified(let transaction):
await updatePurchasedProducts(transaction.productID)
case .unverified:
break // suspicious transaction
}
}
// Android: BillingClient
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
grantEntitlement(purchase.products)
}
}
}
}
"Restore Purchases" button — mandatory per App Store guidelines.
Typical Mistakes
Transfer without version validation. Data from app v1.0 imported to v3.5 without schema migration — crash or incorrect state.
Unencrypted QR. QR code contains plaintext user data — someone photographed stranger's screen.
Migration token not invalidated. Code can be reused — data leaked to third device.
Implementing data transfer with QR code, server sync and purchase restoration: 2–3 weeks. Cost calculated individually.







