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

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# 

11 

12from __future__ import annotations 

13 

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 

19 

20from flipdare.generated.shared.app_deep_link import AppDeepLink 

21 

22__all__ = ["AppDeepLinkFactory", "AppPlatform"] 

23 

24 

25class AppPlatform(StrEnum): 

26 IOS = "ios" 

27 ANDROID = "android" 

28 WEB = "web" 

29 

30 @property 

31 def is_mobile(self) -> bool: 

32 return self in (AppPlatform.IOS, AppPlatform.ANDROID) 

33 

34 

35class AppDeepLinkFactory: 

36 __slots__ = ("_link_type", "_req") 

37 

38 def __init__(self, req: flask.Request, link: AppDeepLink | None = None) -> None: 

39 self._req = req 

40 self._link_type = self._parse_link(link) 

41 

42 @property 

43 def req(self) -> flask.Request: 

44 return self._req 

45 

46 @property 

47 def link(self) -> AppDeepLink | None: 

48 return self._link_type 

49 

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 

63 

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}") 

67 

68 return None 

69 

70 if link is not None: 

71 return link 

72 

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 

78 

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 

89 

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 

96 

97 @property 

98 def is_valid_link(self) -> bool: 

99 return self._link_type is not None 

100 

101 @property 

102 def app_url(self) -> str | None: 

103 link = self._link_type 

104 if link is None: 

105 return None 

106 

107 return f"{DEFERRED_DEEP_LINK_URL}?deep_link={self._req.url}" 

108 

109 @property 

110 def is_ios(self) -> bool: 

111 return self.platform == AppPlatform.IOS 

112 

113 @property 

114 def is_android(self) -> bool: 

115 return self.platform == AppPlatform.ANDROID 

116 

117 @property 

118 def platform(self) -> AppPlatform: 

119 req = self._req 

120 user_agent = req.headers.get("User-Agent", "") 

121 

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