Skip to content

Commit 1949cf1

Browse files
committed
Add Android and iOS native libraries
Kotlin library (packages/android) wraps Android WebView with Snapfill script injection and message bridge. Swift package (packages/ios) wraps WKWebView with WKUserScript injection and WKScriptMessageHandler bridge. Both use fillScriptTemplate from @snapfill/core.
1 parent a5feb49 commit 1949cf1

17 files changed

Lines changed: 1609 additions & 0 deletions

packages/android/build.gradle.kts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
plugins {
2+
id("com.android.library")
3+
id("org.jetbrains.kotlin.android")
4+
}
5+
6+
android {
7+
namespace = "com.snapfill"
8+
compileSdk = 35
9+
10+
defaultConfig {
11+
minSdk = 24
12+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
13+
}
14+
15+
compileOptions {
16+
sourceCompatibility = JavaVersion.VERSION_11
17+
targetCompatibility = JavaVersion.VERSION_11
18+
}
19+
20+
kotlinOptions {
21+
jvmTarget = "11"
22+
}
23+
}
24+
25+
dependencies {
26+
testImplementation("junit:junit:4.13.2")
27+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
</manifest>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
(function(){
2+
'use strict';
3+
var mappings=__SNAPFILL_MAPPINGS__;
4+
if(!window.__snapfillFieldMap){console.warn('[snapfill] No field map found. Inject snapfillScript first.');return;}
5+
var nIS=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value');nIS=nIS&&nIS.set;
6+
var nTS=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value');nTS=nTS&&nTS.set;
7+
var nSS=Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,'selectedIndex');nSS=nSS&&nSS.set;
8+
function de(el,evts){evts.forEach(function(n){el.dispatchEvent(new Event(n,{bubbles:true,cancelable:true}));});}
9+
function fillIn(el,v){el.focus();de(el,['focus','focusin']);
10+
if(el instanceof HTMLInputElement&&nIS)nIS.call(el,v);else if(el instanceof HTMLTextAreaElement&&nTS)nTS.call(el,v);else el.value=v;
11+
de(el,['input','change']);setTimeout(function(){el.blur();de(el,['blur','focusout']);},50);}
12+
function fillSel(el,v){var o=el.options,mi=-1;
13+
for(var i=0;i<o.length;i++)if(o[i].value.toLowerCase()===v.toLowerCase()){mi=i;break;}
14+
if(mi===-1)for(var j=0;j<o.length;j++)if((o[j].textContent||'').trim().toLowerCase()===v.toLowerCase()){mi=j;break;}
15+
if(mi===-1)for(var k=0;k<o.length;k++)if((o[k].textContent||'').trim().toLowerCase().indexOf(v.toLowerCase())>=0){mi=k;break;}
16+
if(mi>=0){el.focus();de(el,['focus','focusin']);if(nSS)nSS.call(el,mi);else el.selectedIndex=mi;de(el,['input','change']);
17+
setTimeout(function(){el.blur();de(el,['blur','focusout']);},50);}}
18+
function fillCB(el,v){var c=v==='true'||v==='1'||v==='yes'||v==='on';if(el.checked!==c){el.focus();el.checked=c;de(el,['click','input','change']);}}
19+
function fill(el,v){if(!el||!v)return;if(el instanceof HTMLSelectElement)fillSel(el,v);
20+
else if(el instanceof HTMLInputElement&&(el.type==='checkbox'||el.type==='radio'))fillCB(el,v);else fillIn(el,v);}
21+
if(window.__snapfillFieldMap.has('fullName')&&!mappings.fullName){
22+
var parts=[mappings.firstName,mappings.middleName,mappings.lastName].filter(Boolean);
23+
if(parts.length)mappings.fullName=parts.join(' ');}
24+
var filled=0,failed=[];
25+
Object.keys(mappings).forEach(function(f){var el=window.__snapfillFieldMap.get(f);
26+
if(el){fill(el,mappings[f]);filled++;}else{failed.push(f);}});
27+
var result={filled:filled,total:Object.keys(mappings).length,failed:failed};
28+
var msg=JSON.stringify({type:'formFillComplete',result:result});
29+
if(window.ReactNativeWebView&&window.ReactNativeWebView.postMessage)window.ReactNativeWebView.postMessage(msg);
30+
else if(window.parent!==window)window.parent.postMessage({snapfill:true,type:'formFillComplete',result:result},'*');
31+
})();
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
(function(){
2+
'use strict';
3+
if(window.__snapfillDetectorInit)return;
4+
window.__snapfillDetectorInit=true;
5+
6+
var AC={
7+
'given-name':'firstName','first-name':'firstName','family-name':'lastName','last-name':'lastName',
8+
'honorific-prefix':'honorific','name':'firstName',
9+
'cc-number':'ccNumber','cc-name':'ccName','cc-exp':'ccExpiry',
10+
'cc-exp-month':'ccExpiryMonth','cc-exp-year':'ccExpiryYear','cc-csc':'ccCCV','cc-type':'ccType',
11+
email:'email',tel:'phoneNumber','tel-national':'phoneNumber','tel-country-code':'phoneCountryCode',
12+
'address-line1':'postalAddressLine1','address-line2':'postalAddressLine2',
13+
'address-level2':'postalSuburb','address-level1':'postalState',
14+
'postal-code':'postalPostCode',country:'postalCountry','country-name':'postalCountry',
15+
'street-address':'postalAddressLine1'
16+
};
17+
18+
var RX=[
19+
{p:/card.?num|cc.?num|cc-?number|\bpan\b/i,f:'ccNumber'},
20+
{p:/card.?name|name.?on.?card|cc.?name|cardholder/i,f:'ccName'},
21+
{p:/cvv|cvc|ccv|security.?code|card.?code|card.?verif|cvd|csv/i,f:'ccCCV'},
22+
{p:/exp.*month|cc.?month|card.?month/i,f:'ccExpiryMonth'},
23+
{p:/exp.*year|cc.?year|card.?year/i,f:'ccExpiryYear'},
24+
{p:/expir.*dat|exp.?dat|cc.?exp(?!.*(?:month|year))/i,f:'ccExpiry'},
25+
{p:/card.?type|cc.?type|payment.?method/i,f:'ccType'},
26+
{p:/first.?name|given.?name|\bfname\b|Name_First|Name\.first/i,f:'firstName'},
27+
{p:/last.?name|family.?name|surname|\blname\b|Name_Last|Name\.last/i,f:'lastName'},
28+
{p:/middle.?name|Name_Middle|Name\.middle|\bmname\b|middle.?initial/i,f:'middleName'},
29+
{p:/honorific|Name_Prefix|name.?prefix|salutation/i,f:'honorific'},
30+
{p:/name.?suffix|Name_Suffix|\bsuffix\b/i,f:'nameSuffix'},
31+
{p:/full.?name|your.?name|customer.?name|\bname\b/i,f:'fullName'},
32+
{p:/e.?mail/i,f:'email'},
33+
{p:/phone.?country|phone.?code|dial.?code|tel.?code/i,f:'phoneCountryCode'},
34+
{p:/phone|mobile|\btel\b|telephone|Telecom.?Phone/i,f:'phoneNumber'},
35+
{p:/street.?line.?2|address.?2|address.?line.?2|\bapt\b|\bsuite\b|\bunit\b|\baddr2\b/i,f:'postalAddressLine2'},
36+
{p:/street.?line.?1|address.?1|address.?line.?1|street.?addr|\baddr1\b/i,f:'postalAddressLine1'},
37+
{p:/street.?line|street.?addr|\baddress\b/i,f:'postalAddressLine1'},
38+
{p:/street.?num/i,f:'postalStreetNumber'},
39+
{p:/street.?name/i,f:'postalStreetName'},
40+
{p:/street.?type/i,f:'postalStreetType'},
41+
{p:/city|suburb|\btown\b|locality|Postal_City/i,f:'postalSuburb'},
42+
{p:/\bstate\b|province|\bregion\b|StateProv/i,f:'postalState'},
43+
{p:/\bzip\b|postal.?code|postcode|\bpost.?code\b|PostalCode/i,f:'postalPostCode'},
44+
{p:/country.?code|country.?name|\bcountry\b/i,f:'postalCountry'}
45+
];
46+
47+
var TM={email:'email',tel:'phoneNumber'};
48+
49+
function isBill(el){
50+
var n=el.getAttribute('name')||'',id=el.id||'';
51+
if(/bill/i.test(n)||/bill/i.test(id))return true;
52+
var p=el.parentElement;
53+
for(var i=0;i<5&&p;i++){
54+
var c=p.className||'',pi=p.id||'',pn=p.getAttribute('name')||'';
55+
if(/bill/i.test(c)||/bill/i.test(pi)||/bill/i.test(pn))return true;
56+
if(p.tagName==='FIELDSET'){var lg=p.querySelector('legend');if(lg&&/bill/i.test(lg.textContent))return true;}
57+
p=p.parentElement;}
58+
return false;}
59+
60+
function bc(f,el){if(f&&f.startsWith('postal')&&isBill(el))return f.replace('postal','billing');return f;}
61+
62+
function labelOf(el){
63+
if(el.id){try{var l=document.querySelector('label[for="'+CSS.escape(el.id)+'"]');if(l)return l.textContent||'';}catch(e){}}
64+
var pl=el.closest('label');if(pl)return pl.textContent||'';
65+
var lb=el.getAttribute('aria-labelledby');if(lb){var le=document.getElementById(lb);if(le)return le.textContent||'';}
66+
var pv=el.previousElementSibling;
67+
if(pv&&(pv.tagName==='LABEL'||pv.tagName==='SPAN'||pv.tagName==='TD'))return pv.textContent||'';
68+
if(el.parentElement&&el.parentElement.tagName==='TD'){var pt=el.parentElement.previousElementSibling;if(pt&&pt.tagName==='TD')return pt.textContent||'';}
69+
return '';}
70+
71+
function byAC(el){
72+
if(el.type==='hidden'||el.type==='submit'||el.type==='button'||el.type==='radio'||el.type==='checkbox')return null;
73+
if(el.disabled||el.readOnly)return null;
74+
var ac=(el.getAttribute('autocomplete')||'').trim().toLowerCase();
75+
if(!ac||ac==='off'||ac==='on')return null;
76+
var tk=ac.split(/\s+/),sec=null,ft=null;
77+
for(var i=0;i<tk.length;i++){if(tk[i]==='shipping'||tk[i]==='billing')sec=tk[i];else if(AC[tk[i]])ft=tk[i];}
78+
if(!ft)return null;
79+
var m=AC[ft];
80+
if(sec==='billing'&&m.startsWith('postal'))m=m.replace('postal','billing');
81+
if(!sec)m=bc(m,el)||m;
82+
return m;}
83+
84+
function byRX(el){
85+
if(el.type==='hidden'||el.type==='submit'||el.type==='button'||el.type==='radio'||el.type==='checkbox')return null;
86+
if(el.disabled||el.readOnly)return null;
87+
var s=[el.getAttribute('name')||'',el.getAttribute('id')||'',el.getAttribute('placeholder')||'',el.getAttribute('aria-label')||''].join(' ');
88+
if(s.trim()){for(var i=0;i<RX.length;i++)if(RX[i].p.test(s))return bc(RX[i].f,el)||RX[i].f;}
89+
var t=(el.getAttribute('type')||'').toLowerCase();if(TM[t])return TM[t];
90+
var lt=labelOf(el);
91+
if(lt.trim()){for(var j=0;j<RX.length;j++)if(RX[j].p.test(lt))return bc(RX[j].f,el)||RX[j].f;}
92+
return null;}
93+
94+
function isVis(el){if(!el)return false;var s=window.getComputedStyle(el);return s.display!=='none'&&s.visibility!=='hidden'&&s.opacity!=='0'&&el.offsetParent!==null;}
95+
96+
window.__snapfillFieldMap=new Map();
97+
98+
function scan(root){
99+
var els=(root||document).querySelectorAll('input, select, textarea');
100+
var df=new Set();
101+
window.__snapfillFieldMap.clear();
102+
els.forEach(function(el){var f=byAC(el);if(f){df.add(f);if(!window.__snapfillFieldMap.has(f)||!isVis(window.__snapfillFieldMap.get(f)))window.__snapfillFieldMap.set(f,el);}});
103+
els.forEach(function(el){var f=byRX(el);if(f){df.add(f);if(!window.__snapfillFieldMap.has(f)||!isVis(window.__snapfillFieldMap.get(f)))window.__snapfillFieldMap.set(f,el);}});
104+
return Array.from(df);}
105+
106+
var dt=null,lr='';
107+
function report(){clearTimeout(dt);dt=setTimeout(function(){
108+
var f=scan(document),k=f.sort().join(',');
109+
if(k!==lr){lr=k;
110+
var msg=JSON.stringify({type:'formDetected',fields:f});
111+
if(window.ReactNativeWebView&&window.ReactNativeWebView.postMessage)window.ReactNativeWebView.postMessage(msg);
112+
else if(window.parent!==window)window.parent.postMessage({snapfill:true,type:'formDetected',fields:f},'*');
113+
}},500);}
114+
115+
var md=null;
116+
new MutationObserver(function(){clearTimeout(md);md=setTimeout(report,300);}).observe(document.body,{childList:true,subtree:true});
117+
report();
118+
})();
119+
(function(){
120+
'use strict';
121+
if(window.__snapfillCartInit)return;
122+
window.__snapfillCartInit=true;
123+
124+
function p2c(s){
125+
if(typeof s==='number')return Math.round(s*100);
126+
if(!s||typeof s!=='string')return 0;
127+
var c=s.replace(/[^0-9.,\-]/g,'').trim();if(!c)return 0;
128+
var lc=c.lastIndexOf(','),lp=c.lastIndexOf('.');
129+
var a;if(lc>lp)a=parseFloat(c.replace(/\./g,'').replace(',','.'));else a=parseFloat(c.replace(/,/g,''));
130+
return isNaN(a)?0:Math.round(a*100);}
131+
132+
function dCur(s,m){
133+
if(m)return m.toUpperCase();if(!s||typeof s!=='string')return null;
134+
if(/A\$/.test(s))return'AUD';if(/NZ\$/.test(s))return'NZD';if(/US\$/.test(s))return'USD';
135+
if(/CA\$|C\$/.test(s))return'CAD';if(/£/.test(s))return'GBP';if(//.test(s))return'EUR';if(/¥/.test(s))return'JPY';
136+
var h=window.location.hostname;
137+
if(/\.co\.uk$|\.uk$/.test(h))return'GBP';if(/\.com\.au$|\.au$/.test(h))return'AUD';
138+
if(/\.co\.nz$|\.nz$/.test(h))return'NZD';if(/\.ca$/.test(h))return'CAD';
139+
return null;}
140+
141+
function fromLD(){
142+
var ss=document.querySelectorAll('script[type="application/ld+json"]'),ps=[],cur=null,tot=0;
143+
ss.forEach(function(s){try{var d=JSON.parse(s.textContent),items=Array.isArray(d)?d:d['@graph']?d['@graph']:[d];
144+
items.forEach(function(it){var t=it['@type'];
145+
if(t==='Product'||t==='IndividualProduct'){var o=it.offers||it.offer||{};if(Array.isArray(o))o=o[0]||{};
146+
var pr=o.price||it.price||0,pc=o.priceCurrency||it.priceCurrency||null;if(pc)cur=pc;
147+
var img=it.image;var iu=typeof img==='string'?img:Array.isArray(img)?img[0]||null:img&&img.url||null;
148+
ps.push({name:it.name||null,quantity:1,itemPrice:p2c(pr),lineTotal:p2c(pr),url:it.url||null,imageUrl:iu});}
149+
if(t==='Order'||t==='Invoice'){tot=p2c((it.totalPaymentDue&&it.totalPaymentDue.value)||it.total||0);
150+
cur=(it.totalPaymentDue&&it.totalPaymentDue.priceCurrency)||it.priceCurrency||cur;}});}catch(e){}});
151+
if(!ps.length)return null;
152+
if(!tot)tot=ps.reduce(function(s,p){return s+p.lineTotal;},0);
153+
return{total:tot,currency:cur,products:ps,source:'json-ld'};}
154+
155+
function fromMD(){
156+
var pe=document.querySelectorAll('[itemtype*="schema.org/Product"]');if(!pe.length)return null;
157+
var ps=[],cur=null;
158+
pe.forEach(function(el){var ne=el.querySelector('[itemprop="name"]'),pe2=el.querySelector('[itemprop="price"]'),
159+
ce=el.querySelector('[itemprop="priceCurrency"]'),ie=el.querySelector('[itemprop="image"]'),ue=el.querySelector('[itemprop="url"]');
160+
var pr=pe2?pe2.getAttribute('content')||pe2.textContent:'0';
161+
var dc=ce?ce.getAttribute('content')||ce.textContent:null;if(dc)cur=dc;
162+
ps.push({name:ne?ne.textContent.trim():null,quantity:1,itemPrice:p2c(pr),lineTotal:p2c(pr),
163+
url:ue?ue.getAttribute('href')||ue.getAttribute('content'):null,
164+
imageUrl:ie?ie.getAttribute('src')||ie.getAttribute('content'):null});});
165+
if(!ps.length)return null;
166+
var tot=ps.reduce(function(s,p){return s+p.lineTotal;},0);
167+
return{total:tot,currency:cur,products:ps,source:'microdata'};}
168+
169+
function fromOG(){
170+
var ot=document.querySelector('meta[property="og:type"]');
171+
if(!ot||ot.getAttribute('content')!=='product')return null;
172+
var ti=document.querySelector('meta[property="og:title"]'),
173+
pr=document.querySelector('meta[property="product:price:amount"]'),
174+
cu=document.querySelector('meta[property="product:price:currency"]'),
175+
im=document.querySelector('meta[property="og:image"]'),
176+
ur=document.querySelector('meta[property="og:url"]');
177+
if(!pr)return null;
178+
var p=p2c(pr.getAttribute('content')),c=cu?cu.getAttribute('content'):null;
179+
return{total:p,currency:c,products:[{name:ti?ti.getAttribute('content'):null,quantity:1,itemPrice:p,lineTotal:p,
180+
url:ur?ur.getAttribute('content'):null,imageUrl:im?im.getAttribute('content'):null}],source:'opengraph'};}
181+
182+
function fromDOM(){
183+
var cs=['[class*="cart"]','[class*="basket"]','[class*="order-summary"]','[class*="checkout-summary"]',
184+
'[id*="cart"]','[id*="basket"]','[id*="order-summary"]','[data-testid*="cart"]','[data-testid*="order"]'];
185+
var cc=null;for(var i=0;i<cs.length;i++){cc=document.querySelector(cs[i]);if(cc)break;}
186+
if(!cc)return null;
187+
var rx=/(?:[$£])\s*[\d,]+\.?\d{0,2}/g;
188+
var li=cc.querySelectorAll('[class*="item"],[class*="product"],[class*="line"],li,tr');
189+
var ps=[],seen=new Set();
190+
li.forEach(function(it){
191+
var ne=it.querySelector('[class*="name"],[class*="title"],[class*="description"],h2,h3,h4,a')||it.querySelector('td:first-child,span:first-child');
192+
var nm=ne?ne.textContent.trim():null;
193+
if(!nm||nm.length<2||nm.length>200||seen.has(nm))return;
194+
var pm=it.textContent.match(rx);if(!pm||!pm.length)return;
195+
var ip=p2c(pm[pm.length-1]);if(ip<=0)return;
196+
var qe=it.querySelector('input[type="number"],[class*="qty"],[class*="quantity"]');
197+
var q=qe?parseInt(qe.value||qe.textContent,10)||1:1;
198+
var ie=it.querySelector('img');
199+
seen.add(nm);
200+
ps.push({name:nm.substring(0,200),quantity:q,itemPrice:ip,lineTotal:ip*q,url:null,imageUrl:ie?ie.src:null});});
201+
if(!ps.length)return null;
202+
var te=cc.querySelector('[class*="total"]:not([class*="sub"]),[class*="grand-total"],[class*="order-total"]');
203+
var tot=0;if(te){var tp=te.textContent.match(rx);if(tp)tot=p2c(tp[tp.length-1]);}
204+
if(!tot)tot=ps.reduce(function(s,p){return s+p.lineTotal;},0);
205+
var at=cc.textContent,ap=at.match(rx),cur=ap?dCur(ap[0],null):null;
206+
return{total:tot,currency:cur,products:ps,source:'dom'};}
207+
208+
function detect(){return fromLD()||fromMD()||fromOG()||fromDOM();}
209+
window.__snapfillDetectCart=detect;
210+
211+
var dt=null,lr='';
212+
function report(){clearTimeout(dt);dt=setTimeout(function(){
213+
var c=detect();if(!c)return;
214+
var k=JSON.stringify(c);if(k!==lr){lr=k;
215+
var msg=JSON.stringify({type:'cartDetected',cart:{total:c.total,currency:c.currency,products:c.products}});
216+
if(window.ReactNativeWebView&&window.ReactNativeWebView.postMessage)window.ReactNativeWebView.postMessage(msg);
217+
else if(window.parent!==window)window.parent.postMessage({snapfill:true,type:'cartDetected',cart:{total:c.total,currency:c.currency,products:c.products}},'*');
218+
}},500);}
219+
220+
var md=null;
221+
new MutationObserver(function(){clearTimeout(md);md=setTimeout(report,500);}).observe(document.body,{childList:true,subtree:true,characterData:true});
222+
report();
223+
})();
224+
(function(){
225+
'use strict';
226+
if(window.__snapfillValueInit)return;
227+
window.__snapfillValueInit=true;
228+
229+
var ST=['password'];
230+
var SP=/ssn|social.?security|tax.?id/i;
231+
function isSens(el){if(!el)return false;var t=(el.getAttribute('type')||'').toLowerCase();if(ST.indexOf(t)>=0)return true;
232+
var s=[el.getAttribute('name')||'',el.getAttribute('id')||'',el.getAttribute('autocomplete')||''].join(' ');return SP.test(s);}
233+
234+
var dt=null,attached=new Set();
235+
function capture(){clearTimeout(dt);dt=setTimeout(function(){
236+
if(!window.__snapfillFieldMap)return;
237+
var m={},h=false;
238+
window.__snapfillFieldMap.forEach(function(el,f){if(isSens(el))return;
239+
var v='';if(el instanceof HTMLSelectElement){var s=el.options[el.selectedIndex];v=s?s.value||s.textContent.trim():'';}
240+
else if(el instanceof HTMLInputElement&&(el.type==='checkbox'||el.type==='radio'))v=el.checked?'true':'false';
241+
else v=el.value||'';
242+
if(v){m[f]=v;h=true;}});
243+
if(h){
244+
var msg=JSON.stringify({type:'valuesCaptured',mappings:m});
245+
if(window.ReactNativeWebView&&window.ReactNativeWebView.postMessage)window.ReactNativeWebView.postMessage(msg);
246+
else if(window.parent!==window)window.parent.postMessage({snapfill:true,type:'valuesCaptured',mappings:m},'*');
247+
}},1000);}
248+
249+
function attach(){if(!window.__snapfillFieldMap)return;
250+
window.__snapfillFieldMap.forEach(function(el){
251+
if(attached.has(el)||isSens(el))return;
252+
el.addEventListener('input',capture);el.addEventListener('change',capture);attached.add(el);});}
253+
254+
window.__snapfillAttachCapture=attach;
255+
window.__snapfillCaptureNow=capture;
256+
setTimeout(attach,600);
257+
})();

0 commit comments

Comments
 (0)