# django-shopify-app

Add the app in settings.py

```plain

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'shopify_app',
        'shops',
    ]

```

Add the required configurations in settings.py

```plain

    SHOPIFY_API_KEY = config('SHOPIFY_API_KEY')
    SHOPIFY_API_SECRET = config('SHOPIFY_API_SECRET')

    SHOPIFY_APP_SCOPES = [
        'read_products',
        'read_orders',
    ]
    SHOPIFY_WEBHOOK_TOPICS = [
        'products/update',
        'app/uninstalled',
    ]

    SHOPIFY_SHOP_MODEL = 'shops.Shop'

    SHOPIFY_WEBHOOK_HOST = 'https://moship.ngrok.io'
    SHOPIFY_APP_HOST = 'https://moship.ngrok.io'

    SHOPIFY_WEBHOOK_CALLBACK = 'shops.webhooks.webhook_entry'
    SHOPIFY_GDPR_WEBHOOK_CALLBACK = 'shops.webhooks.webhook_entry'

```

## Authorization

The package supports two authorization flows: **token exchange** (recommended for embedded apps) and **authorization code grant** (legacy / non-embedded apps).

### Token exchange (recommended)

Token exchange eliminates OAuth redirects. The backend exchanges the session token from App Bridge for an access token via a server-side POST to Shopify. No page reloads or flicker.

Scopes are managed via `shopify.app.toml` and deployed with Shopify CLI (`shopify app deploy`). Shopify handles installation and scope updates automatically.

Add to settings.py:

```python
SHOPIFY_TOKEN_EXCHANGE = True           # Enable token exchange
SHOPIFY_DASHBOARD_PATH = '/dashboard'   # Where to redirect from app root
```

Set up your URLs:

```python
from django.urls import path, include
from shopify_app.views import AppRootView

urlpatterns = [
    path('', AppRootView.as_view()),
    path('shopify/', include('shopify_app.urls')),
    # your dashboard urls...
]
```

When a merchant opens your app, `AppRootView` redirects to the dashboard. The first API request from the dashboard triggers token exchange automatically via `ShopSessionMixin` / `shop_session`, storing the access token for subsequent requests.

### Authorization code grant (legacy)

For non-embedded apps or apps that don't use Shopify managed installation.

```python
from django.urls import path
from shopify_app.views import AppRootView, EndTokenRequestView

app_name = 'my_shopify_app'

urlpatterns = [
    path(
        '',
        AppRootView.as_view(
            redirect_path_name='my_shopify_app:end-token-request',
        ),
    ),
    path(
        'confirm/',
        EndTokenRequestView.as_view(
            redirect_path_name='embed_admin:dashboard',
        ),
        name='end-token-request'
    ),
]
```

With `SHOPIFY_TOKEN_EXCHANGE = False` (default), `AppRootView` falls back to the OAuth authorization code grant flow.

### Webhook URLs

```python
from django.urls import path, include

urlpatterns = [
    path('shopify/', include('shopify_app.urls')),
]
```

## ShopSessionMixin

A mixin that authenticates requests against a valid Shopify shop session (JWT). Use it with any `APIView` or DRF generic view:

```python
from rest_framework.views import APIView
from shopify_app.mixins import ShopSessionMixin

class MyView(ShopSessionMixin, APIView):
    def get(self, request, *args, **kwargs):
        shop = request.shop
        ...
```

### Staff bypass

Staff users can skip Shopify JWT validation if they have a shop associated with their user model. Enable it globally in settings:

```python
SHOPIFY_STAFF_BYPASS = True  # Default: False
SHOPIFY_STAFF_SHOP_ATTR = 'admin_shop'  # Default: 'admin_shop'
```

Or per-view:

```python
class MyView(ShopSessionMixin, APIView):
    allow_staff_bypass = True  # Overrides the global setting
```

When enabled, if the request user is authenticated, is staff, and has a truthy value on the configured attribute (`admin_shop` by default), the mixin sets `request.shop` from that attribute and skips JWT validation.
