@@ -3,7 +3,8 @@ import Foundation
33/// Which of an alert's buttons an `accept` or `dismiss` activates, decided on the button labels
44/// alone, so the rule is checkable without a screen.
55///
6- /// `dismiss` activates only a label `isDismissButton` recognizes. An unknown decline cannot be
6+ /// Earlier label preferences win; equal-rank buttons refuse rather than use their order.
7+ /// `dismiss` activates only a recognized decline label. An unknown decline cannot be
78/// identified from labels or button position: iOS localizes prompts unevenly, so one prompt can
89/// pair a localized decline with a still-English affirmative, and a positional guess would tap
910/// that affirmative and record an authorization (#3627). An unrecognized decline is therefore
@@ -14,49 +15,33 @@ import Foundation
1415/// `dismiss` take the last actionable element of that window. A consent prompt can never carry
1516/// that marker.
1617enum RunnerAlertButtonChoice {
18+ private static let acceptPreferences = [
19+ " ^allow while using (?:the )?app$ " , " ^while using the app$ " , " ^allow all$ " ,
20+ " ^always allow$ " , " ^allow all the time$ " , " ^allow notifications$ " , " ^allow$ " ,
21+ " ^ok$ " , " ^yes$ " , " ^continue$ " , " ^save$ " , " ^confirm " , " ^turn on$ " ,
22+ " ^open settings$ " , " ^done$ " , " ^open$ " , " ^allow once$ " , " ^only this time$ "
23+ ]
24+ private static let dismissPreferences = [
25+ " ^cancel$ " , " ^deny$ " , " ^don.?t allow$ " , " ^not now$ " , " ^no$ " , " ^dismiss$ " ,
26+ " ^close$ " , " ^close app$ " , " ^later$ " , " ^skip$ " , " ^ask app not to track$ " , " ^keep browsing$ "
27+ ]
28+
1729 static func buttonIndex ( for labels : [ String ] , action : String , popupMarkerSurface : Bool = false ) -> Int ? {
18- if action == " accept " {
19- if let accept = labels . firstIndex ( where : { isAcceptButton ( $0 ) } ) {
20- return accept
21- }
22- // A lone button is the only way out of the prompt, so accept takes it unless it declines.
23- return labels . count == 1 && ! isDismissButton ( labels [ 0 ] ) ? 0 : nil
24- }
25- if let decline = labels . firstIndex ( where : { isDismissButton ( $0 ) } ) {
26- return decline
30+ let preferences = action == " accept " ? acceptPreferences : dismissPreferences
31+ for pattern in preferences {
32+ let matchingIndices = labels . indices . filter { matches ( labels [ $0 ] , pattern : pattern ) }
33+ if matchingIndices . count > 1 { return nil }
34+ if let index = matchingIndices . first { return index }
2735 }
28- return popupMarkerSurface ? labels . indices . last : nil
36+ return action != " accept " && popupMarkerSurface ? labels . indices . last : nil
2937 }
3038
3139 static func isAcceptButton ( _ label : String ) -> Bool {
32- let normalized = normalize ( label )
33- return [
34- " ok " ,
35- " allow " ,
36- " yes " ,
37- " continue " ,
38- " done " ,
39- " open " ,
40- " open settings "
41- ] . contains ( normalized ) || normalized . hasPrefix ( " confirm " )
42- }
43-
44- static func isDismissButton ( _ label : String ) -> Bool {
45- [
46- " cancel " ,
47- " close " ,
48- " dismiss " ,
49- " don't allow " ,
50- " don’t allow " ,
51- " not now " ,
52- " no " ,
53- " keep browsing " ,
54- " later " ,
55- " ask app not to track "
56- ] . contains ( normalize ( label ) )
40+ acceptPreferences . contains { matches ( label , pattern : $0 ) }
5741 }
5842
59- private static func normalize ( _ label : String ) -> String {
60- label . trimmingCharacters ( in : . whitespacesAndNewlines ) . lowercased ( )
43+ private static func matches ( _ label : String , pattern : String ) -> Bool {
44+ label . trimmingCharacters ( in : . whitespacesAndNewlines )
45+ . range ( of : pattern , options : [ . regularExpression , . caseInsensitive ] ) != nil
6146 }
6247}
0 commit comments