# Shopify Install Flow API

This document describes the backend contract for the non-embedded Shopify install flow used by the admin dashboard.

## Purpose

The backend supports the real Shopify two-step install flow:

1. Shopify opens the app URL with signed install params: `shop`, `hmac`, `host`, `timestamp`
2. The dashboard calls `startShopifyInstall`
3. The backend validates the install request and returns a Shopify OAuth authorize URL
4. The dashboard redirects the merchant to Shopify's authorize screen
5. Shopify redirects back to the dashboard with `code`, `state`, `hmac`, `shop`, `host`, `timestamp`
6. The dashboard calls `bindShopifyInstall`
7. The backend exchanges the OAuth code for an admin token, creates a storefront token, stores the final v2 config, and sets up webhooks

`bindShopifyInstall` is therefore the completion step, not the first install step.

## Preconditions

Before the flow starts, the target store must already exist in SuperPay and be configured with:

- `shop_integration = shopify`
- stored Shopify `clientId`
- stored Shopify `clientSecret`

The frontend never sends `clientId` or `clientSecret` in either mutation.

## Authentication and authorization

Both mutations are protected by:

- `JwtAuthGuard`
- `StoreAccessGuard`

The caller must be logged in and have access to the target `storeDomain`.

## Mutation 1: startShopifyInstall

Use this mutation when the dashboard lands on the first Shopify install URL that only contains signed install params.

### Input

```graphql
input StartShopifyInstallInput {
  storeDomain: String!
  shop: String!
  hmac: String!
  host: String!
  timestamp: String!
  redirectHostname: String
}
```

### Result

```graphql
type StartShopifyInstallResult {
  success: Boolean!
  message: String!
  authorizeUrl: String
  state: String
  storeDomain: String
  shop: String
}
```

### Behavior

The backend will:

- validate `shop`
- validate timestamp freshness
- validate HMAC using the stored `clientSecret`
- generate an OAuth `state`
- store that `state` server-side in cache for 5 minutes
- use `redirectHostname` for the Shopify `redirect_uri` when provided
- return a Shopify OAuth authorize URL

### Frontend action

After a successful response, the dashboard should redirect the browser to `authorizeUrl`.

### Example mutation

```graphql
mutation StartShopifyInstall($input: StartShopifyInstallInput!) {
  startShopifyInstall(input: $input) {
    success
    message
    authorizeUrl
    state
    storeDomain
    shop
  }
}
```

### Example variables

```json
{
  "input": {
    "storeDomain": "my-superpay-store.com",
    "shop": "superpay-dev.myshopify.com",
    "hmac": "17044cb00c65f371be2ac291ad483e046a5ebde0eceb2798abd2bd2aa5d7d1be",
    "host": "YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvc3VwZXJwYXktZGV2",
    "timestamp": "1773769747",
    "redirectHostname": "sp-dashboard.eu.ngrok.io"
  }
}
```

## Mutation 2: bindShopifyInstall

Use this mutation after Shopify redirects back from the authorize screen with an OAuth `code`.

### Input

```graphql
input BindShopifyInstallInput {
  storeDomain: String!
  shop: String!
  hmac: String!
  host: String!
  timestamp: String!
  code: String!
  state: String
}
```

### Result

```graphql
type BindShopifyInstallResult {
  success: Boolean!
  message: String!
  storeDomain: String
  shop: String
  webhooksConfigured: Boolean
}
```

### Behavior

The backend will:

- validate `shop`
- validate timestamp freshness
- validate callback HMAC using all signed callback params
- validate the returned `state` against the cached state from `startShopifyInstall`
- exchange `code` for an offline admin token
- create a storefront token
- upgrade or persist Shopify v2 config
- attempt webhook setup

The cached install `state` is invalidated after a valid completion attempt.

### Example mutation

```graphql
mutation BindShopifyInstall($input: BindShopifyInstallInput!) {
  bindShopifyInstall(input: $input) {
    success
    message
    storeDomain
    shop
    webhooksConfigured
  }
}
```

### Example variables

```json
{
  "input": {
    "storeDomain": "my-superpay-store.com",
    "shop": "superpay-dev.myshopify.com",
    "code": "shopify-oauth-code",
    "state": "oauth-state-from-startShopifyInstall",
    "hmac": "ac3a092522f83737bc32efd28bf683c2d855ab7dc55339d90678fbd5d2168569",
    "host": "YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvc3VwZXJwYXktZGV2",
    "timestamp": "1773769780"
  }
}
```

## Validation rules

### Shared validation

Both mutations validate:

- valid Shopify shop hostname
- fresh timestamp within a 5 minute window
- valid store config for `storeDomain`
- stored Shopify `clientId` and `clientSecret`

### Start-step validation

`startShopifyInstall` validates the initial Shopify signature over the params Shopify sent in the first install hit.

When `redirectHostname` is provided, the backend uses it to build `redirect_uri` as `https://<redirectHostname>/shopify/install`. The field may also be sent as a full origin such as `https://sp-dashboard.eu.ngrok.io`.

### Completion-step validation

`bindShopifyInstall` validates:

- callback HMAC using `code`, `state`, `shop`, `host`, `timestamp`
- presence of callback `state`
- match between callback `state` and cached server-side `state`

## Successful responses

`startShopifyInstall` success means:

- the initial Shopify link was authentic
- the backend produced a valid authorize URL
- the frontend should redirect immediately

`bindShopifyInstall` success means:

- the OAuth callback was validated
- the store now has a persisted Shopify v2 config

Possible completion messages:

- `Shopify app installed and webhooks configured successfully`
- `Shopify app installed successfully, but webhook setup failed. Use Test Connection to retry.`

## Error scenarios

The frontend should expect GraphQL errors for these cases.

### Shared config/auth problems

- `Unauthorized`
- `Store config not found for domain: '<storeDomain>'`
- `Store '<storeDomain>' is not configured as a Shopify integration`
- `Store '<storeDomain>' is missing Shopify app credentials (clientSecret). Configure it in the store settings before installing.`
- `Store '<storeDomain>' is missing Shopify app credentials (clientId). Configure it in the store settings before installing.`

### Invalid install request

- `Invalid Shopify shop format: '<shop>'`
- `Shopify install request has expired`
- `Invalid Shopify install signature`

### Invalid callback request

- `Missing Shopify OAuth code. The frontend must forward the code parameter from the Shopify install callback URL.`
- `Missing Shopify OAuth state. Start the install flow again from the Shopify install link.`
- `Invalid Shopify OAuth state. Start the install flow again from the Shopify install link.`

### Upstream Shopify failures

The backend may also fail when:

- OAuth code exchange is rejected by Shopify
- storefront token creation fails
- webhook setup partially fails

Webhook setup failure is non-fatal for the install itself.

## Backend state changes on successful completion

On successful `bindShopifyInstall`, the backend stores a Shopify v2 config containing:

- `version = v2`
- `shop`
- `clientId`
- `clientSecret`
- `adminAccessToken`
- `storefrontAccessToken`
- `handleOrderTransactions`
- `customerEmailWhitelist`
- `installedAt`

If the store started on legacy v1 config, operational settings are preserved during the upgrade.

## Recommended frontend sequence

1. Read `shop`, `hmac`, `host`, and `timestamp` from the first Shopify install URL.
2. Call `startShopifyInstall` with those values plus the selected `storeDomain`.
3. Redirect the browser to the returned `authorizeUrl`.
4. After Shopify redirects back, read `code`, `state`, `shop`, `hmac`, `host`, and `timestamp` from the callback URL.
5. Call `bindShopifyInstall` immediately.
6. Show install success or failure based on the response.

## Relevant backend files

- [src/models/stores/dto/bind-shopify-install.dto.ts](/home/cra/projects/sp-admin-backend/src/models/stores/dto/bind-shopify-install.dto.ts)
- [src/models/stores/stores.resolver.ts](/home/cra/projects/sp-admin-backend/src/models/stores/stores.resolver.ts)
- [src/models/stores/utils/shopify-hmac.ts](/home/cra/projects/sp-admin-backend/src/models/stores/utils/shopify-hmac.ts)
- [src/models/stores/utils/shopify-token-exchange.ts](/home/cra/projects/sp-admin-backend/src/models/stores/utils/shopify-token-exchange.ts)