Coverage for functions \ flipdare \ core \ app_deep_link_factory.py: 83%
81 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
2#
3# This file is part of Flipdare's proprietary software and contains
4# confidential and copyrighted material. Unauthorised copying,
5# modification, distribution, or use of this file is strictly
6# prohibited without prior written permission from Flipdare Pty Ltd.
7#
8# This software includes third-party components licensed under MIT,
9# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
10#
12from __future__ import annotations
14from enum import StrEnum
15import flask
16from flipdare.app_log import LOG
17from flipdare.constants import DEEP_LINK_BASE, DEFERRED_DEEP_LINK_URL, IS_TRACE
18from furl import furl
20from flipdare.generated.shared.app_deep_link import AppDeepLink
22__all__ = ["AppDeepLinkFactory", "AppPlatform"]
25class AppPlatform(StrEnum):
26 IOS = "ios"
27 ANDROID = "android"
28 WEB = "web"
30 @property
31 def is_mobile(self) -> bool:
32 return self in (AppPlatform.IOS, AppPlatform.ANDROID)
35class AppDeepLinkFactory:
36 __slots__ = ("_link_type", "_req")
38 def __init__(self, req: flask.Request, link: AppDeepLink | None = None) -> None:
39 self._req = req
40 self._link_type = self._parse_link(link)
42 @property
43 def req(self) -> flask.Request:
44 return self._req
46 @property
47 def link(self) -> AppDeepLink | None:
48 return self._link_type
50 def _parse_link(self, link: AppDeepLink | None = None) -> AppDeepLink | None:
51 # ALL we need to do is run basic safety checks and
52 # check we can create an AppBackendLink.
53 # since we are passing to Chotto, we dont need to rewrite..
54 url = self._req.url
55 platform = self.platform
56 # get the platform first, it will help us redirect to the right
57 # store if not a deep link..
58 if not platform.is_mobile:
59 if IS_TRACE:
60 msg = f"Request with URL {url} is from platform {platform}, not a mobile deep link"
61 LOG().trace(msg)
62 return None
64 if not url.startswith(DEEP_LINK_BASE):
65 if IS_TRACE:
66 LOG().trace(f"URL {url} does not start with deep link base {DEEP_LINK_BASE}")
68 return None
70 if link is not None:
71 return link
73 # path should be in the format /l/{class}/{code} for resources
74 # or /l/{class}?ref={code} for actions
75 f = furl(url)
76 segments = f.path.segments
77 link_class: str | None = None
79 if len(segments) == 2: # noqa: PLR2004
80 # this must be a action
81 link_class = segments[1]
82 elif len(segments) > 2: # noqa: PLR2004
83 # this must be a resource
84 link_class = segments[1]
85 else:
86 if IS_TRACE:
87 LOG().trace(f"URL {url} has unexpected number of path segments for deep link")
88 return None
90 try:
91 return AppDeepLink(link_class.lower())
92 except ValueError:
93 if IS_TRACE:
94 LOG().trace(f"URL {url} contains invalid link class {link_class}")
95 return None
97 @property
98 def is_valid_link(self) -> bool:
99 return self._link_type is not None
101 @property
102 def app_url(self) -> str | None:
103 link = self._link_type
104 if link is None:
105 return None
107 return f"{DEFERRED_DEEP_LINK_URL}?deep_link={self._req.url}"
109 @property
110 def is_ios(self) -> bool:
111 return self.platform == AppPlatform.IOS
113 @property
114 def is_android(self) -> bool:
115 return self.platform == AppPlatform.ANDROID
117 @property
118 def platform(self) -> AppPlatform:
119 req = self._req
120 user_agent = req.headers.get("User-Agent", "")
122 ua = user_agent.lower()
123 if "iphone" in ua or "ipad" in ua or "ipod" in ua:
124 return AppPlatform.IOS
125 if "android" in ua:
126 return AppPlatform.ANDROID
127 return AppPlatform.WEB