Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ecommerce_integrations/patches.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ecommerce_integrations.patches.update_shopify_custom_fields
ecommerce_integrations.patches.set_default_amazon_item_fields_map
ecommerce_integrations.patches.set_default_shopify_auth_method
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import frappe

from ecommerce_integrations.shopify.constants import SETTING_DOCTYPE


def execute():
"""
Migration patch to set default authentication method for existing Shopify installations.
This ensures backward compatibility when introducing OAuth 2.0 support.
"""
frappe.reload_doc("shopify", "doctype", "shopify_setting")

if frappe.db.exists("DocType", SETTING_DOCTYPE):
settings = frappe.get_doc(SETTING_DOCTYPE)

# Set default authentication method to "Static Token" for existing installations
if not settings.authentication_method:
settings.db_set("authentication_method", "Static Token", update_modified=False)
frappe.db.commit()

frappe.logger().info(
"Shopify Setting: Set default authentication method to 'Static Token' for existing installation"
)
58 changes: 55 additions & 3 deletions ecommerce_integrations/shopify/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@


def temp_shopify_session(func):
"""Any function that needs to access shopify api needs this decorator. The decorator starts a temp session that's destroyed when function returns."""
"""Any function that needs to access shopify api needs this decorator.
The decorator starts a temp session that's destroyed when function returns.

Supports both Static Token and OAuth 2.0 Client Credentials authentication methods.
For OAuth, automatically refreshes token if expired or expiring soon.
"""

@functools.wraps(func)
def wrapper(*args, **kwargs):
Expand All @@ -29,14 +34,50 @@ def wrapper(*args, **kwargs):

setting = frappe.get_doc(SETTING_DOCTYPE)
if setting.is_enabled():
auth_details = (setting.shopify_url, API_VERSION, setting.get_password("password"))
# Get access token based on authentication method
access_token = _get_access_token(setting)
auth_details = (setting.shopify_url, API_VERSION, access_token)

with Session.temp(*auth_details):
return func(*args, **kwargs)

return wrapper


def _get_access_token(setting):
"""
Get the appropriate access token based on authentication method.
For OAuth, ensures token is valid and refreshes if needed.

Args:
setting: ShopifySetting document instance

Returns:
Valid access token
"""
if setting.authentication_method == "OAuth 2.0 Client Credentials":
# Import here to avoid circular dependency
from ecommerce_integrations.shopify.oauth import get_valid_access_token

try:
return get_valid_access_token(setting)
except Exception as e:
# Log the error and re-raise with context
create_shopify_log(
status="Error",
method="ecommerce_integrations.shopify.connection._get_access_token",
message=_("Failed to get valid OAuth access token"),
exception=str(e),
)
frappe.throw(
_("Failed to authenticate with Shopify using OAuth 2.0: {0}").format(str(e)),
title=_("Authentication Error"),
)
else:
# Static Token authentication
return setting.get_password("password")


def register_webhooks(shopify_url: str, password: str) -> list[Webhook]:
"""Register required webhooks with shopify and return registered webhooks."""
new_webhooks = []
Expand Down Expand Up @@ -120,7 +161,18 @@ def process_request(data, event):

def _validate_request(req, hmac_header):
settings = frappe.get_doc(SETTING_DOCTYPE)
secret_key = settings.shared_secret

# Get the appropriate secret key based on authentication method
if settings.authentication_method == "OAuth 2.0 Client Credentials":
# For OAuth apps, use client_secret for HMAC validation
secret_key = settings.get_password("client_secret")
else:
# For static token apps, use shared_secret
secret_key = settings.shared_secret

if not secret_key:
create_shopify_log(status="Error", request_data=req.data, exception="Secret key not configured")
frappe.throw(_("Webhook validation failed: Secret key not configured"))

sig = base64.b64encode(hmac.new(secret_key.encode("utf8"), req.data, hashlib.sha256).digest())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
"enable_shopify",
"column_break_4",
"section_break_2",
"authentication_method",
"shopify_url",
"column_break_3",
"password",
"shared_secret",
"client_id",
"client_secret",
"oauth_access_token",
"token_expires_at",
"section_break_4",
"webhooks",
"customer_settings_section",
Expand Down Expand Up @@ -76,6 +81,14 @@
"fieldtype": "Section Break",
"label": "Authentication Details"
},
{
"default": "Static Token",
"description": "Select Static Token for existing apps or OAuth 2.0 for apps created after Jan 1, 2026",
"fieldname": "authentication_method",
"fieldtype": "Select",
"label": "Authentication Method",
"options": "Static Token\nOAuth 2.0 Client Credentials"
},
{
"description": "eg: frappe.myshopify.com",
"fieldname": "shopify_url",
Expand All @@ -89,16 +102,50 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.authentication_method=='Static Token'",
"fieldname": "password",
"fieldtype": "Password",
"label": "Password / Access Token",
"mandatory_depends_on": "eval:doc.enable_shopify"
"mandatory_depends_on": "eval:doc.enable_shopify && doc.authentication_method=='Static Token'"
},
{
"depends_on": "eval:doc.authentication_method=='Static Token'",
"fieldname": "shared_secret",
"fieldtype": "Data",
"label": "Shared secret / API Secret",
"mandatory_depends_on": "eval:doc.enable_shopify"
"mandatory_depends_on": "eval:doc.enable_shopify && doc.authentication_method=='Static Token'"
},
{
"depends_on": "eval:doc.authentication_method=='OAuth 2.0 Client Credentials'",
"description": "Client ID from Shopify Partner Dashboard",
"fieldname": "client_id",
"fieldtype": "Data",
"label": "Client ID",
"mandatory_depends_on": "eval:doc.enable_shopify && doc.authentication_method=='OAuth 2.0 Client Credentials'"
},
{
"depends_on": "eval:doc.authentication_method=='OAuth 2.0 Client Credentials'",
"description": "Client Secret from Shopify Partner Dashboard",
"fieldname": "client_secret",
"fieldtype": "Password",
"label": "Client Secret",
"mandatory_depends_on": "eval:doc.enable_shopify && doc.authentication_method=='OAuth 2.0 Client Credentials'"
},
{
"description": "Auto-generated OAuth access token",
"fieldname": "oauth_access_token",
"fieldtype": "Password",
"hidden": 1,
"label": "OAuth Access Token",
"read_only": 1
},
{
"description": "OAuth token expiry time",
"fieldname": "token_expires_at",
"fieldtype": "Datetime",
"hidden": 1,
"label": "Token Expires At",
"read_only": 1
},
{
"collapsible": 1,
Expand Down Expand Up @@ -392,7 +439,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-10-24 10:38:49.247431",
"modified": "2026-01-13 14:36:33.238183",
"modified_by": "Administrator",
"module": "shopify",
"name": "Shopify Setting",
Expand All @@ -409,8 +456,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Loading