v4.0.0: remove the Checkout and Admin API surfaces, plus parsing and error-handling fixes - #149
Conversation
|
Thank you @qeepcologne for this major update. It looks like you have completely removed the Checkout API in both the package and the example app, which is deprecated and will be removed. But it would be great if you could add its replacement i.e., the Cart API, to imitate the current checkout flow. Checkout is the most important part in the ecommerce app and it would be best for the package to show how the checkout flow can be achieved with an example Also, as for the admin config, it is an intended config. The android play store needs an feature to be able to delete the account if login/register is in the app. So, we need the admin config and related APIs as it is. It is a optional setup |
Reverts the admin removal per maintainer feedback on imsujan276#149: the config is intentional, and deleteCustomer backs the account-deletion flow the Play Store requires when an app offers sign-up/sign-in. Restores adminAccessToken/adminCache, the admin client and its getter, ShopifyAuth.deleteCustomer with the customerDelete mutation, adminAccess on ShopifyCustom.customQuery/customMutation, and the example's admin usage, README config docs and .env.example entry. Two admin paths now raise a ShopifyException naming the missing config rather than an opaque error, matching the error handling elsewhere in this branch: deleteCustomer threw a bare String, and adminAccess: true without a token threw "Null check operator used on a null value". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the review note on imsujan276#149 asking the example to demonstrate the checkout flow that replaces the removed Checkout API. The flow already existed but was hidden behind an unlabelled Icons.logout button in the cart sheet header. Replace it with a full-width "Checkout · <total>" button pinned at the bottom of the sheet, and add a comment explaining that Shopify hosts the checkout and cart.checkoutUrl is the URL to send the buyer to (no checkout mutation exists). Verified end to end on an emulator against a live store: the button opens the hosted Shopify checkout (order total, express checkout, card and PayPal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
signInWithEmailAndPassword collapsed every failure to
Exception('Invalid credentials') because _createAccessToken never
checked for errors — a rate-limit, an unactivated account, or a missing
unauthenticated_write_customers scope all looked like a wrong password.
Request customerUserErrors on the customerAccessTokenCreate mutation and
run checkForError in _createAccessToken, so the real reason (Shopify's
message, or the top-level GraphQL/scope error) is raised as a
ShopifyException. The "Invalid credentials" null-token fallback remains
for the no-error-but-no-token case.
Verified live: a scope-restricted token now surfaces the ACCESS_DENIED
message instead of "Invalid credentials".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review and quick response.
|
Follows on from #147/#148. Branched from current
main(including 3.0.1), so it merges cleanly.Two API surfaces are removed here, which is why this is
4.0.0. The second one is a judgement call and I'd understand pushback — details below, and it's straightforward to drop that commit if you'd rather keep it.Removals
Checkout API — cannot work on any supported version
Shopify deprecated the Checkout APIs in 2024-04, removed the checkout types in 2024-07, and shut the endpoints off on 1 April 2025. Against a live 2026-07 storefront:
So
ShopifyCheckout's 20 methods could only ever fail, with a confusingField '...' doesn't existerror. Removed: the class, 26 GraphQL documents, the checkout-only models, andJsonHelper.lineItems.Migration:
ShopifyCart+cart.checkoutUrl, which is Shopify's documented replacement. The example already worked this way.MailingAddresswas filed undersrc/checkout/but is unrelated to it (it backsCustomer.defaultAddress), so it moved tosrc/mailing_address/rather than being deleted.Admin API — a security judgement, not a lifecycle one
To be clear:
customerDeleteis not deprecated. It is current and working. This is removed on the argument that a Flutter package should not invite Admin credentials into a client at all: a Storefront token is designed to be public and scope-limited, an Admin token grants broad read/write over the whole shop, and anything in a distributed binary can be extracted. Admin work belongs behind a backend.Removed:
adminAccessToken/adminCacheonsetConfig, the admin client andgraphQLClientAdmin,ShopifyAuth.deleteCustomer, andadminAccessonShopifyCustom.customQuery/customMutation.This also drops a latent bug — the admin client built its URL from
storefrontApiVersion, pinning the independently-versioned Admin API to a Storefront version.If you'd rather keep Admin support, dropping the
remove admin apicommit leaves the rest intact. A middle ground would be keeping it with a documented warning plus a separateadminApiVersion.Fixes
Several of these are silent data loss rather than crashes:
_renewAccessTokennever checked for errors and fell back to an empty token, which_setShopifyUserread as "no session" and deleted from memory and disk. An offline refresh logged users out with no error and no way back.quantityAvailablewiped a product's variants. The field is nullable (only returned withunauthenticated_read_product_inventory) but was parsed into a non-nullint;_getProductVariantsswallowed the error and returned[], so affected stores got products with no variants andprice0.0.isAvailableForSalerequiredquantityAvailable > 0, marking purchasable products unavailable both without the inventory scope and on stores that allow overselling — those report a negative quantity whileavailableForSalestaystrue. It now followsavailableForSale, which Shopify documents as authoritative._getMetafieldListreadjson['node']['metafields']inside the branch only reached when there is no'node'key, sogetProductByHandle/getCollectionByHandlenever returned requested metafields.Order.financialStatus/subtotalPrice/customerUrl/totalTax(a fully discounted order has no tax) failed the whole order list;Page.onlineStoreUrlis null for unpublished pages; onlyidis non-null onMailingAddress.String— not anException, so it defeatedon Exceptionas well ason ShopifyExceptionand escaped unhandled. With an emptygraphqlErrors(sockets, timeouts, 5xx) the thrown value was the empty string, discarding the cause. Now aShopifyExceptionincludinglinkException.getCollectionByIdreported failures as "not found" — a bad token was indistinguishable from a missing collection, and not-found only worked by letting the parse throw and swallowing it.BYR/STD/VEFbut notBYN/STN/VES, so those stores formatted every price as"null12.50".ShopifyStorereturned values that cannot be constructed and always threw aTypeErrorfrom outside thetry.getAllProductsOnQuerysilently ignored itscursorargument (a local shadowed the parameter), so paging restarted from page one.getAllOrderssent a read-only query throughmutate(), bypassing the cache and ignoringfetchPolicy._setShopifyUsernever awaited itsSharedPreferenceswrites.ShopifyExceptionandAttributeInputare now exported — both previously needed asrc/import, andupdateCartAttributeswas uncallable without one.Other
Future<List<X>?>toFuture<List<X>>where no return path could produce null. Existing calls still compile;?? []becomes a dead-code warning. Methods that genuinely return null keep the?.json_serializablemoved todev_dependencies— it is codegen-only, and as a runtime dependency it pulledanalyzer,build,source_genetc. into every consuming app.url_launcherremoved (unused).addDeliveryAddressesdocumented.Checks
flutter analyze --fatal-infosclean on package and example; 17 tests pass, including regressions for each parsing fix above. Documents were replayed against a live 2026-07 storefront, and a schema-driven pass over every field, argument, input-object field and enum value the package uses found no remaining deprecated usage. The example app runs against a live store.