Flutter: [IN_APP_PURCHASE] Purchase dialog not showing on iOS 13.4

Created on 29 Mar 2020  ·  122Comments  ·  Source: flutter/flutter

I'm facing an issue with in_app_purchase 0.3.1+2 flutter plugin and iOS 13.4

The purchase dialog opens once and, if canceled, it won't open anymore.

It works fine on Android and any prior version of iOS 13.4

It happens for both, consumable and non-consumable purchases.

By investigating further, I noticed that specifically, nothing happens when calling the following FIAPaymentQueueHandler.m method:

- (void)addPayment:(SKPayment *)payment { [self.queue addPayment:payment]; }

P2 crowd first party in_app_purchase platform-ios plugin regression

Most helpful comment

hai @LHLL I using example code from this repo and I found this error

Unhandled Exception: PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly usingcompletePurchaseto avoid edge cases.
I have tried all the way from all comment above but still not working

All 122 comments

I am facing almost the same error. After I upgraded my iOS to version 13.4 the purchase dialog do not show anymore in the sandbox environment (Local and Test Flight).

Hi @dancamdev @fracon
can you please provide your flutter doctor -v and flutter run --verbose?
Thank you

@TahaTesser Sure, here is my flutter doctor -v:

`[✓] Flutter (Channel master, v1.16.4-pre.18, on Mac OS X 10.15.3 19D76, locale en-IT)
• Flutter version 1.16.4-pre.18 at /Users/danielec/Documents/flutter
• Framework revision c8efcb632b (2 days ago), 2020-03-27 22:31:01 -0700
• Engine revision 3ee9e3d378
• Dart version 2.8.0 (build 2.8.0-dev.17.0 1402e8e1a4)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /Users/danielec/Library/Android/sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.4)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 11.4, Build version 11E146
• CocoaPods version 1.8.4

[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 3.6)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin version 44.0.2
• Dart plugin version 192.7761
• Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] IntelliJ IDEA Community Edition (version 2019.3.1)
• IntelliJ at /Applications/IntelliJ IDEA CE.app
• Flutter plugin version 42.1.4
• Dart plugin version 193.5731

[✓] VS Code (version 1.43.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.8.1

[✓] Connected device (3 available)
• iPhone 11 Pro Max • 66BFC31E-1567-46CC-9B9A-CD14539D558A • ios •
com.apple.CoreSimulator.SimRuntime.iOS-13-4 (simulator)
• Chrome • chrome • web-javascript • Google Chrome
80.0.3987.149
• Web Server • web-server • web-javascript • Flutter Tools

• No issues found!`

and my flutter run --verbose of when I click the "buy" button:

`

[+90990 ms] [DEVICE LOG] 2020-03-30 12:52:19.676922+0200 localhost Runner[15896]: (StoreKit)
: Payment added for transaction already in the SKPaymentQueue: geniusmonthly
[ +216 ms] [DEVICE LOG] 2020-03-30 12:52:19.893680+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> resuming, QOS(0x21) Voucher (null)
[ +1 ms] [DEVICE LOG] 2020-03-30 12:52:19.895682+0200 localhost Runner[15896]: (CFNetwork)
[com.apple.CFNetwork:ATS] Task <32E19877-7B72-489B-BB0D-A84012487DA4>.<1> {strength 1, tls 8, ct 0, sub 0,
sig 0, ciphers 1, bundle 0, builtin 0}
[ +1 ms] [DEVICE LOG] 2020-03-30 12:52:19.897246+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
enabling TLS
[ ] [DEVICE LOG] 2020-03-30 12:52:19.897269+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
starting, TC(0x0)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.897303+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] [C22 7CDDCC30-4293-4F63-A295-7AD7040C8EB0 graph.facebook.com:443 tcp, url hash:
7c59002f, tls] start
[ ] [DEVICE LOG] 2020-03-30 12:52:19.898056+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_connection_report_state_with_handler_on_nw_queue [C22] reporting state preparing
[ ] [DEVICE LOG] 2020-03-30 12:52:19.898804+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> setting up Connection 22
[ +21 ms] [DEVICE LOG] 2020-03-30 12:52:19.920819+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_socket_handle_socket_event [C22.1:3] Socket received CONNECTED event
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921157+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_flow_connected [C22.1 31.13.86.8:443 in_progress socket-flow (satisfied (Path is
satisfied), interface: en18)] Transport protocol connected
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921385+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_handshake_config(1471) [0x7fa045292fc0] set
tls_handshake_config_standard
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921425+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_min_version(324) [0x7fa045292fc0] set 0x0301
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921461+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_max_version(308) [0x7fa045292fc0] set 0x0304
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921513+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_cipher_suites(843) [0x7fa045292fc0]
Ciphersuite string:
TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECD
HE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-
ECDSA-AES128-SHA:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE
-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-CHACHA20-POLY
1305:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:ECDHE-ECDSA-AES12
8-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AES256-SHA:AES128-SHA:ECDHE-ECDSA-DES
-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:DES-CBC3-SHA
[ +1 ms] [DEVICE LOG] 2020-03-30 12:52:19.921590+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_remote_address(2555) [0x7fa045292fc0] Saving
remote IPv4 address
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921654+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_install_association_state(1262) [0x7fa045292fc0]
Client session cache miss
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921732+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_set_peer_hostname(1154) [0x7fa045292fc0] SNI
graph.facebook.com
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921808+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_min_version(324) [C22.1:2][0x7fa045292fc0] set
0x0303
[ ] [DEVICE LOG] 2020-03-30 12:52:19.921888+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_fallback(374) [C22.1:2][0x7fa045292fc0] set
false
[ ] [DEVICE LOG] 2020-03-30 12:52:19.922103+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_session_ticket_enabled(440)
[C22.1:2][0x7fa045292fc0] set false
[ ] [DEVICE LOG] 2020-03-30 12:52:19.922528+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_false_start(410) [C22.1:2][0x7fa045292fc0] set
false
[ ] [DEVICE LOG] 2020-03-30 12:52:19.922800+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_enforce_ev(400) [C22.1:2][0x7fa045292fc0] set
false
[ ] [DEVICE LOG] 2020-03-30 12:52:19.922857+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_ats_enforced(1285) [C22.1:2][0x7fa045292fc0]
set false
[ ] [DEVICE LOG] 2020-03-30 12:52:19.922900+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_ats_minimum_rsa_key_size(1294)
[C22.1:2][0x7fa045292fc0] set 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.923092+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_ats_minimum_ecdsa_key_size(1303)
[C22.1:2][0x7fa045292fc0] set 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.923215+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_ats_minimum_signature_algorithm(1313)
[C22.1:2][0x7fa045292fc0] set 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.923417+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_set_peer_hostname(1154) [C22.1:2][0x7fa045292fc0]
SNI graph.facebook.com
[ ] [DEVICE LOG] 2020-03-30 12:52:19.923598+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_set_cipher_suites(843) [C22.1:2][0x7fa045292fc0]
Ciphersuite string:
TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECD
HE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-
ECDSA-AES128-SHA:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE
-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-CHACHA20-POLY
1305
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924508+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] nw_protocol_boringssl_begin_connection(497)
[C22.1:2][0x7fa045292fc0] early data disabled
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924599+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1970) [C22.1:2][0x7fa045292fc0]
Client handshake started
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924745+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Writing SSL3_RT_HANDSHAKE 512 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924789+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS client enter_early_data
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924824+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_add_handshake_message_pending(578)
[C22.1:2][0x7fa045292fc0] Adding message(1)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924925+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS client read_server_hello
[ ] [DEVICE LOG] 2020-03-30 12:52:19.924959+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_add_handshake_message_pending(578)
[C22.1:2][0x7fa045292fc0] Adding message(2)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.925537+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_handshake_incomplete(170)
[C22.1:2][0x7fa045292fc0] Handshake incomplete: waiting for data to read [2]
[ ] [DEVICE LOG] 2020-03-30 12:52:19.925596+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_handshake_incomplete(170)
[C22.1:2][0x7fa045292fc0] Handshake incomplete: waiting for data to read [2]
[ +14 ms] [DEVICE LOG] 2020-03-30 12:52:19.944869+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 122 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.944952+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_hello_retry_request
[ ] [DEVICE LOG] 2020-03-30 12:52:19.944982+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_add_handshake_message_pending(578)
[C22.1:2][0x7fa045292fc0] Adding message(2)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945009+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Writing SSL3_RT_CHANGE_CIPHER_SPEC 1 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945031+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_server_hello
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945210+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_encrypted_extensions
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945289+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 15 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945360+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_certificate_request
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945536+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_handshake_incomplete(170)
[C22.1:2][0x7fa045292fc0] Handshake incomplete: waiting for data to read [2]
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945648+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 2819 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945698+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_server_certificate
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945830+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_server_certificate_verify
[ ] [DEVICE LOG] 2020-03-30 12:52:19.945872+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 79 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.946121+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_copy_peer_sct_list(1003) [C22.1:2][0x7fa045292fc0]
SSL_get0_signed_cert_timestamp_list returned no SCT extension data
[ ] [DEVICE LOG] 2020-03-30 12:52:19.946380+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(607)
[C22.1:2][0x7fa045292fc0] SecTrustCreateWithCertificates result: 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.946614+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(614)
[C22.1:2][0x7fa045292fc0] No TLS-provided OCSP response
[ ] [DEVICE LOG] 2020-03-30 12:52:19.946948+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(621)
[C22.1:2][0x7fa045292fc0] No TLS-provided SCTs
[ ] [DEVICE LOG] 2020-03-30 12:52:19.947255+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_certificate_verify_callback(2071)
[C22.1:2][0x7fa045292fc0] Asyncing for verify block
[ ] [DEVICE LOG] 2020-03-30 12:52:19.947315+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_handshake_incomplete(170)
[C22.1:2][0x7fa045292fc0] Handshake incomplete: certificate evaluation result pending [16]
[ ] [DEVICE LOG] 2020-03-30 12:52:19.947378+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
asked to evaluate TLS Trust
[ ] [DEVICE LOG] 2020-03-30 12:52:19.947794+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> auth completion disp=1 cred=0x0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.948006+0200 localhost Runner[15896]: (Security) Created
Activity ID: 0x2993c, Description: SecTrustEvaluateIfNecessaryFastAsync
[ ] [DEVICE LOG] 2020-03-30 12:52:19.950367+0200 localhost Runner[15896]: (CFNetwork) System Trust
Evaluation yielded status(0)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.950421+0200 localhost Runner[15896]: (Security) Created
Activity ID: 0x2993d, Description: SecTrustEvaluateIfNecessaryFastAsync
[ +1 ms] [DEVICE LOG] 2020-03-30 12:52:19.952588+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
TLS Trust result 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.952689+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_certificate_verify_callback_block_invoke_3(2080)
[C22.1:2][0x7fa045292fc0] Returning from verify block
[ ] [DEVICE LOG] 2020-03-30 12:52:19.952729+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_certificate_verify_callback(2047)
[C22.1:2][0x7fa045292fc0] Setting trust result to ssl_verify_ok
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953305+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client read_server_finished
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953361+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 36 bytes
[ +7 ms] [DEVICE LOG] 2020-03-30 12:52:19.953424+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client send_end_of_early_data
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953467+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client send_client_certificate
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953517+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client complete_second_flight
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953566+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Writing SSL3_RT_HANDSHAKE 36 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953634+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS 1.3 client done
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953716+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS client finish_client_handshake
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953759+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1983) [C22.1:2][0x7fa045292fc0]
Client handshake state: TLS client done
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953836+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_copy_peer_sct_list(1003) [C22.1:2][0x7fa045292fc0]
SSL_get0_signed_cert_timestamp_list returned no SCT extension data
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953913+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(607)
[C22.1:2][0x7fa045292fc0] SecTrustCreateWithCertificates result: 0
[ ] [DEVICE LOG] 2020-03-30 12:52:19.953944+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(614)
[C22.1:2][0x7fa045292fc0] No TLS-provided OCSP response
[ ] [DEVICE LOG] 2020-03-30 12:52:19.954071+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_helper_create_sec_trust_with_certificates(621)
[C22.1:2][0x7fa045292fc0] No TLS-provided SCTs
[ ] [DEVICE LOG] 2020-03-30 12:52:19.954365+0200 localhost Runner[15896]: (Security) Created
Activity ID: 0x2993e, Description: SecTrustReportTLSAnalytics
[ ] [DEVICE LOG] 2020-03-30 12:52:19.954376+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_add_handshake_message_pending(578)
[C22.1:2][0x7fa045292fc0] Adding message(20)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.954638+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_info_handler(1974) [C22.1:2][0x7fa045292fc0]
Client handshake done
[ ] [DEVICE LOG] 2020-03-30 12:52:19.955219+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] nw_protocol_boringssl_signal_connected(701)
[C22.1:2][0x7fa045292fc0] TLS connected [version(0x0304) ciphersuite(0x1301) group(0x001d) peer_key(0x0403)
alpn(h2) resumed(0) offered_ticket(0) false_started(0) ocsp(0) sct(0)]
[ ] [DEVICE LOG] 2020-03-30 12:52:19.955292+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_flow_connected [C22.1 31.13.86.8:443 in_progress socket-flow (satisfied (Path is
satisfied), interface: en18)] Output protocol connected
[ ] [DEVICE LOG] 2020-03-30 12:52:19.955468+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_connection_report_state_with_handler_on_nw_queue [C22] reporting state ready
[ ] [DEVICE LOG] 2020-03-30 12:52:19.955976+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
connected successfully
[ ] [DEVICE LOG] 2020-03-30 12:52:19.956029+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
TLS handshake complete
[ ] [DEVICE LOG] 2020-03-30 12:52:19.956293+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
ready C(N) E(N)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.956492+0200 localhost Runner[15896]: (CFNetwork)
[com.apple.CFNetwork:Coalescing] new connection to graph.facebook.com config 0x600003665e20
[ ] [DEVICE LOG] 2020-03-30 12:52:19.956760+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> now using Connection 22
[ ] [DEVICE LOG] 2020-03-30 12:52:19.956822+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
received viability advisory(Y)
[ ] [DEVICE LOG] 2020-03-30 12:52:19.957233+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> sent request, body S 604
[ +5 ms] [DEVICE LOG] 2020-03-30 12:52:19.971481+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Reading SSL3_RT_HANDSHAKE 149 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:19.971594+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_new_session_handler(1117)
[C22.1:2][0x7fa045292fc0] New session available
[ +67 ms] [DEVICE LOG] 2020-03-30 12:52:20.039102+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> received response, status 200 content K
[ ] [DEVICE LOG] 2020-03-30 12:52:20.039149+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> done using Connection 22
[ ] [DEVICE LOG] 2020-03-30 12:52:20.039228+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> response ended
[ ] [DEVICE LOG] 2020-03-30 12:52:20.039313+0200 localhost Runner[15896]: (CFNetwork)
[com.apple.CFNetwork:Summary] Task <32E19877-7B72-489B-BB0D-A84012487DA4>.<1> summary for task success
{transaction_duration_ms=145, response_status=200, connection=22, protocol="h2",
domain_lookup_duration_ms=2, connect_duration_ms=55, secure_connection_duration_ms=30, request_start_ms=63,
request_duration_ms=0, response_start_ms=145, response_duration_ms=0, request_bytes=727, response_bytes=478,
cache_hit=0}
[ ] [DEVICE LOG] 2020-03-30 12:52:20.039398+0200 localhost Runner[15896]: (CFNetwork) Task
<32E19877-7B72-489B-BB0D-A84012487DA4>.<1> finished successfully
[ +28 ms] [DEVICE LOG] 2020-03-30 12:52:20.068320+0200 localhost Runner[15896]: (CFNetwork)
[com.apple.CFNetwork:Coalescing] removing all entries config 0x600003665e20
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068369+0200 localhost Runner[15896]: (CFNetwork)
[com.apple.CFNetwork:Coalescing] removing all entries config 0x600003665e20
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068553+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
is being canceled
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068616+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] [C22 7CDDCC30-4293-4F63-A295-7AD7040C8EB0 graph.facebook.com:443 tcp, url hash:
7c59002f, tls] cancel
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068727+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] [C22 7CDDCC30-4293-4F63-A295-7AD7040C8EB0 graph.facebook.com:443 tcp, url hash:
7c59002f, tls] cancelled
[ ] [DEVICE LOG] [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C 192.168.178.54:53375<->31.13.86.8:443]
[ +1 ms] [DEVICE LOG] Connected Path: satisfied (Path is satisfied), interface: en18
[ ] [DEVICE LOG] Duration: 0.171s, DNS @0.000s took 0.002s, TCP @0.003s took 0.020s, TLS took 0.035s
[ ] [DEVICE LOG] bytes in/out: 3959/1546, packets in/out: 8/9, rtt: 0.020s, retransmitted packets:
0, out-of-order packets: 0
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068778+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.000s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] path:start
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068816+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.000s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] path:satisfied
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068854+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.000s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] resolver:start_dns
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068889+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.002s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] resolver:receive_dns
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068932+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.002s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
path:start
[ ] [DEVICE LOG] 2020-03-30 12:52:20.068966+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.003s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
path:satisfied
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069090+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.003s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
flow:start_connect
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069279+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.012s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] resolver:receive_dns
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069446+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.023s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
flow:finish_transport
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069625+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.023s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] flow:finish_transport
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069781+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.058s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
flow:finish_connect
[ ] [DEVICE LOG] 2020-03-30 12:52:20.069942+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.058s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] flow:finish_connect
[ ] [DEVICE LOG] 2020-03-30 12:52:20.070100+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.058s [C22.1 1AABFBE1-F00B-41C8-848D-A6613940EB2C
192.168.178.54:53375<->31.13.86.8:443 socket-flow path=satisfied (Path is satisfied), interface: en18]
flow:changed_viability
[ ] [DEVICE LOG] 2020-03-30 12:52:20.070247+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.058s [C22 C16B1FFA-0528-41EF-ADF3-F3727ECDEDDD graph.facebook.com:443 resolver
path=satisfied (Path is satisfied), interface: en18] flow:changed_viability
[ ] [DEVICE LOG] 2020-03-30 12:52:20.070415+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] 0.171s [C22] path:cancel
[ ] [DEVICE LOG] 2020-03-30 12:52:20.071857+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_message_handler(2258) [C22.1:2][0x7fa045292fc0]
Writing SSL3_RT_ALERT 2 bytes
[ ] [DEVICE LOG] 2020-03-30 12:52:20.072040+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_context_handle_warning_alert(1893)
[C22.1:2][0x7fa045292fc0] write alert, level: warning, description: close notify
[ ] [DEVICE LOG] 2020-03-30 12:52:20.072231+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] boringssl_session_disconnect(504) [C22.1:2][0x7fa045292fc0]
SSL_shutdown 0
[ ] [DEVICE LOG] 2020-03-30 12:52:20.072422+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_flow_disconnected [C22.1 31.13.86.8:443 cancelled socket-flow ((null))] Output
protocol disconnected
[ ] [DEVICE LOG] 2020-03-30 12:52:20.072690+0200 localhost Runner[15896]: (libnetwork.dylib)
[com.apple.network:] nw_connection_report_state_with_handler_on_nw_queue [C22] reporting state cancelled
[ ] [DEVICE LOG] 2020-03-30 12:52:20.073056+0200 localhost Runner[15896]: (CFNetwork) Connection 22:
destroyed
[ ] [DEVICE LOG] 2020-03-30 12:52:20.073299+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] nw_protocol_boringssl_remove_input_handler(1012)
[C22.1:2][0x7fa045292fc0] nw_protocol_boringssl_remove_input_handler forced true
[ ] [DEVICE LOG] 2020-03-30 12:52:20.073450+0200 localhost Runner[15896]: (libboringssl.dylib)
[com.apple.network.boringssl:BoringSSL] nw_protocol_boringssl_remove_input_handler(1030)
[C22.1:2][0x7fa045292fc0] Transferring nw_protocol_boringssl_t handle back into ARC for autorelease`

By debugging the in_app_purchase package I found that the updatedTransaction function is returning the following:

SKErrorDomain Code=2 (Cannot connect to iTunes Store)

Could it be an Apple server issue?

@TahaTesser here the output of Flutter doctor:. @dancamdev I don't know why but after I increment my app version the purchase dialog works again ... once ... now after I do a purchase and the expiration date is reached when I try to do a new purchase using the same product apple returns directly a Purchase object with status purchased instead pending and no dialog shows. I don't if something changed in the 13.4 version and requires some adjustment of this package but according to the last 2 topics here https://forums.developer.apple.com/community/system-frameworks/in-app-purchase the in-app purchase is broken on this ios version.

Flutter (Channel stable, v1.12.13+hotfix.8, on Mac OS X 10.15.3 19D76, locale en-PT)
• Flutter version 1.12.13+hotfix.8 at /Users/rafael.vilaca/flutter_sdk/flutter
• Framework revision 0b8abb4724 (7 weeks ago), 2020-02-11 11:44:36 -0800
• Engine revision e1e6ced81d
• Dart version 2.7.0

[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
• Android SDK at /Users/rafael.vilaca/Library/Android/sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-28, build-tools 28.0.3
• ANDROID_HOME = /Users/rafael.vilaca/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.4)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 11.4, Build version 11E146
• CocoaPods version 1.8.4

[✓] Android Studio (version 3.6)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin version 44.0.2
• Dart plugin version 192.7761
• Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] VS Code (version 1.43.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.8.1

[✓] Connected device (1 available)
• Rafael’s iPhone • c140aa58762554c973f9e995beace0a61c541e2d • ios • iOS 13.4

• No issues found!

@fracon it happened to me too. I was able to open the dialog once and then not anymore upon retry. I'll double check if it returns a purchased product too. I'll let you know in a few minutes.

Yup, it is actually returning a valid purchase although it isn't. That's even worse! Thanks for pointing it out @fracon

any updates? @fracon are you getting the SKErrorDomain Code=2 (Cannot connect to iTunes Store) error too?

@dancamdev I didn't debug in Xcode yet but I search in the apple docs that error 2 means "Error code indicating that the user canceled a payment request." Weird humm.

@fracon do you happen to have any app store promotion for iaps enabled? On this thread they are suggesting that might be the cause. It doesn't seam to work for me, but you might want to try!

@dancamdev no promotions here but it's for me clearly something change from ios 13.3 to 13.4 or something change in the Apple's servers because on Friday my app works so since Saturday (when I updated my device to ios 13.4) stopped work. I just don't know if it's a bug on iOS or if this package needs an update to work with the new iOS.

Totally agree! Hopefully someone will answer soon! What about @TahaTesser ?

Another hypothesis from my side. Somehow iOS 13.4 seems to be braking only the objective c implementation. Swift is working fine. I have no expertise to port the this plugin to Swift. So someone else might want to look into that

Here's a way I'm getting closer to fix this one.

I replaced the handleTransactionUpdated function to forcefully call finishTransaction on every transaction, like this:

- (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)transactions {
//  NSMutableArray *maps = [NSMutableArray new];
//  for (SKPaymentTransaction *transaction in transactions) {
//    [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
//  }
//  [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps];
    for (SKPaymentTransaction *transaction in transactions) {
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
}

The commented code, is the actual in_app_purchase code.

After that, I was able to make the purchase dialog popup correctly, but only the very first time after flushing the transactions. Awesome! We're close!

Now, it looks like finishTransaction doesn't get called on iOS 13.4. Anyone's got an idea to fix it?

@cyanglaz this is the IAP issue I mentioned that's marked as customer critical.

@dancamdev
SKErrorPaymentCancelled (error code 2) means the user cancelled the transaction. The transaction will stay in the SKPaymentQueue with transaction state: SKPaymentTransactionStateFailed

Cancelled transaction is a failed transaction, thus I think you should remove the failed transaction object before adding new payment object with the same product.

Did you use the following method to delete the transaction before adding a new one:
https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart#L211

@LHLL Yes, that was what it was all about. Still iOS 13.3.1 didn’t require you to call completePurchase after cancelling the purchase. I think it has to be explicitly stated in the documentation.

@dancamdev Totally agree this needs to be documented in the plugin's API. Thanks for bring this up!

@cyanglaz Can we reuse Apple's comments and maybe add a code block in the documentation as an example since we are not really providing exactly same APIs as SKStoreKit?
https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc

@LHLL @dancamdev I really don't if this is the point of this issue. I'm completing all my purchases (except pending as documented) but still, when I click to do a new purchase I receive, on listen function, a previous purchase object with purchased status and expired instead receive a new purchase object with pending status. Am I missing some point here?

@ fracon What type of product are you providing? It sounds like you are providing a subscription?

@LHLL exactly my app provides some subscriptions and just to clarify what happened here:

  1. Friday, 27 March, everything was working;
  2. Saturday, 28 March, I updated my iOS to 13.4 and after when I tried to do make a new purchase I got a generic connection error with App Store;
  3. Monday, 29 March, after some tests I tried to increment my app version to see what happens and for my surprise when I tried to make a new purchase everything works good, the dialog shows, I could input my sandbox user password and continues with the flow;
  4. After my subscription reached the expiration date (30 minutes after the purchase) I tried to do a new purchase of the same subscription and nothing works anymore, I started to receive on Listen function a Purchase object representing the old purchase instead of the new one.
  5. I did the same process for another subscription offered in my app and the result was the same, first purchase OK then for the next I started to receive the old Purchase object on the Listen function.

@fracon I believe your issue is a different issue. I periodically experience "Cannot connect to iTunes Store" error under sandbox environment while using SKStoreKit and I never got any responses from Apple while raising this issue in the Apple Developer Forums. Here are some similar issues raised there:
https://forums.developer.apple.com/thread/114644
https://forums.developer.apple.com/thread/73668

From my own experience, I think this might be an issue from Apple's side.

@LHLL exactly my app provides some subscriptions and just to clarify what happened here:

  1. Friday, 27 March, everything was working;

  2. Saturday, 28 March, I updated my iOS to 13.4 and after when I tried to do make a new purchase I got a generic connection error with App Store;

  3. Monday, 29 March, after some tests I tried to increment my app version to see what happens and for my surprise when I tried to make a new purchase everything works good, the dialog shows, I could input my sandbox user password and continues with the flow;

  4. After my subscription reached the expiration date (30 minutes after the purchase) I tried to do a new purchase of the same subscription and nothing works anymore, I started to receive on Listen function a Purchase object representing the old purchase instead of the new one.

  5. I did the same process for another subscription offered in my app and the result was the same, first purchase OK then for the next I started to receive the old Purchase object on the Listen function.

It doesn’t seem like a different problem to me, I was getting exactly the same behavior. Make sure that when you finish your purchase, you call completePurchase(). If you don’t, it won’t be called automatically from iOS anymore, and upon app restart it will create an endless loop preventing the dialogs to appear and the transaction to actually be verified.

If you are willing to share the IAPS code either here or privately. I’ll have a look at it, and check if I get any issue.

@LHLL I think you don't understand, the "Cannot connect to iTunes Store" isn't happening anymore since I incremented my app version. For now my core problem is: Purchase dialog isn't being shown for subscriptions that I did a successful purchase and on listen function I am receiving a Purchase object with data from my last purchase and status purchased instead of a new Purchase object with status pending.

@dancamdev here my method (I deleted parts that handle with my database) to handle purchases update. This method is called in two opportunities inside my app on listen function and on startup getting past purchases.

`_handlePurchaseUpdates(List purchaseDetailsList, bool needNavigation) async{

try{

  for(PurchaseDetails purchase in purchaseDetailsList){

    if(purchase.status == PurchaseStatus.purchased){

      dynamic purchaseDetails;
      if(Platform.isAndroid)
        purchaseDetails = json.decode(purchase.verificationData.localVerificationData);
      else if(Platform.isIOS){
        purchaseDetails = purchase.verificationData.localVerificationData;
      }

      HttpsCallable callable;
      dynamic resp;
      if(Platform.isAndroid){
        callable = CloudFunctions.instance.getHttpsCallable(
          functionName: 'purchaseValidationAndroid',
        );

        resp = await callable.call(<String, dynamic>{
          'packageName': purchaseDetails['packageName'],
          'subscriptionId': purchaseDetails['productId'],
          'token': purchaseDetails['purchaseToken'],
        });

      }else if(Platform.isIOS){
        resp = await this.validateIOSpuchase(purchaseDetails);
      }

      if(resp != null && ( (Platform.isIOS && resp['status'] == <SOMETHING>) || (Platform.isAndroid && resp.data['status'] == <SOMETHING>) )){

        BillingResultWrapper br = await storeConnection.completePurchase(purchase);
        print(br.responseCode == BillingResponse.ok);

      }else{
        BillingResultWrapper br = await storeConnection.completePurchase(purchase);
        print(br.responseCode == BillingResponse.ok);
      }
    }else if(Platform.isIOS && purchase.status == PurchaseStatus.error){
      BillingResultWrapper br = await storeConnection.completePurchase(purchase);
      print(br.responseCode == BillingResponse.ok);
    }
  }

}catch(exception){
  await _handlePurchaseUpdates(purchaseDetailsList, needNavigation);
}

}`

@dancamdev Your original issue is not finishing a failed transaction before buying the same product again. For this scenario, it doesn't really matter whether your app is providing consumable products or subscriptions. According to Apple:
_"Call finishTransaction: only after the app has finished all work it performs to complete the transaction."_
This is why we cannot complete the failed transactions for you, this step needs to be done on the App side instead of Flutter Plug-in.

@fracon Sorry I understood your question wrong in the last comment, let me clarify:

  1. When the user purchased the subscription for the first time, a system dialog shows.
  2. Wait for this transaction to expire in the sandbox environment.
  3. Purchase the same subscription again, silently succeeds or fails.

If this is what you are facing today, then I believe this is the intended behavior of Apple's SKStoreKit according to Apple's documentation:
_"Because of the accelerated expiration and renewal rates in the test environment, a subscription can expire before the system tries to renew it, resulting in a short lapse in the subscription period. Such lapses are also possible in production for a variety of reasons; verify that your app handles them correctly."_

The user cannot initiated a transaction of the same subscription again, instead, iOS system will add the auto-renewal transaction to the SKPaymentQueue when needed. Purchase the same subscription again will not be functional.

First, ensure you're finishing transactions upon success/failure:

In our case, the old code was not calling SKPaymentQueue.default().finishTransaction(transaction) to remove it from the queue. Prior to iOS 13.4, that apparently worked fine even though the documentation says it’s required

So what would happen is the dialog would show once and the person would cancel and then from that point on the transaction would persist in the queue and automatically return as cancelled without showing the dialog again. Finishing the transaction purges it and allows the dialog to show again


If that doesn't work:

Per an Apple engineer's request, I filed a radar for this (FB7648374) with App Store logging and sysdiagnose

Please do the same:

Hi guys, today I analyzed this issue a little bit deeper and as @dancamdev suggested if we call finishTransaction inside handleTransactionsUpdated the purchase dialog back to work (in his case once because when he clicked to purchase the code tried to finish a pending purchase what raised an exception) so based on his comment I tried the following code and my purchases back to work 100%, so as I'm not familiar with the entire plugin code my question is if we use the following code will we have seconds implications/issues?

` (void)handleTransactionsUpdated:(NSArray *)transactions {
NSMutableArray *maps = [NSMutableArray new];
for (SKPaymentTransaction *transaction in transactions) {

  switch (transaction.transactionState) {
      case SKPaymentTransactionStatePurchasing:
          [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
          break;
      case SKPaymentTransactionStatePurchased:
          [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
          [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
          break;
      case SKPaymentTransactionStateFailed:
          [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
          [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
          break;
      case SKPaymentTransactionStateRestored:
          [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
          [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
          break;
      case SKPaymentTransactionStateDeferred:
          [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
          [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
          break;
  }

}
[self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps];
} `

@fracon Apple suggests that all apps should validate App Store receipt before finishing a transaction. If you add this change here, transaction will be removed before the receipt is validated, it could cause multiple potential edge cases for auto-renewable subscriptions.

It should be fixed in version: 0.3.2+1

@LHLL I'm trying the new version but I can't complete any purchase. When I try to call completePurchase I'm receiving an exception with code "storekit_platform_invalid_transaction" and message "The transaction with transactionIdentifer:(null) does not exist. Note that if the transactionState is purchasing, the transactionIdentifier will be nil(null)." the purchase status is PURCHASED and the identifier in PurchaseDetails object is not null.

You already have multiple incomplete transactions in the queue, the fix was intended to prevent it from happening. To unblock the user from current state is not the intention for the PR.

To unblock you from current state you can follow these steps

Find following method defined in InAppPurchasePlugin.m and replace the implementation:

- (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)transactions {
    // This will remove all old transactions regardless of states.
    for (SKPaymentTransaction *transaction in transactions) {
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
}

Launch your app on the test device one.
Restore the implementation of the above method:

- (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)transactions {
  NSMutableArray *maps = [NSMutableArray new];
  for (SKPaymentTransaction *transaction in transactions) {
    [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]];
  }
  [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps];
}

Add a new transaction and verify.

@fracon With the release of 0.3.3, the transactions are now directly accessible and you're able to get in an complete the zombie transactions as such:

var paymentWrapper = SKPaymentQueueWrapper();
var transactions = await paymentWrapper.transactions();
transactions.forEach((transaction) async {
    await paymentWrapper.finishTransaction(transaction);
});

However, a strange behavior I found is I could only complete one transaction before getting the error you specified. I realized by stopping and re-running the app I was able to drain the dead transactions one at a time. That part I do not understand but it got me unblocked.

Am I missing something? There doesn't seem to be any soultion so far. Even with version 0.3.3 and after clearing the transactions, it's still not working and showing SKErrorDomain Code=2 (Cannot connect to iTunes Store). Tested with dev and release version.

@DavidKuennen I believe your issue is a different issue. I periodically experience "Cannot connect to iTunes Store" error under sandbox environment while using SKStoreKit and I never got any responses from Apple while raising this issue in the Apple Developer Forums. Here are some similar issues raised there:
https://forums.developer.apple.com/thread/114644
https://forums.developer.apple.com/thread/73668

I am also experiencing this; the first time I was testing in sandbox the dialog popped up to purchase it, but all other times after that it stayed as pending and nothing popped up. Note that the first time I was messing around and didn't complete the purchase, could that have been it? Anyway I tried to increment my app version, that didn't help and neither did the code snipped suggested above to clear zombie purchases. I am on latest version of the plugin.

I might switch to purchases_flutter that seems to always work no matter what

@fracon With the release of 0.3.3, the transactions are now directly accessible and you're able to get in an complete the zombie transactions as such:

var paymentWrapper = SKPaymentQueueWrapper();
var transactions = await paymentWrapper.transactions();
transactions.forEach((transaction) async {
    await paymentWrapper.finishTransaction(transaction);
});

However, a strange behavior I found is I could only complete one transaction before getting the error you specified. I realized by stopping and re-running the app I was able to drain the dead transactions one at a time. That part I do not understand but it got me unblocked.

I am unsure why all fails when I try to call finishTransaction. getting this error The transaction with transactionIdentifer:(null) does not exist. Note that if the transactionState is purchasing, the transactionIdentifier will be nil(null)

I have 189 zombie queued transactions, most states are purchased, some are even null. I even tried finishing transaction for only the purchased states but no luck. i still get the error

We're still seeing this issue.

This is definitely happening in IOS 13.4.0+

if you don't mark transactions as complete they just sit in the SKPaymentQueue as sort of zombie transactions so any new transaction you make is waiting forever for them to finish which of course will never happen. Anyway here is a bit of code I put in AppDelegate.swift to clear the transactions. Note that this is a platform channel and I usually call it right before I attempt to make any new transactions in the flutter app.

@d9media Another thing I did was switch to purchases_flutter as that plugin is a lot higher level and handles all this stuff for you.

```
let clearQueueChannel = FlutterMethodChannel(name: "queue",
binaryMessenger: controller.binaryMessenger)
clearQueueChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
print(SKPaymentQueue.default().transactions.count); // On first run this showed I had 12 transactions that were not complete
for transaction in SKPaymentQueue.default().transactions {
print(transaction);
SKPaymentQueue.default().cancel(transaction.downloads);
}
for transaction in SKPaymentQueue.default().transactions {
SKPaymentQueue.default().finishTransaction(transaction);
}
result(true);
});

Could this be related to XCode version? I tested this on a mac that is running 10.x with my client's iphone which he upgraded to 13.4.1 and xcode said it wasn't supported. At the moment, I can't upgrade the mac.
Is it possible that could cause potential problems?

I have tested the process on several devices with browserstack.com and it did seem to work fine everywhere except on 13.4.1 that my client is using.

Xcode said what wasn't supported

@d9media If you cancelled the purchase without calling completion in your test environment, you will need to manually remove the uncompleted transactions, you can follow the steps one of the following comments:
https://github.com/flutter/flutter/issues/53534#issuecomment-617959103
https://github.com/flutter/flutter/issues/53534#issuecomment-626903404
https://github.com/flutter/flutter/issues/53534#issuecomment-619419799

The plugin doesn't clean up the dead transactions automatically for you just as the same as StoreKit. We want the developer to be more explicit about delivering purchased products or handling purchase errors.

Please let us now if the workarounds solved your issue.

Xcode said what wasn't supported

image

Said that I needed to upgrade Xcode in order to support IOS 13.4.1. But the machine can't be upgraded from my end because I'm logging onto it remotely.

@cyanglaz

Thanks for your help

Let me clarify. The completion was being called (consumePurchase) and the correct subscription data comes back on all devices except 13.4.1 where the purchase dialog won't even come up. On the other devices the correct data comes back as soon as user is prompted to login but on 13.4.1 that is not the case at all.

Let me read through your suggestions meanwhile

@d9media Please let me know if you still experience the issue described in the original post following one of the steps.

I’ve been looking at a problem I have had with zombie transactions for a few days, and this thread has been very helpful. The issue I experienced persisted because all transactions in my case had the same product ID, and the iOS implementation does a lookup for the transaction internally using the product ID as the key, and the first matching transaction always failed to complete.

In order to remove all my zombie transactions, I forked the plugin and changed the NSMutableDictionary transactionsSetter (FIAPaymentQueueHanlder.m) to use transaction ID as the key, that allowed me to iterate over the SKPaymentQueueWrapper().transactions, finish the transaction and remove the zombies.

@cyanglaz, I’m curious about the comment in paymentQueue:updatedTransactions (FIAPaymentQueueHanlder.m) on the use of Product ID to ensure deferred transactions can be completed. I would be grateful for any further information you can share.

Hi @gjsharp.
@LHLL can probably share more details on this.

Hi @cyanglaz thanks for your quick responses but I honestly was too tired to continue digging yesterday. Anyway, just went back to code today.

Quite frankly I need to admit I might not understand the function of this plugin in full depth still so please excuse if these are a silly questions, but why is it that potential zombie transactions on somebody's device is blocking new users to see the purchase dialog on their devices? Especially because I've tested this on several fresh iPhones with version prior to 13.4.1 without any problem.

Now just so you understand where I'm coming from, the client initially got several independent reports of users not able to press purchase, all running latest iphone version. I haven't had the chance to test-drive latest IOS till yesterday when we've updated my client's iphone to 13.4.1. We had uninstalled the app before and after launching the app, IAP came back with the last order id for the user (test order which had expired 5 days ago). ( On this note and kinda OT, why is it even after uninstalling the app on iPhone data is still there, even firebase login? ) We then removed the sandbox user from the Iphone's settings but after logging a fresh new user in (newly created); IAP would not prompt.

Another odd thing is that Apple sign-in pops up very early on, even way before accessing my paywall class. Is that expected behavior?

Now I've been trying to locate the class mentioned earlier to implement the hotfix for clearing zombie transactions but I can't find it nor edit it. I need to fork the project and include it in pubspec.yaml yes?

Do you see a problem with Xcode not running the latest version? I can't update the development machine because my client needs 32bit legacy support. This caused a bad situation where xcode would refuse to install the app on the newer iphone version. Man, Apple gives me a hard time right now...

@d9media I will try to answer your questions as best as I can.

why is it that potential zombie transactions on somebody's device is blocking new users to see the purchase dialog on their devices

I don't quite understand this question. Are you saying zombie transactions from one device is blocking transactions from a different device? I am surprised this would happen too. Could you explain it in more details? A steps to reproduce maybe?

Now I've been trying to locate the class mentioned earlier to implement the hotfix for clearing zombie transactions but I can't find it nor edit it. I need to fork the project and include it in pubspec.yaml yes?

There is no fork project, everything you need is in the same plugin.

why is it even after uninstalling the app on iPhone data is still there, even firebase login?
Another odd thing is that Apple sign-in pops up very early on, even way before accessing my
paywall class. Is that expected behavior?

@LHLL can explain more on this.

Thanks

I don't quite understand this question. Are you saying zombie transactions from one device is blocking transactions from a different device? I am surprised this would happen too. Could you explain it in more details? A steps to reproduce maybe?

Okay so this issue and the comments you've linked earlier is essentially about zombie transactions in the queue right? I don't know how it would be related to our user reports as well, which is why I was wondering if there may be a connection.

Also if you go give me a hint on where to find nAppPurchasePlugin.m so I could apply your hotfix that would help me cross out that atleast

Hi @gjsharp.
@LHLL can probably share more details on this.

That would be very helpful. I'm reluctant to include my updates in an app outside TestFlight without understanding this use case. Using the transaction ID has unblocked my team, it seemed to be the only way to complete purchases after calling InAppPurchaseConnection.instance.queryPastPurchases without the incomplete restored transactions being passed to the purchaseUpdatedStream listener when next running the app. I may have overlooked an important detail, so any feedback would be very welcome.

There is no way to test IAP on simulator for iOS no? Or is this exclusive to this project?

I am still struggling to understand why 13.4 won't show the purchase dialog. If anybody is keen for a Testflight I'd be happy to provide that.

Hi, I can confirm that trying to purchase the same product twice without completing the purchase leads to a lockdown.
I am aware that not-completing a purchase is a bad practice and we should avoid it but on the other hand I believe that if completePurchase will fail due to connection issue or store availability problem then we will end up in the same situation.

Here is the stack of the exception that I'm getting:

Unhandled Exception: PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly using `completePurchase` to avoid edge cases., {applicationUsername: <username>, requestData: null, quantity: 1, productIdentifier: premium, simulatesAskToBuyInSandbox: true})
#0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:569:7)
#1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:18)
<asynchronous suspension>
#2      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
#3      SKPaymentQueueWrapper.addPayment (package:in_app_purchase/src/store_kit_wrappers/sk_payment_queue_wrapper.dart:88:19)
#4      AppStoreConnection.buyNonConsumable (package:in_app_purchase/src/in_app_purchase/app_store_connection.dart:48:34)
#5      AppStoreConnection.buyConsumable (package:in_app_purchase/src/in_app_purchase/app_store_connection.dart:61:12)
#6      GetPremiumBloc._buyPremium (package:my_package/views/premium/bloc/get_premium_bloc.dart:221:20)
<asynchronous suspension>
#7      GetPremiumBloc.mapEventToState (package:my_package/views/premium/bloc/get_premium_bloc.dart:52:14)
<asynchronous suspension>
#8      Bloc._bindStateSubject.<anonymous closure> (package:bloc/src/bloc.dart:155:14)
#9      Stream.asyncExpand.onListen.<anonymous closure> (dart:async/stream.dart:579:30)
#10     _rootRunUnary (dart:async/zone.dart:1192:38)
#11     _CustomZone.runUnary (dart:async/zone.dart:1085:19)
#12     _CustomZone.runUnaryGuarded (dart:async/zone.dart:987:7)
#13     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#14     _DelayedData.perform (dart:async/stream_impl.dart:594:14)
#15     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:710:11)
#16     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:670:7)
#17     _rootRun (dart:async/zone.dart:1180:38)
#18     _CustomZone.run (dart:async/zone.dart:1077:19)
#19     _CustomZone.runGuarded (dart:async/zone.dart:979:7)
#20     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1019:23)
#21     _rootRun (dart:async/zone.dart:1184:13)
#22     _CustomZone.run (dart:async/zone.dart:1077:19)
#23     _CustomZone.runGuarded (dart:async/zone.dart:979:7)
#24     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1019:23)
#25     _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
#26     _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)

@d9media Unfortunately, Apple doesn't allow us to test in-app purchase on a simulator since it doesn't support Apple Pay.

I am glad Apple fixed that long-term bug that zombie transactions will stay in the queue forever. I used to help debug an app which has more than 2000 zombie transactions in both iOS app and associated backend database which makes delivering the product to user become very challenging.

To provide you more context regarding why we decided to only allow one transaction of a given product at any given time is due to the restriction from Apple that Apple will not expose user's Apple ID to third-party apps and a SKPaymentTransaction will not have a transaction id until the transaction is in either purchased or failed state. As a result, if we have two transactions in the queue with same product info, then it's impossible for us to establish one-to-one mapping between a user and a transaction since we cannot control when will the user switch accounts. If your app supports login, such as login with Google, FB or your own account system, then this becomes even more complicated since now user can switch two different account systems, and it's also not impossible to have a "your_account - Apple ID - transaction" mapping.

@ostrowp1 Just for me to double check I understand your scenario correctly, here are a bunch of edge cases you mentioned could potentially cause the lock down:

  1. Internet connect lost
  2. app killed/suspended before completionTransaction is finished.
class PurchaseService{
    final _connection = InAppPurchaseConnection();

    const PurchaseService(){
          _connection.purchaseUpdatedStream.listen((detail) {
               ...
              // Verify the receipt for succeeded transactions and handle the error for the failed
              // transaction here when app launches.
              _connection.completePurchase(detail);
          });   

    _internetDidRecover(){
          final List<PurchaseDetails> transactions = _connection.purchaseUpdatedStream.last;
          // Processes your transactions here,
    }
}

Thanks for your input @LHLL and @cyanglaz

After upgrading to Catalina and setting up the repository again, I was finally able to confirm that "hard-"clearing queue did help and the purchase went through.

However, we detached the sandbox user from the iPhone to emulate a "cold" purchase but somehow that wasn't entirely successful. At first, Storekit would still assume the sandbox user to be logged in so only password was prompt and after rebuilding the app, both Apple ID and password was requested which we then entered.

Sadly, this is where the problem reappeared. The app stalled at .consumePurchase() giving the error that there was another purchase that hasn't completed.

Now I'm not entirely sure if this is an extreme edge-case due to testing in debug mode and using several sandbox users, but in any case I would like to ship an update that covers those cases and makes sure the error won't appear again.

To clear things up, let me briefly show you guys my Implantation. I wonder if there is any problems with how I handle IAP.

...
import 'dart:async';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'dart:io';

class Paywall extends StatefulWidget {
  @override
  _TrailEndedState createState() => _TrailEndedState();
}

/* @override
void main() {
  InAppPurchaseConnection.enablePendingPurchases();
}
 */
const Set<String> _kIds = {'the_product_id'};

class _TrailEndedState extends State<Paywall> {
  StreamSubscription<List<PurchaseDetails>> _subscription;

  final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
  UserDatabaseService _userStorage;
  List<ProductDetails> _products = [];
  List<PurchaseDetails> _purchases = [];
  final List<PurchaseDetails> verifiedPurchases = [];
  bool _isAvailable = false;
  bool _purchasePending = false;
  bool _loading = true;
  Future<bool> _launched;
  String _queryProductError;

  Future<void> _deleteCacheDir() async {
    // ...
  }
  @override
  void initState() {
    final Stream purchaseUpdated =
        InAppPurchaseConnection.instance.purchaseUpdatedStream;
    _subscription = purchaseUpdated.listen((purchaseDetailsList) {
      _listenToPurchaseUpdated(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (error) {
      // handle error here.
    });
        initStoreInfo();

    super.initState();
  }

  Future<void> initStoreInfo() async {
    /** Connecting to the Storefront **/
    final bool isAvailable = await _connection.isAvailable();
    if (!isAvailable) {
      setState(() {
        _isAvailable = isAvailable;
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    /** Loading products for sale  **/
    ProductDetailsResponse productDetailResponse =
        await _connection.queryProductDetails(_kIds);
    if (!productDetailResponse.notFoundIDs.isNotEmpty) {
      // error handling
    }
    _products = productDetailResponse.productDetails;

    /** Loading previous purchases **/

    QueryPurchaseDetailsResponse purchaseResponse =
        await _connection.queryPastPurchases();
    if (purchaseResponse.error != null) {
      // handle query past purchase error..
      print("erorr: ${purchaseResponse.error.code}");
      showPendingUI();
    }

    print("past purchases");
    print(purchaseResponse.pastPurchases.toString());

    for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
      if (await _verifyPurchase(purchase)) {
        verifiedPurchases.add(purchase);
      }
    }

  }

  void showPendingUI() {
    setState(() {
      _purchasePending = true;
    });
  }

  //** verify purchase **//
  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    // IMPORTANT!! Always verify a purchase before delivering the product.
    // For the purpose of an example, we directly return true.

    if (purchaseDetails.status == PurchaseStatus.purchased) {
      await _deliverPurchase(purchaseDetails);
      _connection.completePurchase(purchaseDetails);
    }

    if (purchaseDetails.status == PurchaseStatus.error ||
        purchaseDetails.status == PurchaseStatus.error) {
      if (Platform.isIOS) {
        _connection.completePurchase(purchaseDetails);
      }

      if (purchaseDetails.status == PurchaseStatus.error) {
        return Future<bool>.value(false);
      }
    }

    return Future<bool>.value(true);
  }

  Future<void> _deliverPurchase(PurchaseDetails purchaseDetails) async {
    _setSubscriptionDB(purchaseDetails);
  }

  void handleError(IAPError error, PurchaseDetails purchaseDetails) async {
    //   print(error.message);

    switch (error.message) {
      case "BillingResponse.itemAlreadyOwned":
        _setSubscriptionDB(purchaseDetails);
        break;
    }
    setState(() {
      _purchasePending = false;
    });

    await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
  }

  void _setSubscriptionDB(PurchaseDetails purchaseDetails) async {
    await _userStorage.setSubscription(purchaseDetails);
  }

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    print("handling invalid purchase");
    // handle invalid purchase here if  _verifyPurchase` failed.
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
    purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
      if (purchaseDetails.status == PurchaseStatus.pending) {
        showPendingUI();
      } else {
        if (purchaseDetails.status == PurchaseStatus.error) {
          handleError(purchaseDetails.error, purchaseDetails);
        } else if (purchaseDetails.status == PurchaseStatus.purchased) {
          bool valid = await _verifyPurchase(purchaseDetails);

          if (valid) {
            _deliverPurchase(purchaseDetails);
          } else {
            _handleInvalidPurchase(purchaseDetails);
            return;
          }
        }

        if (Platform.isAndroid) {
          await InAppPurchaseConnection.instance
              .consumePurchase(purchaseDetails);
        }

        if (purchaseDetails.pendingCompletePurchase) {
          await InAppPurchaseConnection.instance
              .completePurchase(purchaseDetails);
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      backgroundColor: Theme.of(context).primaryColor,
      body: _products.isEmpty
          ? Center(
              child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text("Please sign-in to App Store"),
                MaterialButton(
                  child: Text(
                    "Sign In",
                  ),
                  onPressed: () async {
                    await _deleteCacheDir();
                    Phoenix.rebirth(context); // this is meant to rebuil state in order to prevent cache issues
                  },
                ),
              ],
            ))
          : ListView.builder(
              itemCount: _products.length,
              itemBuilder: (BuildContext ctxt, int index) {
             return productWidget(_products[index]), // i've shortened this section for clarity purposes
                        SizedBox(height: 15),
                        if (!_purchasePending)
                          MaterialButton(
                            onPressed: () async {
                              PurchaseParam purchaseParam = PurchaseParam(
                                  productDetails: _products[index],
                                  applicationUserName: null,
                                  sandboxTesting: false);
                              _connection.buyNonConsumable(
                                  purchaseParam: purchaseParam);
                            },
                            child: Text( "PURCHASE NOW", ),
                          ),
                        if (_purchasePending)
                          Text(
                            "connection to app store couldn't be established or purchase is still pending",
                          ),
                      ]),
                    ),
                  ],
                ));
              }),
    );
  }
}

Are we doing it wrong?

@LHLL thats correct. Thank you for your suggestion! I will try it out.

I'm just wondering if the final List<PurchaseDetails> transactions = _connection.purchaseUpdatedStream.last; will return anything if the app was killed in the meantime (2nd scenario).

I've got one more question associated with these failure scenarios and consumable items. On Android if we fail to validate the purchase (any of the two scenarios mentioned above) we are NOT consuming the purchase. After restarting the app that purchase will be returned from queryPastPurchases() so we can have another try.

How we should approach such a situation for the iOS?
We are kinda forced to run completePurchase() no mater if the verification was successful or not. On the other hand if we complete the purchase after failing the verification then we end up in the situation where user lost his money and we are not delivering him the goodies.

EDIT:
Unfortunately final List<PurchaseDetails> transactions = _connection.purchaseUpdatedStream.last; doesn't help.
If the app for some reason fails to complete the purchase we end up in a situation where:

  • neither purchaseUpdatedStream.listen(..)nor purchaseUpdatedStream.last provide any update
  • queryPastPurchases() does not return incompleted purchase
  • buying product for the second time causes lockdown

Just a quick update from my side. We've shipped the update with the code I've proposed earlier and I'm not aware of any issue right now. Furthermore a customer who couldn't purchase on 13.4 before was then able to do so after updating the app.

However the issue still comes up in testing when switching between sandbox users. I believe the edge cases as outlined by @ostrowp1 are correct and I'm concerned about that as well. I just don't feel terribly confident still about the implementation yet which is partly also due to inconsistencies between demo code on the GitHub project page vs. the demo class. If anybody could briefly read through the class I've outlines earlier that would certainly reassure me whether I've approached this right.

@d9media I think the missing part is completeTransaction is not called.

void _setSubscriptionDB(PurchaseDetails purchaseDetails) async {
    await _userStorage.setSubscription(purchaseDetails);
    // Finish transaction here.
  }

void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // Finish transaction here.
  }

@ostrowp1 Thanks for the detailed update.

As per Apple's documentation, when the receipt verification is failed, call refresh receipt.

If a receipt verification is failed after refreshing the receipt with Apple, then complete the transaction and ask the user to contact Apple if he is charged (Normally will not happen).

If a receipt verification is not failed due the connection error or app kill, then DO NOT complete the transaction.

If you don't complete the transaction, next time when app launches, purchaseUpdatedStream.listen(..) will return this transaction back to you.

hai @LHLL I using example code from this repo and I found this error

Unhandled Exception: PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly usingcompletePurchaseto avoid edge cases.
I have tried all the way from all comment above but still not working

@dwikresno i also received the storekit_duplicate_product_object message, but i cant call completePurchase where i call buyNonConsumable because i don't have PurchaseDetails there. I think you are supposed to call completePurchase from purchaseUpdatedStream right? ..but i don't receive the purchase there.

UPDATE:
queryPastPurchases suddenly returned with my purchase now..

Same problem. Is it bug?

I tried to implement native code in iOS.
it works correctly the following implementation.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        ...
        SKPaymentQueue.default().add(self)
        ...
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

// InAppPurchase
extension AppDelegate: SKPaymentTransactionObserver, SKProductsRequestDelegate, SKRequestDelegate {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction:SKPaymentTransaction in transactions {
           switch transaction.transactionState {
           case SKPaymentTransactionState.purchasing:
               print("purchasing")
           case SKPaymentTransactionState.deferred:
               print("deferred")
           case SKPaymentTransactionState.failed:
               print("failed")
               queue.finishTransaction(transaction)
           case SKPaymentTransactionState.purchased:
               print("purchased")
               queue.finishTransaction(transaction)
           case SKPaymentTransactionState.restored:
               print("restored")
               queue.finishTransaction(transaction)
           }
        }
    }
    ...
}

And not set purchased detail to completePurchase.

InAppPurchaseConnection.instance.purchaseUpdatedStream.listen((purchaseDetailsList) async {
  for (final d in purchaseDetailsList) {
    ...
    if (Platform.isIOS) {
      // finished at native code.
    } else {
      if (d.pendingCompletePurchase) {
        await InAppPurchaseConnection.instance.completePurchase(d);
        logger.fine('finished in app purchased.');
      }
    }
  }
}

Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now. Please don't hesitate to comment on the bug if you have any more information for us; we will reopen it right away!
Thanks for your contribution.

Changed my purchase function to this and it seems to work now. It's really not beautiful, but I guess there's no helping it.

purchase(ProductDetails product) async {
  if (Platform.isIOS) {
    var paymentWrapper = SKPaymentQueueWrapper();
    var transactions = await paymentWrapper.transactions();
    for (var i = 0; i < transactions.length; i++) {
      await paymentWrapper.finishTransaction(transactions[i]);
    } 
    await Future.delayed(Duration(milliseconds: 300));   
  }

  var purchaseParam = PurchaseParam(productDetails: product);
  var success = await connection.buyNonConsumable(
    purchaseParam: purchaseParam
  );
}

im sorry to see that, our team meet the same problem...

but,later,i try to add one sentence code resolved this problem:
FlutterInappPurchase.instance.clearTransactionIOS();
you should add this in your initPlatformState...,like this
image

that means, you should clean the previous transaction on ios. because previous transaction maybe is invalid,you can find the invalid transaction purchaseToken = null.
so,we should clean them before we buy.
hope i can help you.

beautiful

Any dependency between in_app_purchase and flutter_inapp_purchase?
idk why an issue on in_app_purchase will fixed on flutter_inapp_purchase.

but,later,i try to add one sentence code resolved this problem:
FlutterInappPurchase.instance.clearTransactionIOS();
you should add this in your initPlatformState...,like this
image

that means, you should clean the previous transaction on ios. because previous transaction maybe is invalid,you can find the invalid transaction purchaseToken = null.
so,we should clean them before we buy.
hope i can help you.

It seems to work to use the clearTransactionsIOS method of flutter_inapp_purchase in addtion to in_app_purchase in order to clear iOS transactions.

Thanks @Cge001

@erf
Why was this closed? It was not resolved. I feel sorry for the 1000's of other developers that will come across this.

Implementing another library to resolve this issue is unacceptable.

hai @LHLL I using example code from this repo and I found this error

Unhandled Exception: PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly usingcompletePurchaseto avoid edge cases.
I have tried all the way from all comment above but still not working

It's never worked for me this "official" plugin. I've always tried it 9 times now. Always end up using https://github.com/dooboolab/flutter_inapp_purchase

InAppPurchaseConnection.instance.completePurchase never works for well over a year.

@erf why combine the libraries when flutter_inapp_purchase does everything and simpler?

@ollydixon
I have no idea why this was closed.

I had already implemented a solution using the official library, i only needed to clear the pending transactions, and it seems that flutter_inapp_purchase implement this api, but not in_app_purchase. I might convert everything to flutter_inapp_purchase, but it seem like a more complex api, and i would like to use the official package. But you say completePurchase does not work for a year, link?

@ollydixon
I have no idea why this was closed.

I had already implemented a solution using the official library, i only needed to clear the pending transactions, and it seems that flutter_inapp_purchase implement this api, but not in_app_purchase. I might convert everything to flutter_inapp_purchase, but it seem like a more complex api, and i would like to use the official package. But you say completePurchase does not work for a year, link?

I have exactly same observations. This plugin lacks of functions to work on iOS.
There is no way to finish iOS IAP transactions.

@ollydixon
I have no idea why this was closed.

I had already implemented a solution using the official library, i only needed to clear the pending transactions, and it seems that flutter_inapp_purchase implement this api, but not in_app_purchase. I might convert everything to flutter_inapp_purchase, but it seem like a more complex api, and i would like to use the official package. But you say completePurchase does not work for a year, link?

@ming-chu I don't think referencing inapp_purchases is necessary. I believe @DavidKuennen 's approach is right. We implemented the method in our code and will enter testing now. I will come back with more information but I feel this is a good approach.

var paymentWrapper = SKPaymentQueueWrapper();
                                var transactions =
                                    await paymentWrapper.transactions();
                                for (var i = 0; i < transactions.length; i++) {
                                  await paymentWrapper
                                      .finishTransaction(transactions[i]);
                                }

Changed my purchase function to this and it seems to work now. It's really not beautiful, but I guess there's no helping it.

purchase(ProductDetails product) async {
  if (Platform.isIOS) {
    var paymentWrapper = SKPaymentQueueWrapper();
    var transactions = await paymentWrapper.transactions();
    for (var i = 0; i < transactions.length; i++) {
      await paymentWrapper.finishTransaction(transactions[i]);
    } 
    await Future.delayed(Duration(milliseconds: 300));   
  }

  var purchaseParam = PurchaseParam(productDetails: product);
  var success = await connection.buyNonConsumable(
    purchaseParam: purchaseParam
  );
}

This worked like a charm - thanks

@LHLL are you still working on this one?

I think the main issues here are the following:

1) In some apps, the function below, might run before you have a chance to init your app, due to WidgetsFlutterBinding.ensureInitialized() as we do some async calls before calling runApp(MyApp()), meaning that the restored transactions might clog up the PaymentQueue and Apple since 13.4 won't allow you to initiate a new transaction until the PaymentQueue is emptied.

(void)paymentQueue:(SKPaymentQueue *)queue
    updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
} 

2) The Flutter Documentation Example does not mention anything about finishing transactions for iOS when the PurchaseDetails.status == PurchaseStatus.error or PurchaseDetails.status == PurchaseStatus.purchased in the case of subscriptions. Meaning the PaymentQueue gets clogged once again.

I did the following adjustments in my code and it solves some of the issues.

 if (purchaseDetails.status == PurchaseStatus.error) {
     if (Platform.isIOS) {
        SKPaymentQueueWrapper().finishTransaction(purchaseDetails.skPaymentTransaction);
     }

For purchased and restored transactions, I did the following:

in AppDelegate.swift

if call.method == "clearTransactionsIOS"{
  let pendingTrans = SKPaymentQueue.default().transactions;
  for transaction in pendingTrans {
    SKPaymentQueue.default().finishTransaction(transaction)
   }
   result("Cleared Transactions")
}

if you prefere Objective C you can use the code from flutter_inapp_purchase

if ([@"clearTransaction" isEqualToString:call.method]) {
        NSArray *pendingTrans = [[SKPaymentQueue defaultQueue] transactions];
        NSLog(@"\n\n\n  ***  clear remaining Transactions. Call this before make a new transaction   \n\n.");
        for (int k = 0; k < pendingTrans.count; k++) {
            [[SKPaymentQueue defaultQueue] finishTransaction:pendingTrans[k]];
        }
        result(@"Cleared transactions");
    }

In your Dart Code

class InAppPurchaseFix {
  static const platform = const MethodChannel('{YOUR CHANNEL}');
  static Future<String> clearTransactionsIOS() async {
    if (Platform.isIOS) {
      return await platform.invokeMethod('clearTransactionsIOS');
    } else {
      return 'Done, but not IOS';
    }
  }
}

I call InAppPurchaseFix.clearTransactionsIOS() once I initialized the app as it takes some time and each time I finish validating a subscription.

This fixes the rest of the issues.

but,later,i try to add one sentence code resolved this problem:
FlutterInappPurchase.instance.clearTransactionIOS();
you should add this in your initPlatformState...,like this
image

that means, you should clean the previous transaction on ios. because previous transaction maybe is invalid,you can find the invalid transaction purchaseToken = null.
so,we should clean them before we buy.
hope i can help you.

OMG, I got same error and cannot believe the error from the Official package (in_app_purchase) is fixed by 1 line of code from another one (flutter_inapp_purchase). 😆. Anyway, thanks for solution. Hope the official one will fix it in the next version.

I got same error . And fixed it by flutter_inapp_purchase , too . Anyway , thank you very much

Critical. This happens on iOS 13.5+ as well. Any updates?

@LHLL are you still working on this one?

Sorry, didn't get a chance last week, I will work on this from tomorrow.

Just finished reading through all comments in this issue after it was closed by the bot a while ago, looks like this issue was still caused by unfinished transactions.

Dumping all transactions when app starts can suppress this issue, but it doesn't really solve the problem and can introduce side effects:
https://developer.apple.com/documentation/storekit/in-app_purchase/processing_a_transaction?language=objc

Client apps are responsible for validating on-device receipt before finishing a transaction, for apps that deliver products after validating receipts, dumping all transactions upon app launch might cause products not being delivered to the consumer.

Temp solution or you just want to unblock yourself right now would be adding following code to your AppDelegate.swift and run the project once, then remove it:

func application(_ application: UIApplication, 
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
    let pendingTrans = SKPaymentQueue.default().transactions;
    for transaction in pendingTrans {
        SKPaymentQueue.default().finishTransaction(transaction)
    }
}

Is there any update on this?

I am experiencing the same issue, which is preventing my app from being released. I would love an update as well.

Same here

@ntrampe @regulus33 my I suggest https://github.com/dooboolab/flutter_inapp_purchase it works flawlessly on all our apps.

@ollydixon ok looking at it now. If it works flawlessly for auto renewable subscriptions that would be ideal

So I was wondering SKPaymentQueue - is this exclusive to each individual app or would that be a device / iTunes account queue? What happens if say another app on the user's phone leaves unfinished transactions behind, does this affect my app? I'm asking because the reports we've gotten indicated that users could not even get to the paywall screen - obviously because apparently Storekit had unfinished purchases thus didn't return past purchases but strangely those users did not seem to initiate a purchase from our end. Now this may seem like a stupid question but if anybody could clarify how this works internally for iOS that would help.

@regulus33 we have many commercial apps, a lot of them with subscriptions of varying products and it works flawlessly.

Thanks @ollydixon and @LHLL thanks for working on this issue, I appreciate all the work you guys do on these plugins and I wish I had more time to contribute. Hopefully a patch can be released soon for this plugin but for now a switch may be necessary for me.

@ollydixon I just finished implementing flutter_inapp_purchase and it works flawlessly!

I am going to use another plugin to solve my problem.

@LHLL
Thank you for your response.
It's an easy to use package and I'm looking forward to fixing it!

One thing I'd like to know is, roughly, when will you know when the revised version will be available?

but,later,i try to add one sentence code resolved this problem:
FlutterInappPurchase.instance.clearTransactionIOS();
you should add this in your initPlatformState...,like this
image

that means, you should clean the previous transaction on ios. because previous transaction maybe is invalid,you can find the invalid transaction purchaseToken = null.
so,we should clean them before we buy.
hope i can help you.

If I clear all iOS transactions when starting up, how could I process promo code transactions from the App Store?

@markqq this technique has only worked for me for temporarily fixing the problem only to be faced with it again and again. The ios side of this plugin is definitely not ready for production (at least in my case). It may work for some specific corner cases but in its current state I'm not confident about the stability.

If you are pressed for time, I would try flutter_inapp_purchase, some folks in this thread recommended that I use it and I am happy with the results. The implementation is similar enough and you can always switch back to this one when they fix these transaction queue bugs.

If you look at flutter_inapp_purchase I would recommend going straight to the example project, I found their docs to be sort of confusing. Again, this queueing issue is non existent in flutter_inapp_purchase plugin so you would only need to run the above clearTransactionsIos() once after switching. Good luck!

Now all transactions should be passed to purchaseUpdatedStream stream. Following is an example to finish all past transactions upon launch.

class PurchaseService{
    final _connection = InAppPurchaseConnection();

    const PurchaseService(){
          _connection.purchaseUpdatedStream.listen((detail) {
               ...
              // Verify the receipt for successful transactions and handle the error for the failed
              // transaction here.
              _connection.completePurchase(detail);
          });    
    }
}

when does in_app_purchase v0.3.4+1 will be released?
I think it's not builded yet

I just released the v0.3.4+1 which should fix this issue. I apologize for the delay.

thank you!
I checked it too!
chefs kiss!

Now all transactions should be passed to purchaseUpdatedStream stream. Following is an example to finish all past transactions upon launch.

class PurchaseService{
    final _connection = InAppPurchaseConnection();

    const PurchaseService(){
          _connection.purchaseUpdatedStream.listen((detail) {
               ...
              // Verify the receipt for successful transactions and handle the error for the failed
              // transaction here.
              _connection.completePurchase(detail);
          });    
    }
}

Not sure how to implement this. The stream listens for a List of PurchaseDetails whilst the completePurchase function expects just one instance of PurchaseDetails. Some help would be appreciated. I've included my current implementation below.

// App Store and Google Play consumable IDS
const String _diamond50 = 'diamond_50';
const String _diamond150 = 'diamond_150';
const String _diamond300 = 'diamond_300';
const String _diamond500 = 'diamond_500';
String _currentPurchase;

class ShopScreen extends StatefulWidget {
  @override
  _ShopScreenState createState() => _ShopScreenState();
}

class _ShopScreenState extends State<ShopScreen> {
  // IAP Plugin Interface
  final InAppPurchaseConnection _iap = InAppPurchaseConnection.instance;
  // Updates to purchases
  StreamSubscription<List<PurchaseDetails>> _subscription;

  // Products for sale
  List<ProductDetails> _products = [];
  // Past purchases
  List<PurchaseDetails> _purchases = [];
  // Is the API available on the device
  bool _isAvailable = false;
  // Diasble button if purchase pending
  bool _isPurchasePending = true;

  // Consumable credits the user can buy
  int _diamonds = 0;
  int _keys = 0;

  @override
  void initState() {

    _initialise();
    super.initState();
  }

  @override
  void dispose() {
    _subscription.cancel();
    _isPurchasePending = false;
    super.dispose();
  }

  void _initialise() async {
    // Check availilbility of In App Purchases

    _isAvailable = await _iap.isAvailable();

    // await Future.delayed(Duration(seconds: 2));

    if (_isAvailable) {
      List<Future> futures = [_getProducts(), _getPastPurchases()];
      await Future.wait(futures);
      _verifyPurchase();

      // Listen to new purchases

      _subscription = _iap.purchaseUpdatedStream.listen((data) {
        setState(() {
          _purchases.addAll(data);

          _verifyPurchase();


        });
      }

      );
    }
  }

  // Get all products available for sale
  Future<void> _getProducts() async {
    Set<String> ids =
        Set.from([_diamond50, _diamond150, _diamond300, _diamond500]);
    try {
      ProductDetailsResponse response = await _iap.queryProductDetails(ids);
      if (response.notFoundIDs.isEmpty)
        setState(() {
          _products = response.productDetails;
          _products.sort((a, b) => a.title.length.compareTo(b.title.length));
        });
    } catch (e) {
      PlatformAlertDialog(
              title: 'Error', content: e.toString(), defaultActionText: 'OK')
          .show(context);
    }
  }

  // Gets past purchases and consume/complete purchase
  Future<void> _getPastPurchases() async {
    QueryPurchaseDetailsResponse response = await _iap.queryPastPurchases();

    for (PurchaseDetails purchase in response.pastPurchases) {
      if (Platform.isIOS) {
        InAppPurchaseConnection.instance.completePurchase(
          purchase,
        );
      } else
        InAppPurchaseConnection.instance.consumePurchase(
          purchase,
        );
    }

    setState(() {
      _purchases = response.pastPurchases;
    });
  }

  // Returns purchase of specific product ID
  PurchaseDetails _hasPurchased(String productID) {
    return _purchases.firstWhere(
      (purchase) => purchase.productID == productID,
      orElse: () => null,
    );
  }

  void _verifyPurchase() async {
    DatabaseService _databaseService =
        Provider.of<DatabaseService>(context, listen: false);
    UserData _userData = Provider.of<UserData>(context, listen: false);

    PurchaseDetails _purchase = _hasPurchased(_currentPurchase);
    print('productID: ${_purchase?.productID}');

    if (_purchase != null && _purchase.status == PurchaseStatus.purchased) {
      _displayDiamondAndKeys(_purchase);
      _isPurchasePending = true;
      final _updateUserData = UserData(
        displayName: _userData.displayName,
        email: _userData.email,
        locationsExplored: _userData.locationsExplored,
        photoURL: _userData.photoURL,
        uid: _userData.uid,
        points: _userData.points,
         isAdmin: _userData.isAdmin,
        userDiamondCount: _userData.userDiamondCount + _diamonds,
        userKeyCount: _userData.userKeyCount + _keys,
      );
      await _databaseService.updateUserData(userData: _updateUserData);
      final _didSelectOK = await PlatformAlertDialog(
              backgroundColor: Colors.brown,
              titleTextColor: Colors.white,
              contentTextColor: Colors.white,
              title: 'Jackpot!',
              content:
                  'The loot has been added to ye treasure chest. Happy adventures.',
              image: Image.asset('images/ic_thnx.png'),
              defaultActionText: 'Sweet')
          .show(context);
      if (_didSelectOK) {
        _isPurchasePending = false;

      }
    // } else if (_purchase != null &&
    //     _purchase.status == PurchaseStatus.pending) {
    //   _isPurchasePending = true;

    //   PlatformAlertDialog(
    //           backgroundColor: Colors.brown,
    //           titleTextColor: Colors.white,
    //           contentTextColor: Colors.white,
    //           title: 'Purchase Pending',
    //           content:
    //               'Your order is being processed, you\'ll recieve an order update very soon.',
    //           image: Image.asset('images/ic_credit_card.png'),
    //           defaultActionText: 'OK')
    //       .show(context);


    } else if (_purchase != null && _purchase.status == PurchaseStatus.error) {
      _isPurchasePending = true;
      final _didSelectOK = await PlatformAlertDialog(
              backgroundColor: Colors.brown,
              titleTextColor: Colors.white,
              contentTextColor: Colors.white,
              title: 'Shiver Me Timbers!',
              content:
                  'There has been an error whilst processing your payment. Please try again.',
              image: Image.asset('images/ic_owl_wrong.png'),
              defaultActionText: 'OK')
          .show(context);
      if (_didSelectOK) {
        _isPurchasePending = false;

      }
    } else
      setState(() => _isPurchasePending = false);
  }

  void _displayDiamondAndKeys(PurchaseDetails purchase) {
    switch (purchase.productID) {
      case _diamond50:
        _diamonds = 50;
        _keys = 0;
        break;
      case _diamond150:
        _diamonds = 150;
        _keys = 1;
        break;
      case _diamond300:
        _diamonds = 300;
        _keys = 3;
        break;
      case _diamond500:
        _diamonds = 500;
        _keys = 5;
        break;
    }
  }

  void _buyProduct(ProductDetails productDetails) async {
    _isPurchasePending = true;
    final PurchaseParam purchaseParam =
        PurchaseParam(productDetails: productDetails);
// if (Platform.isIOS) {
//     var paymentWrapper = SKPaymentQueueWrapper();
//     var transactions = await paymentWrapper.transactions();
//     for (var i = 0; i < transactions.length; i++) {
//       await paymentWrapper.finishTransaction(transactions[i]);
//     } 
//     await Future.delayed(Duration(milliseconds: 300));   
//   }
    try {
      await _iap.buyConsumable(
        purchaseParam: purchaseParam,
      );

      await _getPastPurchases();
    } catch (e) {
      _isPurchasePending = false;
      print(e.toString());
    }
  }

  @override
  Widget build(BuildContext context) {
    final UserData _userData = Provider.of<UserData>(context);
    return Scaffold(
      backgroundColor: Colors.brown,
      appBar: AppBar(
        title: _isAvailable ? null : Text('Shop Loading...'),
        backgroundColor: Colors.brown,
        iconTheme: const IconThemeData(color: Colors.white),
        actions: <Widget>[
          DiamondAndKeyContainer(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            numberOfDiamonds: _userData.userDiamondCount,
            numberOfKeys: _userData.userKeyCount,
            color: Colors.white,
          ),
          const SizedBox(
            width: 20,
          )
        ],
      ),
      body: Container(
        width: double.infinity,
        decoration: const BoxDecoration(
          image: DecorationImage(
            alignment: Alignment.bottomCenter,
            image: AssetImage(
              "images/background_shop.png",
            ),
            fit: BoxFit.cover,
          ),
        ),
        child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
          const SizedBox(
            height: 10,
          ),
          IconButton(
            icon: Icon(
              Icons.help_outline,
              color: Colors.brown.shade300,
              size: 40,
            ),
            tooltip: 'Store questions',
            onPressed: () async {
              final _didSelectOK = await PlatformAlertDialog(
                      backgroundColor: Colors.brown,
                      titleTextColor: Colors.white,
                      contentTextColor: Colors.white,
                      title: 'Welcome to The Shop',
                      content:
                          'Stock ye treasure chest with diamonds and keys. Use \'em to unlock quests and ye can also trade \'em for hints',
                      image: Image.asset('images/ic_thnx.png'),
                      defaultActionText: 'OK')
                  .show(context);
              if (_didSelectOK) {}
            },
          ),
          const SizedBox(
            height: 10,
          ),


          if (_isAvailable)
            // Display products from store
            for (var prod in _products)
              Padding(
                padding: const EdgeInsets.only(bottom: 10.0),
                child: BuyDiamondOrKeyButton(
                  numberOfDiamonds: numberOfDiamonds(prod.price),
                  diamondCost: prod.price,
                  bonusKey: numberOfKeys(prod.price),
                  isPurchasePending: _isPurchasePending,
                  onPressed: () {
                    _buyProduct(prod);
                    _currentPurchase = prod.id;
                  },
                ),
              )
          else
            Column(
              children: <Widget>[
                storeLoading(),
                storeLoading(),
                storeLoading(),
                storeLoading(),
              ],
            )
        ]),
      ),
    );
  }




Updated to the latest version, I am still experiencing the same issue.

For anyone still having the issue with the newest update..
This is how I fixed it, both solutions worked for me

https://github.com/flutter/flutter/issues/57903#issuecomment-653975598

So from spending a DECENT amount of digging in here is my take:
iOS subscriptions transactionState will inevitably change to this at some point:
case restored = 3 // Transaction was restored from user's purchase history. Client should complete the transaction.

The only way to receive this transaction on flutter side is when you call await InAppPurchaseConnection.instance.queryPastPurchases(); BEFORE you subscribe to
InAppPurchaseConnection.instance.purchaseUpdatedStream;.

But all this isn't even a problem why things get stuck. The problem is with the
- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result method or the way transactions are stored in memory with this lib. You pass a productId to this method to complete transactions but Subscriptions will have multiple transactions but this IAP plugin will only save the last one in the array in the self.transactionsSetter because it uses productId as a key. So the initial transaction that will have the transactionState equal to 3 will be impossible to complete from flutter side because of the way transactions are stored in the transactionsSetter. I can't provide a solution because I can't code ObjectiveC, but please fix this issue ASAP, users can't resubscribe because of this issue.

I hope my research will be useful for a faster solution.

P.S. Maybe the solution is to store an array of all transactions for specific productId and when user calls finishTransaction it loops through all transactions for that productID and complete the ones that need to be completed.

P.S.S
This method is also part of the problem:

- (BOOL)addPayment:(SKPayment *)payment {
  if (self.transactionsSetter[payment.productIdentifier]) {
    return NO;
  }
  [self.queue addPayment:payment];
  return YES;
}

Because It makes impossible for user to re-subscribe if he was once a subscriber but cancelled. Why you do you even need to check that? Appstore will warn you if you have already purchesed an item or are an active subscriber.

As @ziggycrane mentioned this is still not working with the latest update v0.3.4+1.

The error message I see from Crashlytics is "PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly using completePurchase to avoid edge cases".

Yeah, I also have the same issue...

Same I'm still having the same issue

So from spending a DECENT amount of digging in here is my take:
iOS subscriptions transactionState will inevitably change to this at some point:
case restored = 3 // Transaction was restored from user's purchase history. Client should complete the transaction.

The only way to receive this transaction on flutter side is when you call await InAppPurchaseConnection.instance.queryPastPurchases(); BEFORE you subscribe to
InAppPurchaseConnection.instance.purchaseUpdatedStream;.

@ziggycrane querying queryPastPurchases() before listening to purchaseUpdate seems a bit counter-intuitive to me. What is @LHLL take on this?

Currently my client is going through major anxiety because 1 in maybe 5 purchases seem to fail; the purchase is not being delivered that is. Since Apple isn't allowing dev's to test Storekit on the simulator I had to spent countless amount of hours on browserstack trying to debug the issue, it is such a mess from Apple's end. Now I have trouble justifying staying on the package even against my client. I don't know what to do so I'll probably file a report with apple code level support because i am so confused with this.

I strongly recommended flutter_in_app_purchase
It handles every case very well

I strongly recommended flutter_in_app_purchase
It handles every case very well

You mean inapp_purchase? It's deprecated when I first looked into it... Could flutter not merge the code base of the two? People seem very happy with it

Sorry typo
I was thinking about flutter_inapp_purchase

It was stopped in some point,but recently they get it going again
It's awesome really

I also recommend flutter_in_app_purchase. Unlike this package, it actually
works for subscriptions in iOS. It is documented more thoroughly and the
underlying swift code is battle tested. it was used in a popular react
native billing library for years and the maintainer was kind enough to port
a flutter version for us ❤️

On Thu, Jul 30, 2020, 8:49 PM Daniel Schaefer notifications@github.com
wrote:

So from spending a DECENT amount of digging in here is my take:
iOS subscriptions transactionState will inevitably change to this at some
point:
case restored = 3 // Transaction was restored from user's purchase
history. Client should complete the transaction.

The only way to receive this transaction on flutter side is when you call
await InAppPurchaseConnection.instance.queryPastPurchases(); BEFORE you
subscribe to
InAppPurchaseConnection.instance.purchaseUpdatedStream;.

@ziggycrane https://github.com/ziggycrane querying queryPastPurchases()
before listening to purchaseUpdate seems a bit counter-intuitive to me.
What is @LHLL https://github.com/LHLL take on this?

Currently my client is going through major anxiety because 1 in maybe 5
purchases seem to fail; the purchase is not being delivered that is. Since
Apple isn't allowing dev's to test Storekit on the simulator I had to spent
countless amount of hours on browserstack trying to debug the issue, it is
such a mess from Apple's end. Now I have trouble justifying staying on the
package even against my client. I don't know what to do so I'll probably
file a report with apple code level support because i am so confused with
this.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/flutter/flutter/issues/53534#issuecomment-666597777,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AEDE3LNZCBNDBJQGU3X2IDLR6G6D7ANCNFSM4LWDFULQ
.

To fix this, I used purchases_flutter instead

On Thu, Jul 30, 2020 at 3:17 PM Danis Preldžić notifications@github.com
wrote:

https://pub.dev/packages/flutter_inapp_purchase


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/flutter/flutter/issues/53534#issuecomment-666621072,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AGH6KGEJNPZUB4RJNSKBWTLR6HBM7ANCNFSM4LWDFULQ
.

I also recommend flutter_in_app_purchase. Unlike this package, it actually works for subscriptions in iOS. It is documented more thoroughly and the underlying swift code is battle tested. it was used in a popular react native billing library for years and the maintainer was kind enough to port a flutter version for us ❤️

On Thu, Jul 30, 2020, 8:49 PM Daniel Schaefer @.*> wrote: So from spending a DECENT amount of digging in here is my take: iOS subscriptions transactionState will inevitably change to this at some point: case restored = 3 // Transaction was restored from user's purchase history. Client should complete the transaction. The only way to receive this transaction on flutter side is when you call await InAppPurchaseConnection.instance.queryPastPurchases(); BEFORE you subscribe to InAppPurchaseConnection.instance.purchaseUpdatedStream;. @ziggycrane https://github.com/ziggycrane querying queryPastPurchases() before listening to purchaseUpdate seems a bit counter-intuitive to me. What is @LHLL https://github.com/LHLL take on this? Currently my client is going through major anxiety because 1 in maybe 5 purchases seem to fail; the purchase is not being delivered that is. Since Apple isn't allowing dev's to test Storekit on the simulator I had to spent countless amount of hours on browserstack trying to debug the issue, it is such a mess from Apple's end. Now I have trouble justifying staying on the package even against my client. I don't know what to do so I'll probably file a report with apple code level support because i am so confused with this. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#53534 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEDE3LNZCBNDBJQGU3X2IDLR6G6D7ANCNFSM4LWDFULQ .

Thanks it's just hard to drop it after all the time getting it to run on Google's plugin, not to speak the time it'd take to transition. I'll give the next update a chance. I know LHLL and some others are very committed to solving this for good

Any news on this issue?

@pinpong My pull request (link above) fixes the issue, I am using it in my production app already. If you try it, make sure you also call completePurchase when required (see example app), otherwise you will have this issue again.

Currently I call also this method before launching the purchase dialog (notice that this code works properly only if my PR is applied):

  Future<void> clearTransactionsIos() async {
    final transactions = await SKPaymentQueueWrapper().transactions();
    for (final transaction in transactions) {
      try {
        if (transaction.transactionState !=
            SKPaymentTransactionStateWrapper.purchasing) {
          await SKPaymentQueueWrapper().finishTransaction(transaction);
        }
      } catch (e) {
        print(e);
      }
    }
  }

Calling that method should not be necessary in normal circumstances, but it may be necessary for example now due to the current issues in this plugin (your earlier calls to completePurchase may not have been processed properly, so you need to "reset" the transaction queue). I am planning to remove that code later after all my users have upgraded to this fixed version.

@kinex i wrote me a helper class to handle the iap. Thanks for the PR. Do you know why it's not working on Simulator? I get no error neither a purchase dialog.

```dart
abstract class IAPHelper {
bool available = false;

bool initialized = false;

final InAppPurchaseConnection iap = InAppPurchaseConnection.instance;

List availableProducts = [];

List purchases = [];

StreamSubscription subscription;

bool purchased = false;

void initIAP() async {
await clearTransactionsIos();
available = await iap.isAvailable();

if (available) {
  await _getProducts();
  await _getPastPurchases();
  _verifyPurchase();
}

subscription = iap.purchaseUpdatedStream.listen(
    (purchaseDetails) => () {
          purchases.addAll(purchaseDetails);
          _verifyPurchase();
        }, onDone: () {
  subscription?.cancel();
}, onError: (e) {
  initialized = false;
  onLoaded.call(initialized);
});

Future.delayed(Duration(milliseconds: 1000))
    .then((value) => {initialized = true, onLoaded.call(initialized)});

}

void disposeIAP() async {
subscription?.cancel();
}

Future clearTransactionsIos() async {
if (Platform.isIOS) {
final transactions = await SKPaymentQueueWrapper().transactions();
for (final transaction in transactions) {
try {
if (transaction.transactionState !=
SKPaymentTransactionStateWrapper.purchasing) {
await SKPaymentQueueWrapper().finishTransaction(transaction);
}
} catch (e) {
print(e);
}
}
}
}

List _getProductIDs() {
return [Constants.MONTH_ABO_ID, Constants.YEAR_ABO_ID];
}

Future _getProducts() async {
ProductDetailsResponse response =
await iap.queryProductDetails(_getProductIDs().toSet());
availableProducts = response.productDetails;
}

Future _getPastPurchases() async {
QueryPurchaseDetailsResponse response = await iap.queryPastPurchases();
if (response.error != null) {
// Handle the error.
}
for (final purchase in response.pastPurchases) {
if (purchase.pendingCompletePurchase) {
if (Platform.isIOS) {
await iap.completePurchase(purchase);
}
}
}

purchases = response.pastPurchases;

}

PurchaseDetails _hasPurchased(String productID) {
return purchases.firstWhere((purchase) => purchase.productID == productID,
orElse: () => null);
}

void subscribeProduct(ProductDetails prod) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: prod);
iap.buyNonConsumable(purchaseParam: purchaseParam);
}

void _verifyPurchase() async {
for (String id in _getProductIDs()) {
PurchaseDetails purchase = _hasPurchased(id);
if (purchase != null) {
switch (purchase.status) {
case PurchaseStatus.pending:
onPending.call(purchase);
break;
case PurchaseStatus.purchased:
purchased = true;
onSuccessPurchase.call(purchase);
break;
case PurchaseStatus.error:
purchased = false;
onBillingError.call(purchase.error);
break;
}
}
}
onLoaded.call(initialized);
}

void onLoaded(bool initialized) {}

void onPending(PurchaseDetails product) {}

void onSuccessPurchase(PurchaseDetails product) {}

void onBillingError(IAPError error) {}
}

@pinpong IAPs must be tested in a real device. In addition your code is not calling completePurchase in the iap.purchaseUpdatedStream.listen handler. Please refer to the plugin example app and maybe better to ask further questions in StackOverflow.

@cyanglaz it looks like a lot of valuable anecdotes were added to this issue after it was closed. Is there another ticket tracking the post-close issue? I think I'm in the middle of this problem, and if I'm understanding things correctly, this is a serious and fundamental problem in the plugin.

@ziggycrane's comment seems like it should be addressed:
https://github.com/flutter/flutter/issues/53534#issuecomment-657269507

Should we file a new ticket? Is this already being discussed/handled somewhere?

v0.3.4+1

Using v0.3.4+16 with same error

Could everyone who still has this problem please file a new issue with the exact description of what happens, logs, and the output of flutter doctor -v.
All system setups can be slightly different, so it's always better to open new issues and reference related issues.

Could everyone who still has this problem please file a new issue with the exact description of what happens, logs, and the output of flutter doctor -v.
All system setups can be slightly different, so it's always better to open new issues and reference related issues.

I always get the following problem:

PlatformException (PlatformException(storekit_getproductrequest_platform_error, The operation couldn’t be completed. (SKErrorDomain error 0.), Error Domain=SKErrorDomain Code=0 "(null)" UserInfo={NSUnderlyingError=0x600000b97870 {Error Domain=ASDErrorDomain Code=507 "Error decoding object" UserInfo={NSLocalizedDescription=Error decoding object, NSLocalizedFailureReason=Attempted to decode store response}}}, null))

[✓] Flutter (Channel stable, 1.22.3, on Mac OS X 10.15.7 19H15, locale en-GB)
    • Flutter version 1.22.3 at /Users/azzeccagarbugli/dev/flutter
    • Framework revision 8874f21e79 (2 weeks ago), 2020-10-29 14:14:35 -0700
    • Engine revision a1440ca392
    • Dart version 2.10.3


[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/azzeccagarbugli/Library/Android/sdk/
    • Platform android-30, build-tools 30.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 12.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.1, Build version 12A7403
    • CocoaPods version 1.9.3

[!] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.51.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.16.0

[✓] Connected device (1 available)
    • iPhone 11 (mobile) • 64B21A14-8BB8-4BCB-82B6-BDF227D99402 • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-5 (simulator)
    ! Error: iPhone is not connected. Xcode will continue when iPhone is connected. (code -13)

! Doctor found issues in 1 category.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dedeswim picture dedeswim  ·  235Comments

JonathanSum picture JonathanSum  ·  203Comments

cbazza picture cbazza  ·  238Comments

LouisCAD picture LouisCAD  ·  139Comments

krisgiesing picture krisgiesing  ·  183Comments