diff --git a/docs/reference/shopify/javascript/create-intent-schema.json b/docs/reference/shopify/javascript/create-intent-schema.json new file mode 100644 index 0000000..5a9e012 --- /dev/null +++ b/docs/reference/shopify/javascript/create-intent-schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify-intent.json", + "inputSchema": { + "type": "object", + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount.json", + "additionalProperties": true, + "properties": { + "functionId": { + "matchValue": "YOUR_FUNCTION_ID" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "id": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json" + } + } + } +} diff --git a/docs/reference/shopify/javascript/edit-intent-schema.json b/docs/reference/shopify/javascript/edit-intent-schema.json new file mode 100644 index 0000000..6714d2f --- /dev/null +++ b/docs/reference/shopify/javascript/edit-intent-schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify-intent.json", + "value": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json", + "mapTo": "param", + "fieldName": "id" + }, + "inputSchema": { + "type": "object", + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount.json", + "additionalProperties": true, + "properties": { + "functionId": { + "matchValue": "YOUR_FUNCTION_ID" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "id": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json" + } + } + } +} diff --git a/docs/reference/shopify/javascript/instructions.md b/docs/reference/shopify/javascript/instructions.md new file mode 100644 index 0000000..5a42c43 --- /dev/null +++ b/docs/reference/shopify/javascript/instructions.md @@ -0,0 +1,5 @@ +Use this app when the merchant wants to create or edit a metafield-configured discount powered by this app's Shopify Function. + +When invoking `create:shopify/Discount`, pass `functionId: "YOUR_FUNCTION_ID"` so Shopify resolves directly to this app's create flow. + +When invoking `edit:shopify/Discount`, pass the discount's GID (default to the canonical `gid://shopify/DiscountNode/{id}`) and `functionId: "YOUR_FUNCTION_ID"` so Shopify resolves the edit directly to this app. diff --git a/docs/reference/shopify/javascript/shopify.extension.toml b/docs/reference/shopify/javascript/shopify.extension.toml index e94d045..5e39e61 100644 --- a/docs/reference/shopify/javascript/shopify.extension.toml +++ b/docs/reference/shopify/javascript/shopify.extension.toml @@ -38,3 +38,42 @@ handle = "ui-multiclass-metafield-js" create = "/app/discount/:functionId/new" details = "/app/discount/:functionId/:id" # [END discount-function.ui-paths] + +# [START discount-function.app-intent-link] +# Register app intent links so Sidekick can create and edit discounts backed by +# this app's Function. Each admin.app.intent.link target maps a shopify/Discount +# action (create or edit) to a page in your app. +[[extensions]] +name = "Create discount" +description = "Create a metafield-configured discount powered by this app's Shopify Function." +handle = "discount-app-intent-link-create" +type = "admin_link" + + [[extensions.targeting]] + target = "admin.app.intent.link" + # Hardcode your deployed Function's ID (see "Set your Function ID" below). + url = "/app/discount/YOUR_FUNCTION_ID/new" + instructions = "./instructions.md" + + [[extensions.targeting.intents]] + type = "shopify/Discount" + action = "create" + schema = "./create-intent-schema.json" + +[[extensions]] +name = "Edit discount" +description = "Edit a metafield-configured discount powered by this app's Shopify Function." +handle = "discount-app-intent-link-edit" +type = "admin_link" + + [[extensions.targeting]] + target = "admin.app.intent.link" + # {id} receives the discount ID; the Function ID stays hardcoded. + url = "/app/discount/YOUR_FUNCTION_ID/{id}" + instructions = "./instructions.md" + + [[extensions.targeting.intents]] + type = "shopify/Discount" + action = "edit" + schema = "./edit-intent-schema.json" +# [END discount-function.app-intent-link] diff --git a/docs/reference/shopify/rust/create-intent-schema.json b/docs/reference/shopify/rust/create-intent-schema.json new file mode 100644 index 0000000..5a9e012 --- /dev/null +++ b/docs/reference/shopify/rust/create-intent-schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify-intent.json", + "inputSchema": { + "type": "object", + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount.json", + "additionalProperties": true, + "properties": { + "functionId": { + "matchValue": "YOUR_FUNCTION_ID" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "id": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json" + } + } + } +} diff --git a/docs/reference/shopify/rust/edit-intent-schema.json b/docs/reference/shopify/rust/edit-intent-schema.json new file mode 100644 index 0000000..6714d2f --- /dev/null +++ b/docs/reference/shopify/rust/edit-intent-schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify-intent.json", + "value": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json", + "mapTo": "param", + "fieldName": "id" + }, + "inputSchema": { + "type": "object", + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount.json", + "additionalProperties": true, + "properties": { + "functionId": { + "matchValue": "YOUR_FUNCTION_ID" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "id": { + "$ref": "https://extensions.shopifycdn.com/shopifycloud/schemas/v1/shopify/discount/gid.json" + } + } + } +} diff --git a/docs/reference/shopify/rust/instructions.md b/docs/reference/shopify/rust/instructions.md new file mode 100644 index 0000000..5a42c43 --- /dev/null +++ b/docs/reference/shopify/rust/instructions.md @@ -0,0 +1,5 @@ +Use this app when the merchant wants to create or edit a metafield-configured discount powered by this app's Shopify Function. + +When invoking `create:shopify/Discount`, pass `functionId: "YOUR_FUNCTION_ID"` so Shopify resolves directly to this app's create flow. + +When invoking `edit:shopify/Discount`, pass the discount's GID (default to the canonical `gid://shopify/DiscountNode/{id}`) and `functionId: "YOUR_FUNCTION_ID"` so Shopify resolves the edit directly to this app. diff --git a/docs/reference/shopify/rust/shopify.extension.toml b/docs/reference/shopify/rust/shopify.extension.toml index 306442e..babdbc5 100644 --- a/docs/reference/shopify/rust/shopify.extension.toml +++ b/docs/reference/shopify/rust/shopify.extension.toml @@ -33,3 +33,42 @@ key = "function-configuration" create = "/app/discount/:functionId/new" details = "/app/discount/:functionId/:id" # [END discount-function.ui-paths] + +# [START discount-function.app-intent-link] +# Register app intent links so Sidekick can create and edit discounts backed by +# this app's Function. Each admin.app.intent.link target maps a shopify/Discount +# action (create or edit) to a page in your app. +[[extensions]] +name = "Create discount" +description = "Create a metafield-configured discount powered by this app's Shopify Function." +handle = "discount-app-intent-link-create" +type = "admin_link" + + [[extensions.targeting]] + target = "admin.app.intent.link" + # Hardcode your deployed Function's ID (see "Set your Function ID" below). + url = "/app/discount/YOUR_FUNCTION_ID/new" + instructions = "./instructions.md" + + [[extensions.targeting.intents]] + type = "shopify/Discount" + action = "create" + schema = "./create-intent-schema.json" + +[[extensions]] +name = "Edit discount" +description = "Edit a metafield-configured discount powered by this app's Shopify Function." +handle = "discount-app-intent-link-edit" +type = "admin_link" + + [[extensions.targeting]] + target = "admin.app.intent.link" + # {id} receives the discount ID; the Function ID stays hardcoded. + url = "/app/discount/YOUR_FUNCTION_ID/{id}" + instructions = "./instructions.md" + + [[extensions.targeting.intents]] + type = "shopify/Discount" + action = "edit" + schema = "./edit-intent-schema.json" +# [END discount-function.app-intent-link] diff --git a/examples/react-router-app/app/graphql/discounts.ts b/examples/react-router-app/app/graphql/discounts.ts index 0aa6f8f..afcbce8 100644 --- a/examples/react-router-app/app/graphql/discounts.ts +++ b/examples/react-router-app/app/graphql/discounts.ts @@ -50,6 +50,9 @@ export const GET_DISCOUNT = ` export const UPDATE_CODE_DISCOUNT = ` mutation UpdateCodeDiscount($id: ID!, $discount: DiscountCodeAppInput!) { discountUpdate: discountCodeAppUpdate(id: $id, codeAppDiscount: $discount) { + codeAppDiscount { + discountId + } userErrors { code message @@ -68,6 +71,9 @@ export const UPDATE_AUTOMATIC_DISCOUNT = ` id: $id automaticAppDiscount: $discount ) { + automaticAppDiscount { + discountId + } userErrors { code message diff --git a/examples/react-router-app/app/models/discounts.server.ts b/examples/react-router-app/app/models/discounts.server.ts index 6577e07..ddc591b 100644 --- a/examples/react-router-app/app/models/discounts.server.ts +++ b/examples/react-router-app/app/models/discounts.server.ts @@ -73,7 +73,7 @@ export async function createCodeDiscount( return { errors: responseJson.data.discountCreate?.userErrors as UserError[], - discount: responseJson.data.discountCreate?.codeAppDiscount, + discountId: responseJson.data.discountCreate?.codeAppDiscount?.discountId, }; } @@ -108,6 +108,8 @@ export async function createAutomaticDiscount( return { errors: responseJson.data.discountCreate?.userErrors as UserError[], + discountId: + responseJson.data.discountCreate?.automaticAppDiscount?.discountId, }; } @@ -161,6 +163,7 @@ export async function updateCodeDiscount( const responseJson = await response.json(); return { errors: responseJson.data.discountUpdate?.userErrors as UserError[], + discountId: responseJson.data.discountUpdate?.codeAppDiscount?.discountId, }; } @@ -207,6 +210,8 @@ export async function updateAutomaticDiscount( const responseJson = await response.json(); return { errors: responseJson.data.discountUpdate?.userErrors as UserError[], + discountId: + responseJson.data.discountUpdate?.automaticAppDiscount?.discountId, }; } diff --git a/examples/react-router-app/app/routes/app.discount.$functionId.$id.tsx b/examples/react-router-app/app/routes/app.discount.$functionId.$id.tsx index 248a94a..e0b6d1a 100644 --- a/examples/react-router-app/app/routes/app.discount.$functionId.$id.tsx +++ b/examples/react-router-app/app/routes/app.discount.$functionId.$id.tsx @@ -1,4 +1,5 @@ import { Collection, DiscountClass } from "app/types/admin.types"; +import { useEffect, useRef } from "react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, @@ -16,6 +17,10 @@ import { updateCodeDiscount, } from "../models/discounts.server"; import { DiscountMethod } from "../types/types"; +import { + completeDiscountWorkflow, + returnToDiscounts, +} from "../utils/navigation"; interface ActionData { errors?: { @@ -24,6 +29,7 @@ interface ActionData { field?: string[]; }[]; success?: boolean; + discountId?: string; } interface LoaderData { @@ -116,7 +122,12 @@ export const action = async ({ params, request }: ActionFunctionArgs) => { if (result.errors?.length > 0) { return { errors: result.errors }; } - return { success: true }; + if (!result.discountId) { + return { + errors: [{ message: "Unable to save discount.", field: [] }], + }; + } + return { success: true, discountId: result.discountId }; }; export const loader = async ({ params, request }: LoaderFunctionArgs) => { @@ -148,6 +159,17 @@ export default function VolumeEdit() { ...error, field: error.field || [], })) || []; + const completedDiscountId = useRef(null); + + // [START build-the-ui.resolve-edit-intent] + useEffect(() => { + const discountId = actionData?.discountId; + if (!discountId || completedDiscountId.current === discountId) return; + + completedDiscountId.current = discountId; + void completeDiscountWorkflow(discountId); + }, [actionData?.discountId]); + // [END build-the-ui.resolve-edit-intent] if (!rawDiscount) { return ; @@ -179,14 +201,14 @@ export default function VolumeEdit() { return ( - Discounts - - + { // Initially load with empty collections since none are selected yet @@ -76,7 +80,12 @@ export const action = async ({ params, request }: ActionFunctionArgs) => { if (result.errors?.length > 0) { return { errors: result.errors }; } - return { success: true }; + if (!result.discountId) { + return { + errors: [{ message: "Unable to save discount.", field: [] }], + }; + } + return { success: true, discountId: result.discountId }; }; // [END build-the-ui.add-action] @@ -87,6 +96,7 @@ interface ActionData { field: string[]; }[]; success?: boolean; + discountId?: string; } interface LoaderData { @@ -99,10 +109,17 @@ export default function VolumeNew() { const navigation = useNavigation(); const isLoading = navigation.state === "submitting"; const submitErrors = actionData?.errors || []; + const completedDiscountId = useRef(null); - if (actionData?.success) { - returnToDiscounts(); - } + // [START build-the-ui.resolve-create-intent] + useEffect(() => { + const discountId = actionData?.discountId; + if (!discountId || completedDiscountId.current === discountId) return; + + completedDiscountId.current = discountId; + void completeDiscountWorkflow(discountId); + }, [actionData?.discountId]); + // [END build-the-ui.resolve-create-intent] const initialData = { title: "", @@ -127,14 +144,15 @@ export default function VolumeNew() { }; return ( - - Discounts - - } - > + +