Hi,
Pretty sure abusive refunds are hurting our app placement. Noticed we had two refunds in the exact same hour from Poland and thought it odd since Poland makes up a very small part of our user base. Extra odd since we've had few refunds in general (~2% when comparing against activations + renewals) so I was worried our backend servers were down and the app not working but noticed no problems. I then checked the past 5 months (life time of our app) and noticed that over 60% of our refunds are all from Poland even though it makes up a very, very tiny amount of our sales.
I then also remembered my user support team member telling me they get endless emails from one user in Poland who is making rather unusual and paranoid statements about how various governments are out to get them along with complaining about our app not stopping them...at least that's what the machine translation is telling us, it's all in Polish.
Assuming it's all done by this one guy, their pattern is purchase, a few days later a refund, a few days go by, another sale, a day or two later, refund, etc. Then one day two sales and then two weeks later, two refunds at the same exact hour (presumably from two different Apple IDs this person has). As I was writing this it happened again, two more refunds (although this time I don't see the purchases they are tied to).
I'm considering just removing Poland from the list of countries for my IAPs. I don't want my app placement ruined by refunds associated with one user in one country! I've already noticed a sudden drop in installs in the past week corresponding to this guy increasing the frequency of this and I'm starting to get very worried!
I also noticed that nothing is pro-rated, the refunds match the proceeds exactly while the person does things with the app that cost us money on the server/cloud costs side. Fortunately it's not a lot so more surprised than concerned. I thought each day they waited after a purchase to request a refund they would still owe 1/30 of the monthly subscription price.
My questions are:
Can this truly be the same user or would Apple have blocked them from doing this cycle by now? I assume even if they used a new Apple ID that using the same purchase method would get them flagged.
Are a few refunds hurting our app placement in the App Store or as long as we're relatively low overall it's fine? It's been a marked change in # installs per day in the past week...
Is there a way to ask Apple to block one specific person from buying your app? I know their Apple ID (or at least one of them) since they emailed our support team with it.
Is 2% refunds an acceptable rate? We'd be below 1% if not for Poland.
Aren't IAP refunds pro-rated?
I checked events for Poland over the lifetime of the app and I get the following, which seems odd to me. Like how could we have a reactivation or does a purchase after a refund count as that? If so, would mean same Apple ID being used.
9 refunds
7 activations
3 reactivations
1 cancellation
1 enter grace period
1 entered billing retry
1 renewal
Thank you!
Colin
Post
Replies
Boosts
Views
Activity
When users share a file with my app I am having trouble 5-10% of the time obtaining the file meta data, specifically creation and modified time and size.
Using SwiftUI with the code below..
.onOpenURL { url in
var fileSize: Int64 = 0
var creationTime: Date = Date(timeIntervalSince1970: 0)
var modificationTime: Date = Date(timeIntervalSince1970: 0)
do {
let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path)
fileSize = fileAttributes[FileAttributeKey.size] as? Int64 ?? 0
creationTime = fileAttributes[FileAttributeKey.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
modificationTime = fileAttributes[FileAttributeKey.modificationDate] as? Date ?? Date(timeIntervalSince1970: 0)
<SNIPPED CODE no other tries though and not involving above variables>
} catch {
// quite confident I am ending up here because variables after the above code aren’t being set and there are no other try blocks,
// so FileManager.default.attributesOfItem(atPath: url.path) must be throwing….
}
<SNIPPED CODE>
To attempt to resolve this, I added in a 0.5 second wait cycle if creationTime == 0 and modificationTime == 0 , so if obtaining both metadata fails, wait 0.5 seconds and try again, try this a max of 3 times and then give up. I don’t know how often I am entering this code (didn’t instrument the app for it), but am still getting times when metadata comes back blank which means this code wasn’t successful after 3 tries.
I assume the file would only become visible and sharable with my app after it has completed being written by the original app/process. Perhaps it hasn’t finalized yet? Is there a way to detect this so I can tell the user in my share screen to wait and try again?
I am assuming that the file has finished writing though since when I read the data from the file contents, it’s good data and complete even when metadata failed.
I will be instrumenting the above code in my next app version, just hoping to fix it right now since users are emailing saying my app is broken.
Thanks!
In my app I need to determine what hardware the app is running on (also forms part of the UI).
iPhone 15 series identifiers are as below, wondering if anyone knows what iPhone 16, Plus, Pro and Pro Max will be?
case "iPhone15,4": return "iPhone 15"
case "iPhone15,5": return "iPhone 15 Plus"
case "iPhone16,1": return "iPhone 15 Pro"
case "iPhone16,2": return "iPhone 15 Pro Max"
I assume since all four 16 models are A18 that it'll be a bump to the base level to 17 for all four phones.
case "iPhone17,1": return "iPhone 16"
case "iPhone17,2": return "iPhone 16 Plus"
case "iPhone17,3": return "iPhone 16 Pro"
case "iPhone17,4": return "iPhone 16 Pro Max"
Anyone else making a different assumption? Trying to avoid the "if unknown just say iPhone 16" option.
Thanks!
Hi,
Trying to convert the lsaw.csstoredump file in a sysdiagnose directory in to a human readable format.
The readme file in sysdiagnose indicates
".csstoredump files:
sysdiagnose generates the output of lregister/lsaw in a binary form. To convert
these .csstoredump files to text files, use the following command:
lsaw dump --file "PATH TO DUMP FILE" > lsaw.txt
These files can also be opened in CSStore Viewer."
But no such command lsaw exists, at least not on macOS 14. No CSStore viewer either.
Any ideas where I can find this utility or convert the file? I know it's in .gz format so have also tried just decompressing it but it is still in a binary format. Also tried running strings on it but not useful.
Thanks!
Hi,
My app uses Sign in with Apple as the only login option and this has worked great for 99.99% of my user base.
A small number of users though have Mobile Device Management (MDM) profiles installed on their devices that have disabled iCloud (using Apple Configurator). Even though my app makes no use of iCloud at all, when they try and use Sign in with Apple they get the prompt "You need to sign in with your Apple ID in Settings" even though they are signed in already as shown in Settings and the App Store. I have a subscription based app and they can see in the App Store that they are considered signed in and when they use my app it sees an active subscription tied to that Apple ID.
Same Apple ID on a device without the MDM profile, everything works as expected.
Anyone know if there is a way to solve this?
Thanks!
For the periods of time covered by the previous two payment periods (#7 and #8), everything is looking close enough (# of units sold don’t match up and proceeds off by a few % points even when using Apple’s exchange rate but nothing to worry about). For the August 1st payment though (period 9 covering June 2nd - June 29th), there is a larger discrepancy between units sold and proceeds during that time period and the estimated payment coming tomorrow (August 1st).
Anyone else experiencing this? Did it get resolved with the actual payment (I'll see mine in my account in a few days).
My proceeds for period 9 (based on data from App Store Connect > Trends > Proceeds with date range June 2nd - June 29th) were $1,227.83 USD.
Under Trends > Units > Transaction Type I have 100 units “Paid” and 2 units “Refund”, so 98 units counting towards proceeds I would assume.
In Payments and Financial Reports for the month of June though (which I assume matches Period 9 June 2-29), I am seeing only 65 total units sold (not 98) and the total estimated proceeds are $1,203.29 Canadian or roughly $872 USD. So $355 USD less than my proceeds would indicate and 33 units less. Note that I have two apps with multiple time periods for subscriptions so the dollar value of each unit is all over the place. I was expecting $1,668 Canadian or so (using the 1.36 exchange rate Apple shows in the estimated payment * the proceeds shown of $1,227.83 USD), not $1,203 Canadian.
There weren't a flood of sales on June 1st/2nd or June 29th/30th so I don't think this is caused by timezone differences or anything like that.
I’m using https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/jumpTo?page=fiscalcalendar to determine dates for August 1st payment.
Perhaps the payment tomorrow will be 38% higher than the estimate but that would seem way off.
Period 7 “April” Mar 31 - May 4:
Trends > Proceeds indicates 13 units for $107 USD * 1.37 Apple exchange rate = $146 Canadian
Actual payment: $135.46 Canadian with 8 units
Period 8 “May” May 5 - June 1:
Trends > Proceeds indicates 55 units for $640 USD * 1.37 Apple exchange rate = $876 Canadian
Actual payment: $855.59 CAD with 49 units
A client asked why we can't detect other apps installed on a device without an MDM profile, we explained this isn't possible due to privacy and security restrictions on iOS. A regular app cannot find other apps that are installed unless part of the same group.
The client then told us to download SpyBuster (on the App Store) which somehow is collecting a list of Bundle IDs or names of all installed apps somehow.
We were skeptical, but sure enough, the app showed us a list of apps we had installed. How is it doing this?!?! No MDM profile associated with the app. No special permissions requested. No access to anything shown in privacy & security in settings.
Is there a special entitlement we're not aware of?
Just seems like they must be using a private API call to get this info but that would of course mean it should be pulled from the App Store. We'd love to have this capability in our apps if it's legit and accepted by App Store review.
Thanks!
Anyone else seeing renewals show up as activations for auto-renewing subscriptions in App Store Connect?
Our app is almost 2 months old on the App Store and we have had monthly subscriptions starting to sell almost on day one so we've had one full subscription monthly cycle go by for a bunch of our paid users. We noticed though that after 4-5 weeks, there was only one subscription renewal shown but we also were't seeing cancellations or a drop in our subscriber numbers. We just kept seeing more activations. Now at the 8 week mark we are very sure that renewals are showing as activations instead since our subscriber numbers aren't tanking, but instead slowly climbing, and we've only had a handful of renewals or cancellations.
We'll see say 10 subscription activations in a day but then our total paid subscriber numbers only go up by 5, so once again, assuming 5 are actually renewals and not activations.
We do see refunds but at the exact same time as a longer subscription period sells so assuming an upgrade and the total paid subscribers doesn't go down.
Supposedly our retention rate is 2% but that doesn't seem to match our subscriber numbers at all. Assume this is due to renewals being counted as activations instead.
We are NOT using server side validation or tracking of sales, using StoreKit to validate on device and unlock features. So all of this is from the App Store Connect web site.
None of this is really an issue right this moment but longer term we'd like to better understand how our retention is going vs new sales. Are we just misunderstanding what the categories mean? We assume this isn't an issue with how we implemented StoreKit in the app but maybe it is?
Thanks!
Colin
Hi,
So I know an app will get rejected during App Store review if it includes the ability to open any Settings app URLs other than the main settings page or the app's own settings page, so you can't go directly to the Wi-Fi settings page for example.
Wondering though if I create an iOS shortcut file via the Shortcuts app that includes the ability to open a "rejectable" URL (such as a sub-page of the Settings app) and include it in my app that the user can then open and install the shortcut to their device and use outside of my app, is that ok? I can share it via email, AirDrop, etc. no problem and it works on other devices.
If the above is NOT ok, would it be ok if the app had a link to a web site that hosted the shortcut file so it wasn't actually embedded in my app itself?
If it is ok, also wondering if shared shortcut files expire or have a validity period? I know the Settings app URLs can change at any time which would break the shortcut but not worried about updating the app if that happens with a new shortcut.
Thanks!
Colin
Is it even possible or part of VisionOS?
Thanks,
Colin
Detecting New WiFi Connection + WiFi Details
What I want to accomplish:
The app, including when backgrounded or suspended, creates a local notification (assuming the app has permission for notifications) when there is a new WiFi network being used and ideally being able to execute some small code to customize the notification. This code would also have access to SSID info, security type, etc., so the sort of info in NEHotspotNetwork. A number of apps seem able to do this but I am having trouble replicating what they are doing.
What I’ve looked at or tried:
Looking at “TN3111: iOS Wi-Fi API overview”
https://developer.apple.com/documentation/technotes/tn3111-ios-wifi-api-overview
Navigate an internet hotspot (NEHotspotHelper)
Doesn’t look like NEHotspotHelper would provide the above functionality for detecting changes while backgrounded and it seems to indicate that the special entitlement com.apple.developer.networking.HotspotHelper would not be granted for this use case anyway.
Add an accessory to the user’s network
(Wireless Accessory Configuration (WAC) or HomeKit)
Doesn’t seem relevant to my use case
Peer-to-peer networking
Doesn’t seem relevant to my use case
Location tracking
I don’t want to know my user’s location and Lookout and Norton 360 (just two of many examples) don’t request or have location permissions (or request any permissions for that matter except notifications) and are still able to obtain the WiFi network info without it as well as detect changes in the background.
Current Wi-Fi network
NEHotspotNetwork .fetchCurrent(completionHandler:)
So this is the most obvious since it returns the info I want but it requires the following permissions or configurations that neither Lookout or Norton 360 are requesting and also I don’t see how this API would trigger a backgrounded app to run, more for when your app is in the foreground and able to run already.
From Apple docs:
“This method produces a non-nil NEHotspotNetwork object only when the current network environment meets all four of the following critieria:
The app is using the Core Location API and has user’s authorization to access precise location.
The app used the NEHotspotConfiguration API to configure the current Wi-Fi network.
The app has active VPN configurations installed.
The app has an active NEDNSSettingsManager configuration installed.
This method also requires the app to have the Access Wi-Fi Information Entitlement, and produces nil if the app lacks this entitlement.”
Once again, apps that are able to do what I want don't seem to have location permissions, no VPN profile, no DNS config, no hotspot config....
Additional things I’ve considered that are not mentioned in the above:
Using NWPathMonitor works for identifying a change, doesn’t trigger when app backgrounded and no access to SSID or other WiFi info.
What am I missing? Is there some API that I totally missed?
Thank you!
Colin
Hi,
My iOS app allows a user to perform a rather expensive cloud operation (costs me 10 cents per time) and I want to ensure they can only do it once a day across all of their iOS devices. The marketing will make it clear this is how it will work so users won't be surprised. While most will only have one iPhone in practice, with my subscription pricing I'd lose money the second someone started doing it on a second device.
I could solve this using Sign in with Apple to ensure there is a 1:1 correlation between Apple ID with paid subscription and a user account in my system that I could track usage against but I'd like to avoid users having to sign in at all since it would serve no purpose from a user perspective.
identifierForVendor won't work since it's different across every device the same user has
Is there something in StoreKit v2 receipts or transaction data that would be stable longterm and have the same result across all devices using the same Apple ID? I don't actually want any info about the user, just a stable anonymized identifier to see they are the same user on different devices.
I could also go with a consumable in-app purchase but I don't think users would like this pricing model for this app.
Thanks!
Colin
Hi, calls to URLSession for an IPv6 only domain (i.e. only an AAAA DNS record) within my app are failing even though the same domain name works in Safari. This is on a device with both an IPv4 and IPv6 connection when iCloud Private Relay is enabled and when it is not.
I'm trying to obtain the external IPv4 and IPv6 addresses of the user's device from my app using ipify.org's api4.ipify.org (IPv4), api64 (6 to 4) and api6 (IPv6) addresses. The issue I am having is within my app the api64 call always returns an IPv4 address and the api6 call just fails outright, resulting in a throw "A server with the specified hostname could not be found." api6.ipify.org only has an AAAA DNS record. But everything works right on the same device in Safari.
In Safari:
api4.ipify.org -> IPv4
api64.ipify.org -> IPv6 (since device has IPv6 connectivity)
api6.ipify.org -> IPv6
In my app though:
api4.ipify.org -> IPv4
api64.ipify.org -> IPv4 (but should return an IPv6 address, like for Safari)
api6.ipify.org -> IPv6 (fails...domain name not found)
Tried with cellular only connection (device has two public IPv6 addresses assigned) and from WiFi (device has both a private IPv4 and a private IPv6 address) and same results. Happening in both iOS 17.03 and 16.7.1.
I am having the exact same results when using other sites similar to ipify.org. 6to4 only returns an IPv4 address and AAAA record only domains fail, so not something specific to ipify.org.
Here's the code block that is failing within my app when api6.ipify.org is used and returns the wrong answer when api64 is used.
Thank you!
Task {
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api<insert 4, 64 or 6>.ipify.org")!)
self.statusPublicIPv6 = String(data: data, encoding: .utf8) ?? ""
} catch {
print(error.localizedDescription)
}
}
Hi,
The "Siri & Search" option is listed in my app's settings and I'd like to know how to remove it. I'm pretty sure it wasn't listed when I first started developing my app over a year ago and I never added any Siri related entitlements to my app, not sure when it started being there. It seems half of the apps on my device (from other developers) have this listed, other half do not, so it's not there by default.
I've checked my entitlements file, nothing related to Siri. Checked capabilities, nothing there either related to Siri.
I know my users could go and manually block the app's data from being accessible but I'd rather remove the option entirely and definitely don't want it on by default.
Shows up on at least iOS 15 and iOS 16.
Thanks!
Colin
Hi, thanks for reading my question. I need help with some odd behaviour with product.purchase() not triggering a confirmation dialog after a subscription has expired and trying to purchase it again. Seeing this in iOS 16.2 and 15.7.2 (haven't tried any other versions) on actual devices, not in simulator.
I'm using a sandbox user on the sandbox environment (not using the local store kit config file testing option).
Using a newly created sandbox user, first subscription purchase goes through just fine, dialog box pops up, login with sandbox user, get confirmation of purchase and then Transaction.currentEntitlements has one item as expected. It auto renews for 12 times (each time Transaction.currentEntitlements contains the correct results) and then expires, as expected for sandbox. Transaction.currentEntitlements is then also empty, as expected. All good so far.
Now I want to test purchasing it again...Call product.purchase() again to renew/start a new subscription and nothing happens, no confirm purchase dialog box pops up at all. The purchase function simply exits BUT returns success (as in the following gets called) but in self.updatePurchasedProducts(), Transaction.currentEntitlements is empty.
case let .success(.verified(transaction)):
// Successful purchase
await transaction.finish()
await self.updatePurchasedProducts()
if I instead go to Settings->App Store->Sandbox User-> Manage Subscriptions and renew the subscription there, instead of in my app, then Transaction.currentEntitlements has a new entry and all is good again.
Alternatively, if I create yet another new sandbox user and logout of the old one I was using, I am once again able to purchase from within the app, so .purchase() once again works as normal.
Is there something I am missing about expired subscriptions and trying to purchase them again in the app? Is this a sandbox issue and in production I'll have no problem?
The sandbox user has purchasing enabled in Settings->App Store.
I've also tried calling AppStore.sync() (which is in my "Restore Purchase" button) before calling product.purchase() after the subscription stops renewing, expires and this issue comes up, doesn't resolve it.
Also have a less important question, the initial call to product.purchase(), the one that works as expected, has a bit of a delay before the confirmation dialog pops up, a few seconds, which will probably result in the user clicking the buy button again thinking it didn't work. Is a bit of a delay normal for sandbox? Will it be ok in production? When it fails, and I have to renew in Settings->AppStore->Sandbox user, there's also a bit of a delay after I return to my app, 5-15 or so seconds, before the transaction observer fires and currentEntitlements is checked again, is there a way to reduce this delay?
Thank you!
Colin
@MainActor
class IAPManager: NSObject, ObservableObject {
// removed other functions.....
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case let .success(.verified(transaction)):
// Successful purchase
await transaction.finish()
await self.updatePurchasedProducts()
case let .success(.unverified(_, error)):
break
case .pending:
break
case .userCancelled:
break
@unknown default:
break
}
}
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
if transaction.revocationDate == nil {
self.purchasedProductIDs.insert(transaction.productID)
} else {
self.purchasedProductIDs.remove(transaction.productID)
}
}
}
}