Skip to content

v4.0.0: remove the Checkout and Admin API surfaces, plus parsing and error-handling fixes - #149

Merged
imsujan276 merged 14 commits into
imsujan276:mainfrom
qeepcologne:sync-4.0.0
Aug 6, 2026
Merged

v4.0.0: remove the Checkout and Admin API surfaces, plus parsing and error-handling fixes#149
imsujan276 merged 14 commits into
imsujan276:mainfrom
qeepcologne:sync-4.0.0

Conversation

@qeepcologne

Copy link
Copy Markdown
Contributor

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:

__type(name: "Checkout")   -> null (not in schema)
checkout* mutations        -> none exist on Mutation
get_checkout_* queries     -> "No such type Checkout, so it can't be a fragment condition"

So ShopifyCheckout's 20 methods could only ever fail, with a confusing Field '...' doesn't exist error. Removed: the class, 26 GraphQL documents, the checkout-only models, and JsonHelper.lineItems.

Migration: ShopifyCart + cart.checkoutUrl, which is Shopify's documented replacement. The example already worked this way.

MailingAddress was filed under src/checkout/ but is unrelated to it (it backs Customer.defaultAddress), so it moved to src/mailing_address/ rather than being deleted.

Admin API — a security judgement, not a lifecycle one

To be clear: customerDelete is 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/adminCache on setConfig, the admin client and graphQLClientAdmin, ShopifyAuth.deleteCustomer, and adminAccess on ShopifyCustom.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 api commit leaves the rest intact. A middle ground would be keeping it with a documented warning plus a separate adminApiVersion.

Fixes

Several of these are silent data loss rather than crashes:

  • A failed token renewal signed the user out. _renewAccessToken never checked for errors and fell back to an empty token, which _setShopifyUser read as "no session" and deleted from memory and disk. An offline refresh logged users out with no error and no way back.
  • A null quantityAvailable wiped a product's variants. The field is nullable (only returned with unauthenticated_read_product_inventory) but was parsed into a non-null int; _getProductVariants swallowed the error and returned [], so affected stores got products with no variants and price 0.0.
  • isAvailableForSale required quantityAvailable > 0, marking purchasable products unavailable both without the inventory scope and on stores that allow overselling — those report a negative quantity while availableForSale stays true. It now follows availableForSale, which Shopify documents as authoritative.
  • Metafields were always dropped on unwrapped payloads: _getMetafieldList read json['node']['metafields'] inside the branch only reached when there is no 'node' key, so getProductByHandle/getCollectionByHandle never returned requested metafields.
  • Nullable fields threw while parsingOrder.financialStatus/subtotalPrice/customerUrl/totalTax (a fully discounted order has no tax) failed the whole order list; Page.onlineStoreUrl is null for unpublished pages; only id is non-null on MailingAddress.
  • Network failures threw a bare String — not an Exception, so it defeated on Exception as well as on ShopifyException and escaped unhandled. With an empty graphqlErrors (sockets, timeouts, 5xx) the thrown value was the empty string, discarding the cause. Now a ShopifyException including linkException.
  • getCollectionById reported 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.
  • Deprecated currency codes: the symbol table had BYR/STD/VEF but not BYN/STN/VES, so those stores formatted every price as "null12.50".
  • Three unreachable fallbacks in ShopifyStore returned values that cannot be constructed and always threw a TypeError from outside the try.
  • getAllProductsOnQuery silently ignored its cursor argument (a local shadowed the parameter), so paging restarted from page one.
  • getAllOrders sent a read-only query through mutate(), bypassing the cache and ignoring fetchPolicy.
  • Pagination loops now stop on an empty page instead of re-issuing the same request forever.
  • _setShopifyUser never awaited its SharedPreferences writes.
  • ShopifyException and AttributeInput are now exported — both previously needed a src/ import, and updateCartAttributes was uncallable without one.

Other

  • Twelve methods narrowed from Future<List<X>?> to Future<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_serializable moved to dev_dependencies — it is codegen-only, and as a runtime dependency it pulled analyzer, build, source_gen etc. into every consuming app. url_launcher removed (unused).
  • README signatures corrected against the actual API (7 were stale, 5 of them pre-existing), and addDeliveryAddresses documented.

Checks

flutter analyze --fatal-infos clean 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.

@imsujan276

Copy link
Copy Markdown
Owner

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

qeepcologne and others added 4 commits July 24, 2026 17:16
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>
@qeepcologne

Copy link
Copy Markdown
Contributor Author

Thanks for the review and quick response.
Please check again:

  • Admin API — reverted the removal;
  • Checkout example — the Cart API flow (cart.checkoutUrl → webview) is now a labelled "Checkout · " button in the cart, verified opening the live hosted Shopify checkout on device.
  • Login error handling — signInWithEmailAndPassword now surfaces Shopify's real reason (wrong password, unactivated, rate-limit, missing scope) instead of a blanket Invalid credentials.

@imsujan276
imsujan276 merged commit 58463e4 into imsujan276:main Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants