Hosted selection page
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
The hosted selection page is Globe’s pickup point picker on its own page, served on your store’s domain. Open it from a mobile app or a headless storefront with a Storefront API cart ID. The buyer picks a point, Globe saves it on the cart, and your app gets the result.
Set it up first in Settings → Headless and mobile apps. See Set up Globe.
https://{shop domain}/apps/appfleece-globe/select?cart={cart id}&return_url={return URL}&state={state}&locale={locale}&country={country}{shop domain} is your store’s own storefront domain: your-store.myshopify.com or a custom domain of the online store. Shopify serves the page through Globe’s app proxy, so it works only on domains the online store answers on. If your custom domain points to a Hydrogen or other headless storefront, use the myshopify.com domain.
| Parameter | Required | Description |
|---|---|---|
cart | Yes | The Storefront API cart ID, gid://shopify/Cart/<token>?key=<key>, URL-encoded. A bare cart token also works |
return_url | No | Where to send the buyer after they pick a point or close the picker. Must match one of your return URLs |
state | No | Any value of 1 to 200 characters from A–Z a–z 0–9 . _ ~ : -. Returned unchanged with the result. Other values are ignored |
locale | No | en, cs, sk, ro, hu, pl or de. Without it, the page uses the browser’s Accept-Language, then English |
country | No | ISO 3166-1 alpha-2 delivery country, for a cart that has no delivery address yet. Otherwise the cart’s country applies |
Encode the cart ID. It contains ? and =, which break the URL when sent as they are.
What the page does
Section titled “What the page does”- Shows Globe’s map and list, with search, the buyer’s location and a carrier filter. It is the same picker as the cart widget, with the carriers you mapped for the delivery country.
- When the buyer picks a point, Globe verifies the point with the carrier’s network.
- Globe writes the cart attributes to the cart with the Storefront API mutation
cartAttributesUpdate. Other attributes on the cart are kept. - If Checkout address prefill is on in Settings → Cart Widget, Globe also sets the point’s address as the cart’s delivery address, for one use.
- The page reports the result in every way described below.
Return URLs
Section titled “Return URLs”A return_url is allowed when it starts with one of the return URLs you added in the Globe admin:
- the same scheme,
- exactly the same host (subdomains do not match),
- a path at or under the saved path.
The query and fragment of return_url are free. The page adds its own parameters.
| Return URL in Globe | Allowed return_url | Refused |
|---|---|---|
myapp://pickup | myapp://pickup, myapp://pickup/cz?from=cart | myapp://checkout, otherapp://pickup |
https://shop.example.com/cart | https://shop.example.com/cart?step=2 | https://www.shop.example.com/cart, https://shop.example.com/account |
You can save up to 10 return URLs. Saved URLs cannot have a query or fragment. Web addresses must use https, except http://localhost and http://127.0.0.1 for development. Custom schemes, universal links and Android App Links are all accepted.
Result
Section titled “Result”The page sends the result every way it can, so use whichever fits your app. Handle the result once, because more than one may arrive.
Redirect
Section titled “Redirect”With a return_url, the page navigates to it with these query parameters added:
| Parameter | Description |
|---|---|
status | selected or cancelled |
point_id | The point’s ID. Only for selected |
carrier | The carrier code, for example packeta. Only for selected |
point_name | The point’s name. Only for selected |
state | Your state, if you sent one |
myapp://pickup?status=selected&point_id=12345&carrier=packeta&point_name=Z-BOX%20Praha%201&state=c8f2Without a return_url, the page shows a short message that the buyer can return to the app.
Native bridges
Section titled “Native bridges”If the page runs in a WebView that exposes one of these objects, it calls it with the result message:
| Platform | Bridge | Argument |
|---|---|---|
iOS (WKWebView) | window.webkit.messageHandlers.globePickup.postMessage(message) | The message as an object |
Android (WebView) | window.GlobePickupAndroid.postMessage(json), an @JavascriptInterface | The message as a JSON string |
React Native (react-native-webview) | window.ReactNativeWebView.postMessage(json) | The message as a JSON string |
postMessage to the opener
Section titled “postMessage to the opener”If the page was opened with window.open or in an iframe, it posts the message to window.opener or window.parent. It posts only to the web origins of your https return URLs. Add your storefront’s address as a return URL, for example https://shop.example.com/cart, even if you never redirect to it.
Message
Section titled “Message”{ "type": "globe:pickup-point", "status": "selected", "state": "c8f2", "cartId": "gid://shopify/Cart/c1-7a2b9e?key=4f6d0c", "point": { "id": "12345", "carrier": "packeta", "carrierId": "64aaaaaaaaaaaaaaaaaaaaa1", "name": "Z-BOX Praha 1, Karlova", "address": "Karlova 3, 11000 Praha" }}| Field | Description |
|---|---|
type | Always globe:pickup-point. Ignore other messages |
status | selected or cancelled |
state | Your state, or null |
cartId | The cart the point was saved on |
point | For selected: id, carrier (carrier code), carrierId (your Globe carrier instance), name and address. null for cancelled |
After the result
Section titled “After the result”The message only tells your app what happened. The cart is the source of truth. Query the cart’s attributes again, show the chosen point if you want to, and continue to the cart’s checkoutUrl.
query Cart($cartId: ID!) { cart(id: $cartId) { checkoutUrl attributes { key value } }}Errors
Section titled “Errors”The page shows a short error in the buyer’s language when it cannot run. Nothing is written to the cart, and no result is sent.
| Cause | Status | What the buyer sees |
|---|---|---|
cart is missing or not a cart ID | 400 | The link is missing the cart |
| The cart does not exist: it expired, became an order, or belongs to another store | 404 | This cart no longer exists |
return_url does not match a return URL | 400 | This return address is not allowed for this shop |
| The page is turned off | 403 | Pickup point selection is not enabled for this shop |
| Globe has no active plan, or the storefront cart permissions were not approved | 402, 503 | Pickup point selection is temporarily unavailable |
When the delivery country has no pickup point carriers, the page says so and offers a link back to return_url with status=cancelled. It sends no bridge or postMessage result in that case, so give the buyer a way to close the view.
If saving fails, for example because the carrier no longer has the point, the picker shows an alert and stays open, so the buyer can choose another point.
Privacy
Section titled “Privacy”The cart ID gives access to the buyer’s cart. The page is served with Cache-Control: no-store and Referrer-Policy: no-referrer, so it is not cached and does not pass its URL to map tiles or other requests. Do not log the full page URL in your app or analytics.
WebView requirements
Section titled “WebView requirements”- JavaScript on. Android also needs DOM storage.
- No third-party cookies are needed.
- Catch your
return_urlin the WebView’s navigation callback, so the WebView does not try to load it. - On iOS, implement
WKUIDelegate’swebView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:).WKWebViewdrops JavaScript alerts without it, and the page uses one when a point cannot be saved. - If the online store is password protected, as development stores are, the page shows Shopify’s password page first. Enter the storefront password once in the same WebView.
Location on iOS. Add NSLocationWhenInUseUsageDescription to your app’s Info.plist. WKWebView then uses the app’s location permission, and asks the buyer once per site when the page requests the location. Without the key, the page cannot get the location and the buyer searches by address instead.
Location on Android. Declare ACCESS_FINE_LOCATION in the manifest, call setGeolocationEnabled(true), and implement WebChromeClient.onGeolocationPermissionsShowPrompt. Request the runtime permission there and pass the answer to the callback.
iOS (Swift, WKWebView)
Section titled “iOS (Swift, WKWebView)”The view controller registers the globePickup message handler and also catches the myapp://pickup redirect.
import UIKitimport WebKit
final class PickupPointViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate { private let shopDomain = "your-store.myshopify.com" private let returnURL = "myapp://pickup" private let cartId: String private let onFinish: (_ status: String, _ pointId: String?) -> Void private var webView: WKWebView! private var finished = false
init(cartId: String, onFinish: @escaping (_ status: String, _ pointId: String?) -> Void) { self.cartId = cartId self.onFinish = onFinish super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() { super.viewDidLoad() let configuration = WKWebViewConfiguration() configuration.userContentController.add(self, name: "globePickup") webView = WKWebView(frame: view.bounds, configuration: configuration) webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] webView.navigationDelegate = self view.addSubview(webView) webView.load(URLRequest(url: selectionURL())) }
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) webView.configuration.userContentController.removeScriptMessageHandler(forName: "globePickup") }
private func selectionURL() -> URL { let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~")) func encode(_ value: String) -> String { value.addingPercentEncoding(withAllowedCharacters: allowed) ?? "" } let query = "cart=\(encode(cartId))&return_url=\(encode(returnURL))&locale=cs" return URL(string: "https://\(shopDomain)/apps/appfleece-globe/select?\(query)")! }
// Native bridge func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "globePickup", let body = message.body as? [String: Any], body["type"] as? String == "globe:pickup-point", let status = body["status"] as? String else { return } let point = body["point"] as? [String: Any] finish(status: status, pointId: point?["id"] as? String) }
// Redirect to return_url func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url, url.absoluteString.hasPrefix(returnURL) else { decisionHandler(.allow) return } decisionHandler(.cancel) let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] let status = items.first { $0.name == "status" }?.value ?? "cancelled" finish(status: status, pointId: items.first { $0.name == "point_id" }?.value) }
private func finish(status: String, pointId: String?) { guard !finished else { return } finished = true dismiss(animated: true) { self.onFinish(status, pointId) } }}In onFinish, query the cart again and continue to checkoutUrl.
Alternative: ASWebAuthenticationSession. For the redirect flow only, open the page in an authentication session with callbackURLScheme: "myapp". The completion handler receives the return_url with the result parameters, and nil when the buyer closes the sheet. iOS shows a system prompt before the sheet opens, and the page cannot reach native bridges there.
import AuthenticationServices
let session = ASWebAuthenticationSession(url: selectionURL, callbackURLScheme: "myapp") { callbackURL, _ in guard let callbackURL else { return } // closed by the buyer let items = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems ?? [] let status = items.first { $0.name == "status" }?.value // Query the cart again, then continue to checkoutUrl.}session.presentationContextProvider = selfsession.start()Android (Kotlin, WebView)
Section titled “Android (Kotlin, WebView)”The activity exposes GlobePickupAndroid, handles the location prompt and catches the myapp://pickup redirect.
import android.Manifestimport android.content.Intentimport android.content.pm.PackageManagerimport android.net.Uriimport android.os.Bundleimport android.webkit.*import androidx.activity.result.contract.ActivityResultContractsimport androidx.appcompat.app.AppCompatActivityimport androidx.core.content.ContextCompatimport org.json.JSONObject
class PickupPointActivity : AppCompatActivity() { private val shopDomain = "your-store.myshopify.com" private val returnUrl = "myapp://pickup" private lateinit var webView: WebView private var finished = false private var geoOrigin: String? = null private var geoCallback: GeolocationPermissions.Callback? = null
private val locationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> geoCallback?.invoke(geoOrigin, granted, false) geoCallback = null }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) webView = WebView(this) setContentView(webView)
webView.settings.javaScriptEnabled = true webView.settings.domStorageEnabled = true webView.settings.setGeolocationEnabled(true) webView.addJavascriptInterface(Bridge(), "GlobePickupAndroid")
webView.webChromeClient = object : WebChromeClient() { override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) { val granted = ContextCompat.checkSelfPermission( this@PickupPointActivity, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED if (granted) { callback.invoke(origin, true, false) } else { geoOrigin = origin geoCallback = callback locationPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) } } }
webView.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { val url = request.url if (!url.toString().startsWith(returnUrl)) return false finishWith(url.getQueryParameter("status") ?: "cancelled", url.getQueryParameter("point_id")) return true } }
val cartId = requireNotNull(intent.getStringExtra("cartId")) val url = Uri.parse("https://$shopDomain/apps/appfleece-globe/select").buildUpon() .appendQueryParameter("cart", cartId) .appendQueryParameter("return_url", returnUrl) .appendQueryParameter("locale", "cs") .build() webView.loadUrl(url.toString()) }
inner class Bridge { @JavascriptInterface fun postMessage(json: String) { val message = JSONObject(json) if (message.optString("type") != "globe:pickup-point") return val pointId = message.optJSONObject("point")?.optString("id") // JavascriptInterface methods run on a background thread. runOnUiThread { finishWith(message.optString("status"), pointId) } } }
private fun finishWith(status: String, pointId: String?) { if (finished) return finished = true val result = Intent().putExtra("status", status).putExtra("pointId", pointId) setResult(if (status == "selected") RESULT_OK else RESULT_CANCELED, result) finish() }}Uri.Builder.appendQueryParameter encodes the cart ID. If your return URL is an Android App Link (https://…), match it the same way in shouldOverrideUrlLoading. The WebView does not hand App Links to your app on its own.
React Native (react-native-webview)
Section titled “React Native (react-native-webview)”import React, { useRef } from 'react';import { WebView, type WebViewMessageEvent } from 'react-native-webview';
const SHOP_DOMAIN = 'your-store.myshopify.com';const RETURN_URL = 'myapp://pickup';
type Result = { status: 'selected' | 'cancelled'; pointId?: string };
function queryParam(url: string, name: string): string | undefined { const match = new RegExp(`[?&]${name}=([^&#]*)`).exec(url); return match ? decodeURIComponent(match[1]) : undefined;}
export function PickupPointScreen({ cartId, onDone }: { cartId: string; onDone: (result: Result) => void }) { const finished = useRef(false); const finish = (result: Result) => { if (finished.current) return; finished.current = true; onDone(result); };
const uri = `https://${SHOP_DOMAIN}/apps/appfleece-globe/select` + `?cart=${encodeURIComponent(cartId)}&return_url=${encodeURIComponent(RETURN_URL)}&locale=cs`;
return ( <WebView source={{ uri }} javaScriptEnabled domStorageEnabled geolocationEnabled // Without myapp://* here, the WebView opens the return URL itself. originWhitelist={['https://*', 'myapp://*']} onMessage={(event: WebViewMessageEvent) => { let message: any; try { message = JSON.parse(event.nativeEvent.data); } catch { return; } if (message?.type !== 'globe:pickup-point') return; finish({ status: message.status, pointId: message.point?.id }); }} onShouldStartLoadWithRequest={(request) => { if (!request.url.startsWith(RETURN_URL)) return true; const status = queryParam(request.url, 'status') === 'selected' ? 'selected' : 'cancelled'; finish({ status, pointId: queryParam(request.url, 'point_id') }); return false; }} /> );}geolocationEnabled applies to Android. Request ACCESS_FINE_LOCATION with PermissionsAndroid before you open the screen. On iOS, add NSLocationWhenInUseUsageDescription to Info.plist.
Hydrogen and Next.js (web)
Section titled “Hydrogen and Next.js (web)”Open the page from a click handler, so the browser does not block the popup. Add your storefront’s origin as a return URL in Globe, for example https://shop.example.com/cart, or the page will not post the message to it.
const SHOP_ORIGIN = 'https://your-store.myshopify.com';
type PickupMessage = { type: 'globe:pickup-point'; status: 'selected' | 'cancelled'; state: string | null; cartId: string; point: { id: string; carrier: string | null; carrierId: string | null; name: string; address: string } | null;};
export function choosePickupPoint(cartId: string): Promise<PickupMessage> { const state = crypto.randomUUID(); const url = new URL('/apps/appfleece-globe/select', SHOP_ORIGIN); url.searchParams.set('cart', cartId); url.searchParams.set('state', state);
const popup = window.open(url, 'globe-pickup', 'popup,width=480,height=760'); if (!popup) return Promise.reject(new Error('The popup was blocked'));
return new Promise((resolve) => { const closedCheck = window.setInterval(() => { if (popup.closed) done({ type: 'globe:pickup-point', status: 'cancelled', state, cartId, point: null }); }, 500);
function onMessage(event: MessageEvent) { if (event.origin !== SHOP_ORIGIN) return; const data = event.data as PickupMessage; if (data?.type !== 'globe:pickup-point' || data.state !== state) return; done(data); }
function done(message: PickupMessage) { window.clearInterval(closedCheck); window.removeEventListener('message', onMessage); if (!popup.closed) popup.close(); resolve(message); }
window.addEventListener('message', onMessage); });}event.origin is the domain you opened the page on. Check it exactly, then check type and your state.
After the promise resolves, load the cart again. In Hydrogen, revalidate the route that reads the cart:
import { useRevalidator } from 'react-router';
export function PickupButton({ cartId }: { cartId: string }) { const revalidator = useRevalidator(); return ( <button type="button" onClick={async () => { const result = await choosePickupPoint(cartId); if (result.status === 'selected') revalidator.revalidate(); }} > Choose a pickup point </button> );}Iframe
Section titled “Iframe”The page also works in an iframe and posts to window.parent with the same rules. Some stores send frame-ancestors 'none' or X-Frame-Options: DENY on their storefront domain, and then the browser refuses to show the page in a frame. If the frame stays empty, use the popup or the redirect.
Redirect
Section titled “Redirect”Send the buyer to the page with a return_url on your storefront, then read the result when they come back:
const url = new URL('/apps/appfleece-globe/select', SHOP_ORIGIN);url.searchParams.set('cart', cart.id);url.searchParams.set('return_url', 'https://shop.example.com/cart');window.location.assign(url);The buyer returns to https://shop.example.com/cart?status=selected&point_id=…. The cart route’s loader reads the cart again anyway, so the point is in its attributes.